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
This commit is contained in:
Matt Ellis 2018-01-16 11:41:05 -08:00
parent 22938f07be
commit cc04cd6581
6 changed files with 74 additions and 15 deletions

12
CHANGELOG.md Normal file
View file

@ -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.

View file

@ -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
}

View file

@ -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,

View file

@ -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
}

View file

@ -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
}

View file

@ -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)