mirror of https://github.com/pulumi/pulumi.git
Support secrets for cloud stacks.
Use the new {en,de}crypt endpoints in the Pulumi.com API to secure secret config values. The ciphertext for a secret config value is bound to the stack to which it applies and cannot be shared with other stacks (e.g. by copy/pasting it around in Pulumi.yaml). All secrets will need to be encrypted once per target stack.
This commit is contained in:
parent
af087103b9
commit
e4d9eb6fd3
cmd
pkg
backend
resource/config
|
@ -173,7 +173,7 @@ func newConfigSetCmd(stack *string) *cobra.Command {
|
|||
// Encrypt the config value if needed.
|
||||
var v config.Value
|
||||
if secret {
|
||||
c, cerr := state.SymmetricCrypter()
|
||||
c, cerr := backend.GetStackCrypter(s)
|
||||
if cerr != nil {
|
||||
return cerr
|
||||
}
|
||||
|
@ -277,7 +277,7 @@ func listConfig(stack backend.Stack, showSecrets bool) error {
|
|||
// By default, we will use a blinding decrypter to show '******'. If requested, display secrets in plaintext.
|
||||
var decrypter config.Decrypter
|
||||
if cfg.HasSecureValue() && showSecrets {
|
||||
decrypter, err = state.SymmetricCrypter()
|
||||
decrypter, err = backend.GetStackCrypter(stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -326,7 +326,7 @@ func getConfig(stack backend.Stack, key tokens.ModuleMember) error {
|
|||
var d config.Decrypter
|
||||
if v.Secure() {
|
||||
var err error
|
||||
if d, err = state.DefaultCrypter(cfg); err != nil {
|
||||
if d, err = backend.GetStackCrypter(stack); err != nil {
|
||||
return errors.Wrap(err, "could not create a decrypter")
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -6,6 +6,7 @@ package backend
|
|||
import (
|
||||
"github.com/pulumi/pulumi/pkg/engine"
|
||||
"github.com/pulumi/pulumi/pkg/operations"
|
||||
"github.com/pulumi/pulumi/pkg/resource/config"
|
||||
"github.com/pulumi/pulumi/pkg/tokens"
|
||||
)
|
||||
|
||||
|
@ -26,6 +27,9 @@ type Backend interface {
|
|||
// ListStacks returns a list of stack summaries for all known stacks in the target backend.
|
||||
ListStacks() ([]Stack, error)
|
||||
|
||||
// GetStackCrypter returns an encrypter/decrypter for the given stack's secret config values.
|
||||
GetStackCrypter(stack tokens.QName) (config.Crypter, error)
|
||||
|
||||
// Preview initiates a preview of the current workspace's contents.
|
||||
Preview(stackName tokens.QName, debug bool, opts engine.PreviewOptions) error
|
||||
// Update updates the target stack with the current workspace's contents (config and code).
|
||||
|
|
|
@ -43,3 +43,27 @@ type CreateStackResponse struct {
|
|||
// The name of the cloud used if the default was sent.
|
||||
CloudName string `json:"cloudName"`
|
||||
}
|
||||
|
||||
// EncryptValueRequest defines the request body for encrypting a value.
|
||||
type EncryptValueRequest struct {
|
||||
// The value to encrypt.
|
||||
Plaintext []byte `json:"plaintext"`
|
||||
}
|
||||
|
||||
// EncryptValueResponse defines the response body for an encrypted value.
|
||||
type EncryptValueResponse struct {
|
||||
// The encrypted value.
|
||||
Ciphertext []byte `json:"ciphertext"`
|
||||
}
|
||||
|
||||
// DecryptValueRequest defines the request body for decrypting a value.
|
||||
type DecryptValueRequest struct {
|
||||
// The value to decrypt.
|
||||
Ciphertext []byte `json:"ciphertext"`
|
||||
}
|
||||
|
||||
// DecryptValueResponse defines the response body for a decrypted value.
|
||||
type DecryptValueResponse struct {
|
||||
// The decrypted value.
|
||||
Plaintext []byte `json:"plaintext"`
|
||||
}
|
||||
|
|
|
@ -2,6 +2,14 @@ package apitype
|
|||
|
||||
import "github.com/pulumi/pulumi/pkg/tokens"
|
||||
|
||||
// ConfigValue describes a single (possibly secret) configuration value.
|
||||
type ConfigValue struct {
|
||||
// String is either the plaintext value (for non-secrets) or the base64-encoded ciphertext (for secrets).
|
||||
String string `json:"string"`
|
||||
// Secret is true if this value is a secret and false otherwise.
|
||||
Secret bool `json:"secret"`
|
||||
}
|
||||
|
||||
// UpdateProgramRequest is the request type for updating (aka deploying) a Pulumi program.
|
||||
type UpdateProgramRequest struct {
|
||||
// Properties from the Project file. Subset of pack.Package.
|
||||
|
@ -11,7 +19,7 @@ type UpdateProgramRequest struct {
|
|||
Description string `json:"description"`
|
||||
|
||||
// Configuration values.
|
||||
Config map[tokens.ModuleMember]string `json:"config"`
|
||||
Config map[tokens.ModuleMember]ConfigValue `json:"config"`
|
||||
}
|
||||
|
||||
// UpdateProgramResponse is the result of an update program request.
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
package cloud
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -25,6 +26,7 @@ import (
|
|||
"github.com/pulumi/pulumi/pkg/engine"
|
||||
"github.com/pulumi/pulumi/pkg/operations"
|
||||
"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/archive"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
|
@ -135,6 +137,55 @@ func (b *cloudBackend) RemoveStack(stackName tokens.QName, force bool) (bool, er
|
|||
return false, pulumiRESTCall(b.cloudURL, "DELETE", path, nil, nil, nil)
|
||||
}
|
||||
|
||||
// cloudCrypter is an encrypter/decrypter that uses the Pulumi cloud to encrypt/decrypt a stack's secrets.
|
||||
type cloudCrypter struct {
|
||||
backend *cloudBackend
|
||||
stackName string
|
||||
}
|
||||
|
||||
func (c *cloudCrypter) EncryptValue(plaintext string) (string, error) {
|
||||
projID, err := getCloudProjectIdentifier()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/orgs/%s/programs/%s/%s/stacks/%s/encrypt",
|
||||
projID.Owner, projID.Repository, projID.Project, c.stackName)
|
||||
|
||||
var resp apitype.EncryptValueResponse
|
||||
req := apitype.EncryptValueRequest{Plaintext: []byte(plaintext)}
|
||||
if err := pulumiRESTCall(c.backend.cloudURL, "POST", path, &req, &resp, nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(resp.Ciphertext), nil
|
||||
}
|
||||
|
||||
func (c *cloudCrypter) DecryptValue(cipherstring string) (string, error) {
|
||||
projID, err := getCloudProjectIdentifier()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(cipherstring)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/orgs/%s/programs/%s/%s/stacks/%s/decrypt",
|
||||
projID.Owner, projID.Repository, projID.Project, c.stackName)
|
||||
|
||||
var resp apitype.DecryptValueResponse
|
||||
req := apitype.DecryptValueRequest{Ciphertext: ciphertext}
|
||||
if err := pulumiRESTCall(c.backend.cloudURL, "POST", path, &req, &resp, nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(resp.Plaintext), nil
|
||||
}
|
||||
|
||||
func (b *cloudBackend) GetStackCrypter(stackName tokens.QName) (config.Crypter, error) {
|
||||
return &cloudCrypter{backend: b, stackName: string(stackName)}, nil
|
||||
}
|
||||
|
||||
// updateKind is an enum for describing the kinds of updates we support.
|
||||
type updateKind string
|
||||
|
||||
|
@ -319,29 +370,6 @@ func (b *cloudBackend) listCloudStacks() ([]apitype.Stack, error) {
|
|||
return stacks, nil
|
||||
}
|
||||
|
||||
// getDecryptedConfig returns the stack's configuration with any secrets in plain-text.
|
||||
func (b *cloudBackend) getDecryptedConfig(stackName tokens.QName) (map[tokens.ModuleMember]string, error) {
|
||||
cfg, err := state.Configuration(b.d, stackName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting configuration")
|
||||
}
|
||||
|
||||
decrypter, err := state.DefaultCrypter(cfg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting a default encrypter")
|
||||
}
|
||||
|
||||
textConfig := make(map[tokens.ModuleMember]string)
|
||||
for key := range cfg {
|
||||
decrypted, err := cfg[key].Value(decrypter)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not decrypt configuration value")
|
||||
}
|
||||
textConfig[key] = decrypted
|
||||
}
|
||||
return textConfig, nil
|
||||
}
|
||||
|
||||
// getCloudProjectIdentifier returns information about the current repository and project, based on the current
|
||||
// working directory.
|
||||
func getCloudProjectIdentifier() (*cloudProjectIdentifier, error) {
|
||||
|
@ -391,11 +419,20 @@ func (b *cloudBackend) makeProgramUpdateRequest(stackName tokens.QName) (apitype
|
|||
return ""
|
||||
}
|
||||
|
||||
// Gather up configuration.
|
||||
// TODO(pulumi-service/issues/221): Have pulumi.com handle the encryption/decryption.
|
||||
textConfig, err := b.getDecryptedConfig(stackName)
|
||||
// Convert the configuration into its wire form.
|
||||
cfg, err := state.Configuration(b.d, stackName)
|
||||
if err != nil {
|
||||
return apitype.UpdateProgramRequest{}, errors.Wrap(err, "getting decrypted configuration")
|
||||
return apitype.UpdateProgramRequest{}, errors.Wrap(err, "getting configuration")
|
||||
}
|
||||
wireConfig := make(map[tokens.ModuleMember]apitype.ConfigValue)
|
||||
for k, cv := range cfg {
|
||||
v, err := cv.Value(config.NopDecrypter)
|
||||
contract.Assert(err == nil)
|
||||
|
||||
wireConfig[k] = apitype.ConfigValue{
|
||||
String: v,
|
||||
Secret: cv.Secure(),
|
||||
}
|
||||
}
|
||||
|
||||
return apitype.UpdateProgramRequest{
|
||||
|
@ -403,7 +440,7 @@ func (b *cloudBackend) makeProgramUpdateRequest(stackName tokens.QName) (apitype
|
|||
Runtime: pkg.Runtime,
|
||||
Main: pkg.Main,
|
||||
Description: valueOrEmpty(pkg.Description),
|
||||
Config: textConfig,
|
||||
Config: wireConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/pulumi/pulumi/pkg/encoding"
|
||||
"github.com/pulumi/pulumi/pkg/engine"
|
||||
"github.com/pulumi/pulumi/pkg/operations"
|
||||
"github.com/pulumi/pulumi/pkg/resource/config"
|
||||
"github.com/pulumi/pulumi/pkg/tokens"
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
"github.com/pulumi/pulumi/pkg/workspace"
|
||||
|
@ -97,6 +98,10 @@ func (b *localBackend) RemoveStack(stackName tokens.QName, force bool) (bool, er
|
|||
return false, removeStack(stackName)
|
||||
}
|
||||
|
||||
func (b *localBackend) GetStackCrypter(stackName tokens.QName) (config.Crypter, error) {
|
||||
return symmetricCrypter()
|
||||
}
|
||||
|
||||
func (b *localBackend) Preview(stackName tokens.QName, debug bool, opts engine.PreviewOptions) error {
|
||||
pulumiEngine, err := b.getEngine(stackName)
|
||||
if err != nil {
|
||||
|
@ -208,7 +213,7 @@ func (b *localBackend) getEngine(stackName tokens.QName) (engine.Engine, error)
|
|||
return engine.Engine{}, err
|
||||
}
|
||||
|
||||
decrypter, err := state.DefaultCrypter(cfg)
|
||||
decrypter, err := defaultCrypter(cfg)
|
||||
if err != nil {
|
||||
return engine.Engine{}, err
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
||||
|
||||
package state
|
||||
package local
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
|
@ -24,19 +24,19 @@ func readPassphrase(prompt string) (string, error) {
|
|||
return cmdutil.ReadConsoleNoEcho(prompt)
|
||||
}
|
||||
|
||||
// DefaultCrypter gets the right value encrypter/decrypter given the project configuration.
|
||||
func DefaultCrypter(cfg config.Map) (config.Crypter, error) {
|
||||
// defaultCrypter gets the right value encrypter/decrypter given the project configuration.
|
||||
func defaultCrypter(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()
|
||||
}
|
||||
|
||||
// SymmetricCrypter gets the right value encrypter/decrypter for this project.
|
||||
func SymmetricCrypter() (config.Crypter, error) {
|
||||
func symmetricCrypter() (config.Crypter, error) {
|
||||
// First, read the package to see if we've got a key.
|
||||
pkg, err := workspace.GetPackage()
|
||||
if err != nil {
|
|
@ -44,6 +44,11 @@ func DestroyStack(s Stack, debug bool, opts engine.DestroyOptions) error {
|
|||
return s.Backend().Destroy(s.Name(), debug, opts)
|
||||
}
|
||||
|
||||
// GetStackCrypter fetches the encrypter/decrypter for a stack.
|
||||
func GetStackCrypter(s Stack) (config.Crypter, error) {
|
||||
return s.Backend().GetStackCrypter(s.Name())
|
||||
}
|
||||
|
||||
// GetStackLogs fetches a list of log entries for the current stack in the current backend.
|
||||
func GetStackLogs(s Stack, query operations.LogQuery) ([]operations.LogEntry, error) {
|
||||
return s.Backend().GetLogs(s.Name(), query)
|
||||
|
|
|
@ -21,9 +21,9 @@ type Encrypter interface {
|
|||
EncryptValue(plaintext string) (string, error)
|
||||
}
|
||||
|
||||
// Decrypter decrypts encrypted cyphertext to its plaintext representation.
|
||||
// Decrypter decrypts encrypted ciphertext to its plaintext representation.
|
||||
type Decrypter interface {
|
||||
DecryptValue(cypertext string) (string, error)
|
||||
DecryptValue(ciphertext string) (string, error)
|
||||
}
|
||||
|
||||
// Crypter can both encrypt and decrypt values.
|
||||
|
@ -32,6 +32,15 @@ type Crypter interface {
|
|||
Decrypter
|
||||
}
|
||||
|
||||
// A nopDecrypter simply returns the ciphertext as-is.
|
||||
type nopDecrypter int
|
||||
|
||||
var NopDecrypter Decrypter = nopDecrypter(0)
|
||||
|
||||
func (nopDecrypter) DecryptValue(ciphertext string) (string, error) {
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// NewBlindingDecrypter returns a Decrypter that instead of decrypting data, just returns "********", it can
|
||||
// be used when you want to display configuration information to a user but don't want to prompt for a password
|
||||
// so secrets will not be decrypted.
|
||||
|
|
Loading…
Reference in New Issue