pulumi/cmd/util.go
Matt Ellis ade366544e Encrypt secrets in Pulumi.yaml
We now encrypt secrets at rest based on a key derived from a user
suplied passphrase.

The system is designed in a way such that we should be able to have a
different decrypter (either using a local key or some remote service
in the Pulumi.com case in the future).

Care is taken to ensure that we do not leak decrypted secrets into the
"info" section of the checkpoint file (since we currently store the
config there).

In addtion, secrets are "pay for play", a passphrase is only needed
when dealing with a value that's encrypted. If secure config values
are not used, `pulumi` will never prompt you for a
passphrase. Otherwise, we only prompt if we know we are going to need
to decrypt the value. For example, `pulumi config <key>` only prompts
if `<key>` is encrypted and `pulumi deploy` and friends only prompt if
you are targeting a stack that has secure configuration assoicated
with it.

Secure values show up as unecrypted config values inside the language
hosts and providers.
2017-10-24 16:48:12 -07:00

234 lines
5.4 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package cmd
import (
cryptorand "crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/pulumi/pulumi/pkg/pack"
"github.com/pulumi/pulumi/pkg/resource/config"
"golang.org/x/crypto/pbkdf2"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/diag/colors"
"github.com/pulumi/pulumi/pkg/engine"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/pulumi/pulumi/pkg/workspace"
)
// newWorkspace creates a new workspace using the current working directory.
func newWorkspace() (workspace.W, error) {
pwd, err := os.Getwd()
if err != nil {
return nil, err
}
return workspace.New(pwd)
}
// explicitOrCurrent returns an stack name after ensuring the stack exists. When a empty
// stack name is passed, the "current" ambient stack is returned
func explicitOrCurrent(name string) (tokens.QName, error) {
if name == "" {
return getCurrentStack()
}
_, _, _, err := getStack(tokens.QName(name))
return tokens.QName(name), err
}
// getCurrentStack reads the current stack.
func getCurrentStack() (tokens.QName, error) {
w, err := newWorkspace()
if err != nil {
return "", err
}
stack := w.Settings().Stack
if stack == "" {
return "", errors.New("no current stack detected; please use `pulumi stack init` to create one")
}
return stack, nil
}
// setCurrentStack changes the current stack to the given stack name, issuing an error if it doesn't exist.
func setCurrentStack(name tokens.QName, verify bool) error {
if verify {
if _, _, _, err := getStack(name); err != nil {
return err
}
}
// Switch the current workspace to that stack.
w, err := newWorkspace()
if err != nil {
return err
}
w.Settings().Stack = name
return w.Save()
}
// displayEvents reads events from the `events` channel until it is closed, displaying each event as it comes in.
// Once all events have been read from the channel and displayed, it closes the `done` channel so the caller can
// await all the events being written.
func displayEvents(events <-chan engine.Event, done chan bool, debug bool) {
defer func() {
done <- true
}()
Outer:
for event := range events {
switch event.Type {
case engine.CancelEvent:
break Outer
case engine.StdoutColorEvent:
fmt.Print(colors.ColorizeText(event.Payload.(string)))
case engine.DiagEvent:
payload := event.Payload.(engine.DiagEventPayload)
var out io.Writer
out = os.Stdout
if payload.Severity == diag.Error || payload.Severity == diag.Warning {
out = os.Stderr
}
if payload.Severity == diag.Debug && !debug {
out = ioutil.Discard
}
msg := payload.Message
if payload.UseColor {
msg = colors.ColorizeText(msg)
}
fmt.Fprintf(out, msg)
default:
contract.Failf("unknown event type '%s'", event.Type)
}
}
}
func getPackage() (*pack.Package, error) {
pkgPath, err := getPackageFilePath()
if err != nil {
return nil, err
}
pkg, err := pack.Load(pkgPath)
return pkg, err
}
func savePackage(pkg *pack.Package) error {
pkgPath, err := getPackageFilePath()
if err != nil {
return err
}
return pack.Save(pkgPath, pkg)
}
func getPackageFilePath() (string, error) {
dir, err := os.Getwd()
if err != nil {
return "", err
}
pkgPath, err := workspace.DetectPackage(dir)
if err != nil {
return "", err
}
if pkgPath == "" {
return "", errors.Errorf("could not find Pulumi.yaml (searching upwards from %s)", dir)
}
return pkgPath, nil
}
func readConsoleNoEchoWithPrompt(prompt string) (string, error) {
if prompt != "" {
fmt.Printf("%s: ", prompt)
}
return readConsoleNoEcho()
}
func readPassPhrase(prompt string) (string, error) {
if phrase, has := os.LookupEnv("PULUMI_CONFIG_PASSPHRASE"); has {
return phrase, nil
}
return readConsoleNoEchoWithPrompt(prompt)
}
func getSymmetricCrypter() (config.ValueEncrypterDecrypter, error) {
pkg, err := getPackage()
if err != nil {
return nil, err
}
return getSymmetricCrypterForPackage(pkg)
}
func getSymmetricCrypterForPackage(pkg *pack.Package) (config.ValueEncrypterDecrypter, error) {
if pkg.EncryptionSalt != "" {
phrase, err := readPassPhrase("passphrase")
if err != nil {
return nil, err
}
return symmetricCrypterFromPhraseAndState(phrase, pkg.EncryptionSalt)
}
phrase, err := readPassPhrase("passphrase")
if err != nil {
return nil, err
}
confirm, err := readPassPhrase("passphrase (confirm)")
if err != nil {
return nil, err
}
if phrase != confirm {
return nil, errors.New("passphrases do not match")
}
salt := make([]byte, 8)
_, err = cryptorand.Read(salt)
contract.Assertf(err == nil, "could not read from system random")
c := symmetricCrypter{key: keyFromPassPhrase(phrase, salt, aes256GCMKeyBytes)}
// Encrypt a message and store it with the salt so we can test if the password is correct later
msg, err := c.EncryptValue("pulumi")
contract.Assert(err == nil)
pkg.EncryptionSalt = fmt.Sprintf("v1:%s:%s", base64.StdEncoding.EncodeToString(salt), msg)
return c, nil
}
func keyFromPassPhrase(phrase string, salt []byte, keyLength int) []byte {
// 1,000,000 iterations was chosen because it took a little over a second on an i7-7700HQ Quad Core procesor
return pbkdf2.Key([]byte(phrase), salt, 1000000, keyLength, sha256.New)
}
func hasSecureValue(config map[tokens.ModuleMember]config.Value) bool {
for _, v := range config {
if v.Secure() {
return true
}
}
return false
}