-
Notifications
You must be signed in to change notification settings - Fork 253
fix: harden secrets at rest using envelope encryption #6211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
alespour
wants to merge
25
commits into
master
Choose a base branch
from
fix/issue-1046
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
efba8ea
feat: add master key config and validation
alespour 787f11d
feat(kv): add DEK/secret crypto primitives
alespour 989478e
feat(kv): add secret encoding metadata and DEK-based source/server pe…
alespour 64d7fef
feat(kv): bootstrap wrapped DEK and load secret DEK on startup
alespour 10c6c3c
feat(kv): bootstrap wrapped DEK and load secret DEK on startup
alespour 0d93861
feat(kv): auto-migrate legacy plaintext secrets after DEK init
alespour 2fe6657
feat(kv): fail startup when encrypted secrets exist without key state
alespour 127af0b
feat(chronoctl): add gen-secrets-master-key command
alespour 5321b15
feat(secrets): add DEK rewrap workflow and chronoctl rotation command
alespour c40e280
feat(secrets): add DEK rewrap workflow and chronoctl rotation command
alespour 11c5f3c
feat(secrets): add disable-secrets-encryption workflow and chronoctl …
alespour bb9e50c
docs: document secrets key generation, rewrap, and disable workflows
alespour 941675a
test: add tests
alespour 521e112
fix(server): redact source database and management tokens from API re…
alespour 73a0949
Revert "test: add tests"
alespour afeaf65
Revert "Revert "test: add tests""
alespour 2743c05
fix: zero key material
alespour 2db014a
refactor: remove unnecessary wrapper
alespour 1fb3586
fix: harden output file checks
alespour 3bd403c
fix: secrets are redacted now
alespour 5b0702b
docs: better phrase for encryption rollback command
alespour b8afbfe
docs: update CHANGELOG
alespour 2bb6296
fix: error message
alespour b17a357
fix: fail fast when --bolt-path DB is missing for secrets ops
alespour 80ef3d7
Merge branch 'master' into fix/issue-1046
alespour File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| ) | ||
|
|
||
| func init() { | ||
| parser.AddCommand("disable-secrets-encryption", | ||
| "Disable secrets encryption and rewrite persisted secrets to plaintext.", | ||
| "Decrypt stored secrets in BoltDB and remove wrapped DEK. Requires current secrets master key.", | ||
| &disableSecretsEncryptionCommand{}) | ||
| } | ||
|
|
||
| type disableSecretsEncryptionCommand struct { | ||
| BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"` | ||
|
|
||
| SecretsMasterKey string `long:"secrets-master-key" description:"Current base64-encoded 32-byte secrets master key." env:"SECRETS_MASTER_KEY"` | ||
| SecretsMasterKeyFile string `long:"secrets-master-key-file" description:"Path to file containing current base64-encoded 32-byte secrets master key." env:"SECRETS_MASTER_KEY_FILE"` | ||
| } | ||
|
|
||
| func (c *disableSecretsEncryptionCommand) Execute(args []string) error { | ||
| key, err := loadCLISecretsMasterKey(c.SecretsMasterKey, c.SecretsMasterKeyFile, "current") | ||
| if err != nil { | ||
| errExit(err) | ||
| } | ||
|
|
||
| store, err := NewBoltClient(c.BoltPath) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer store.Close() | ||
|
|
||
| svc, err := NewService(store) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if err := svc.DisableSecretEncryption(context.Background(), key); err != nil { | ||
| errExit(err) | ||
| } | ||
|
|
||
| fmt.Println("Successfully disabled secrets encryption and removed wrapped DEK") | ||
| return nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "crypto/rand" | ||
| "encoding/base64" | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
|
|
||
| flags "github.com/jessevdk/go-flags" | ||
| ) | ||
|
|
||
| func init() { | ||
| parser.AddCommand("gen-secrets-master-key", | ||
| "Generate secrets master key.", | ||
| "Generate base64-encoded 32-byte key for Chronograf --secrets-master-key / --secrets-master-key-file.", | ||
| &genSecretsMasterKeyCommand{}) | ||
| } | ||
|
|
||
| type genSecretsMasterKeyCommand struct { | ||
| Out flags.Filename `long:"out" description:"File to save the generated key (0600 permissions). If omitted, key is printed to stdout."` | ||
| } | ||
|
|
||
| func generateSecretsMasterKey() (string, error) { | ||
| raw := make([]byte, 32) | ||
| if _, err := rand.Read(raw); err != nil { | ||
| return "", err | ||
| } | ||
| return base64.StdEncoding.EncodeToString(raw), nil | ||
| } | ||
|
|
||
| func (c *genSecretsMasterKeyCommand) Execute(args []string) error { | ||
| key, err := generateSecretsMasterKey() | ||
| if err != nil { | ||
| errExit(err) | ||
| } | ||
|
|
||
| if c.Out == "" { | ||
| fmt.Println(key) | ||
| return nil | ||
| } | ||
|
|
||
| if _, err := os.Stat(string(c.Out)); err == nil { | ||
| errExit(errors.New("Specify non-existant file to write to.")) | ||
| } else if !errors.Is(err, os.ErrNotExist) { | ||
| errExit(err) | ||
| } | ||
|
|
||
| if err := os.WriteFile(string(c.Out), []byte(key+"\n"), 0600); err != nil { | ||
| errExit(err) | ||
| } | ||
|
|
||
| fmt.Printf("Secrets master key generated and saved at %s\n", c.Out) | ||
| return nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/base64" | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "strings" | ||
| ) | ||
|
|
||
| func init() { | ||
| parser.AddCommand("rewrap-secrets-master-key", | ||
| "Rewrap stored DEK with new secrets master key.", | ||
| "Rotate Chronograf secrets master key by rewrapping the stored DEK in BoltDB.", | ||
| &rewrapSecretsMasterKeyCommand{}) | ||
| } | ||
|
|
||
| type rewrapSecretsMasterKeyCommand struct { | ||
| BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"` | ||
|
|
||
| OldSecretsMasterKey string `long:"old-secrets-master-key" description:"Current base64-encoded 32-byte secrets master key." env:"OLD_SECRETS_MASTER_KEY"` | ||
| OldSecretsMasterKeyFile string `long:"old-secrets-master-key-file" description:"Path to file containing current base64-encoded 32-byte secrets master key." env:"OLD_SECRETS_MASTER_KEY_FILE"` | ||
|
|
||
| NewSecretsMasterKey string `long:"new-secrets-master-key" description:"New base64-encoded 32-byte secrets master key." env:"NEW_SECRETS_MASTER_KEY"` | ||
| NewSecretsMasterKeyFile string `long:"new-secrets-master-key-file" description:"Path to file containing new base64-encoded 32-byte secrets master key." env:"NEW_SECRETS_MASTER_KEY_FILE"` | ||
| } | ||
|
|
||
| func (c *rewrapSecretsMasterKeyCommand) Execute(args []string) error { | ||
| oldKey, err := loadCLISecretsMasterKey(c.OldSecretsMasterKey, c.OldSecretsMasterKeyFile, "old") | ||
| if err != nil { | ||
| errExit(err) | ||
| } | ||
| newKey, err := loadCLISecretsMasterKey(c.NewSecretsMasterKey, c.NewSecretsMasterKeyFile, "new") | ||
| if err != nil { | ||
| errExit(err) | ||
| } | ||
|
|
||
| store, err := NewBoltClient(c.BoltPath) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer store.Close() | ||
|
|
||
| svc, err := NewService(store) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if err := svc.RewrapSecretDEK(context.Background(), oldKey, newKey); err != nil { | ||
| errExit(err) | ||
| } | ||
|
|
||
| fmt.Println("Successfully rewrapped DEK with new secrets master key") | ||
| return nil | ||
| } | ||
|
|
||
| func loadCLISecretsMasterKey(value, filePath, label string) ([]byte, error) { | ||
| if value != "" && filePath != "" { | ||
| return nil, fmt.Errorf("%s secrets master key must be provided by value or file, not both", label) | ||
| } | ||
| if value == "" && filePath == "" { | ||
| return nil, fmt.Errorf("%s secrets master key is required", label) | ||
| } | ||
|
|
||
| raw := value | ||
| if filePath != "" { | ||
| b, err := os.ReadFile(filePath) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("reading %s secrets master key file: %w", label, err) | ||
| } | ||
| raw = string(b) | ||
| } | ||
| raw = strings.TrimSpace(raw) | ||
| if raw == "" { | ||
| return nil, fmt.Errorf("%s secrets master key is empty", label) | ||
| } | ||
|
|
||
| key, err := base64.StdEncoding.DecodeString(raw) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("decoding %s secrets master key: %w", label, err) | ||
| } | ||
| if len(key) != 32 { | ||
| return nil, errors.New("secrets master key must decode to 32 bytes") | ||
| } | ||
| return key, nil | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.