This change allows using the passphrase secrets manager when creating a stack managed by the Pulumi service. `pulumi stack init`, `pulumi new` and `pulumi up` all learned a new optional argument `--secrets-provider` which can be set to "passphrase" to force the passphrase based secrets provider to be used. When unset the default secrets provider is used based on the backend (for local stacks this is passphrase, for remote stacks, it is the key managed by the pulumi service). As part of this change, we also initialize the secrets manager when a stack is created, instead of waiting for the first time a secret config value is stored. We do this so that if an update is run using `pulumi.secret` before any secret configuration values are used, we already have the correct encryption method selected for a stack.
118 lines
3.5 KiB
Go
118 lines
3.5 KiB
Go
// Copyright 2016-2019, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
package cmd
|
|
|
|
import (
|
|
cryptorand "crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/pulumi/pulumi/pkg/diag"
|
|
"github.com/pulumi/pulumi/pkg/resource/config"
|
|
"github.com/pulumi/pulumi/pkg/secrets"
|
|
"github.com/pulumi/pulumi/pkg/secrets/passphrase"
|
|
"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"
|
|
)
|
|
|
|
func readPassphrase(prompt string) (string, error) {
|
|
if phrase := os.Getenv("PULUMI_CONFIG_PASSPHRASE"); phrase != "" {
|
|
return phrase, nil
|
|
}
|
|
return cmdutil.ReadConsoleNoEcho(prompt)
|
|
}
|
|
|
|
func newPassphraseSecretsManager(stackName tokens.QName, configFile string) (secrets.Manager, error) {
|
|
contract.Assertf(stackName != "", "stackName %s", "!= \"\"")
|
|
|
|
if configFile == "" {
|
|
f, err := workspace.DetectProjectStackPath(stackName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
configFile = f
|
|
}
|
|
|
|
info, err := workspace.LoadProjectStack(configFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If we have a salt, we can just use it.
|
|
if info.EncryptionSalt != "" {
|
|
for {
|
|
phrase, phraseErr := readPassphrase("Enter your passphrase to unlock config/secrets\n" +
|
|
" (set PULUMI_CONFIG_PASSPHRASE to remember)")
|
|
if phraseErr != nil {
|
|
return nil, phraseErr
|
|
}
|
|
|
|
sm, smerr := passphrase.NewPassphaseSecretsManager(phrase, info.EncryptionSalt)
|
|
switch {
|
|
case smerr == passphrase.ErrIncorrectPassphrase:
|
|
cmdutil.Diag().Errorf(diag.Message("", "incorrect passphrase"))
|
|
continue
|
|
case smerr != nil:
|
|
return nil, smerr
|
|
default:
|
|
return sm, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
var phrase string
|
|
|
|
// Get a the passphrase from the user, ensuring that they match.
|
|
for {
|
|
// Here, the stack does not have an EncryptionSalt, so we will get a passphrase and create one
|
|
first, err := readPassphrase("Enter your passphrase to protect config/secrets")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
second, err := readPassphrase("Re-enter your passphrase to confirm")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if first == second {
|
|
phrase = first
|
|
break
|
|
}
|
|
// If they didn't match, print an error and try again
|
|
cmdutil.Diag().Errorf(diag.Message("", "passphrases do not match"))
|
|
}
|
|
|
|
// Produce a new salt.
|
|
salt := make([]byte, 8)
|
|
_, err = cryptorand.Read(salt)
|
|
contract.Assertf(err == nil, "could not read from system random")
|
|
|
|
// Encrypt a message and store it with the salt so we can test if the password is correct later.
|
|
crypter := config.NewSymmetricCrypterFromPassphrase(phrase, salt)
|
|
msg, err := crypter.EncryptValue("pulumi")
|
|
contract.AssertNoError(err)
|
|
|
|
// Now store the result and save it.
|
|
info.EncryptionSalt = fmt.Sprintf("v1:%s:%s", base64.StdEncoding.EncodeToString(salt), msg)
|
|
if err = info.Save(configFile); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Finally, build the full secrets manager from the state we just saved
|
|
return passphrase.NewPassphaseSecretsManager(phrase, info.EncryptionSalt)
|
|
}
|