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:
parent
22938f07be
commit
cc04cd6581
12
CHANGELOG.md
Normal file
12
CHANGELOG.md
Normal 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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue