From cc04cd65813d05a0fc11114439623291453104d7 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Tue, 16 Jan 2018 11:41:05 -0800 Subject: [PATCH] Use per stack key for local stacks instead of per project In the Pulumi Cloud, there is no guarantee that two stacks will share the same encryption key. This means that encrypted config can not be shared across stacks (in the Pulumi.yaml) file. To mimic this behavior in the local experience, we now use a unique key per stack. When upgrading an existing project, for any stack with existing secrets, we copy the existing key into this stack. Future stacks will get thier own encryption key. This strikes a balance between expediency of implementation, the end user UX and not having to make a breaking change. As part of this change, I have introduced a CHANGELOG.md file in the root of the repository and added a small note about the change to it. Fixes #769 --- CHANGELOG.md | 12 +++++++ cmd/config.go | 1 + pkg/backend/local/backend.go | 2 +- pkg/backend/local/crypto.go | 66 ++++++++++++++++++++++++++++++------ pkg/backend/local/state.go | 2 +- pkg/pack/package.go | 6 ++-- 6 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..cb56e63e5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +## v0.10.0 + +### Added + +### Changed + + - For local stacks, Pulumi now uses a seperate encryption key for each stack instead of one shared for all stacks, to + encrypt secrets. You are now able to use a different passphrase between two stacks. In addition, the top level + `encryptionsalt` member of the `Pulumi.yaml` is removed and salts are stored per stack in `Pulumi.yaml`. Pulumi will + automatically re-use the existing key for any local stacks in the Pulumi.yaml file which have encrypted, but future + stacks will have new keys generated. There is no impact to stacks deployed using the Pulumi Cloud. + diff --git a/cmd/config.go b/cmd/config.go index be93243c4..b80879365 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -372,6 +372,7 @@ func deleteAllStackConfiguration(stackName tokens.QName) error { if info, has := pkg.Stacks[stackName]; has { info.Config = nil + info.EncryptionSalt = "" pkg.Stacks[stackName] = info } diff --git a/pkg/backend/local/backend.go b/pkg/backend/local/backend.go index 11ef01ab8..e71a8c96c 100644 --- a/pkg/backend/local/backend.go +++ b/pkg/backend/local/backend.go @@ -100,7 +100,7 @@ func (b *localBackend) RemoveStack(stackName tokens.QName, force bool) (bool, er } func (b *localBackend) GetStackCrypter(stackName tokens.QName) (config.Crypter, error) { - return symmetricCrypter() + return symmetricCrypter(stackName) } func (b *localBackend) Preview(stackName tokens.QName, pkg *pack.Package, root string, debug bool, diff --git a/pkg/backend/local/crypto.go b/pkg/backend/local/crypto.go index d15c02e5b..611cb1e21 100644 --- a/pkg/backend/local/crypto.go +++ b/pkg/backend/local/crypto.go @@ -11,7 +11,9 @@ import ( "github.com/pkg/errors" + "github.com/pulumi/pulumi/pkg/pack" "github.com/pulumi/pulumi/pkg/resource/config" + "github.com/pulumi/pulumi/pkg/tokens" "github.com/pulumi/pulumi/pkg/util/cmdutil" "github.com/pulumi/pulumi/pkg/util/contract" "github.com/pulumi/pulumi/pkg/workspace" @@ -25,33 +27,73 @@ func readPassphrase(prompt string) (string, error) { } // defaultCrypter gets the right value encrypter/decrypter given the project configuration. -func defaultCrypter(cfg config.Map) (config.Crypter, error) { +func defaultCrypter(stackName tokens.QName, cfg config.Map) (config.Crypter, error) { // If there is no config, we can use a standard panic crypter. if !cfg.HasSecureValue() { return config.NewPanicCrypter(), nil } // Otherwise, we will use an encrypted one. - return symmetricCrypter() + return symmetricCrypter(stackName) } -// SymmetricCrypter gets the right value encrypter/decrypter for this project. -func symmetricCrypter() (config.Crypter, error) { +// symmetricCrypter gets the right value encrypter/decrypter for this project. +func symmetricCrypter(stackName tokens.QName) (config.Crypter, error) { // First, read the package to see if we've got a key. pkg, err := workspace.GetPackage() if err != nil { return nil, err } - // If there's already a salt, use it. + if pkg.Stacks == nil { + pkg.Stacks = make(map[tokens.QName]pack.StackInfo) + } + + // If we have a top level EncryptionSalt, we are reading an older version of Pulumi.yaml where local stacks shared + // a key. To migrate, we'll simply move this salt to any local stack that has encrypted config and then unset the + // package wide salt. if pkg.EncryptionSalt != "" { - phrase, phraseErr := readPassphrase("Enter your passphrase to unlock config/secrets\n" + - " (set PULUMI_CONFIG_PASSPHRASE to remember)") - if phraseErr != nil { - return nil, phraseErr + localStacks, stacksErr := getLocalStacks() + if stacksErr != nil { + return nil, stacksErr } - return symmetricCrypterFromPhraseAndState(phrase, pkg.EncryptionSalt) + for _, localStack := range localStacks { + stackInfo := pkg.Stacks[localStack] + contract.Assertf(stackInfo.EncryptionSalt == "", "package and stack %v had an encryption salt", localStack) + + if stackInfo.Config.HasSecureValue() { + stackInfo.EncryptionSalt = pkg.EncryptionSalt + } + + pkg.Stacks[localStack] = stackInfo + } + + pkg.EncryptionSalt = "" + + // Now store the result on the package and save it. + if err = workspace.SavePackage(pkg); err != nil { + return nil, err + } + } + + // If there's already a salt for the local stack, we can just use that. + if info, has := pkg.Stacks[stackName]; has { + if info.EncryptionSalt != "" { + + phrase, phraseErr := readPassphrase("Enter your passphrase to unlock config/secrets\n" + + " (set PULUMI_CONFIG_PASSPHRASE to remember)") + if phraseErr != nil { + return nil, phraseErr + } + + crypter, crypterErr := symmetricCrypterFromPhraseAndState(phrase, info.EncryptionSalt) + if crypterErr != nil { + return nil, crypterErr + } + + return crypter, nil + } } // Read a passphrase and confirm it. @@ -78,7 +120,9 @@ func symmetricCrypter() (config.Crypter, error) { contract.AssertNoError(err) // Now store the result on the package and save it. - pkg.EncryptionSalt = fmt.Sprintf("v1:%s:%s", base64.StdEncoding.EncodeToString(salt), msg) + stackInfo := pkg.Stacks[stackName] + stackInfo.EncryptionSalt = fmt.Sprintf("v1:%s:%s", base64.StdEncoding.EncodeToString(salt), msg) + pkg.Stacks[stackName] = stackInfo if err = workspace.SavePackage(pkg); err != nil { return nil, err } diff --git a/pkg/backend/local/state.go b/pkg/backend/local/state.go index e9ae29e19..ae5d5b79e 100644 --- a/pkg/backend/local/state.go +++ b/pkg/backend/local/state.go @@ -91,7 +91,7 @@ func (b *localBackend) getTarget(stackName tokens.QName) (*deploy.Target, error) if err != nil { return nil, err } - decrypter, err := defaultCrypter(cfg) + decrypter, err := defaultCrypter(stackName, cfg) if err != nil { return nil, err } diff --git a/pkg/pack/package.go b/pkg/pack/package.go index fc261c9a9..3dce751cb 100644 --- a/pkg/pack/package.go +++ b/pkg/pack/package.go @@ -48,13 +48,15 @@ type Package struct { } // StackInfo holds stack specific information about a package +// nolint: lll type StackInfo struct { - Config map[tokens.ModuleMember]config.Value `json:"config,omitempty" yaml:"config,omitempty"` // optional config. + EncryptionSalt string `json:"encryptionsalt,omitempty" yaml:"encryptionsalt,omitempty"` // base64 encoded encryption salt. + Config config.Map `json:"config,omitempty" yaml:"config,omitempty"` // optional config. } // IsEmpty returns True if this object contains no information (i.e. all members have their zero values) func (s *StackInfo) IsEmpty() bool { - return len(s.Config) == 0 + return len(s.Config) == 0 && s.EncryptionSalt == "" } var _ diag.Diagable = (*Package)(nil)