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
|
@ -173,7 +173,7 @@ func newConfigSetCmd(stack *string) *cobra.Command {
|
||||||
// Encrypt the config value if needed.
|
// Encrypt the config value if needed.
|
||||||
var v config.Value
|
var v config.Value
|
||||||
if secret {
|
if secret {
|
||||||
c, cerr := state.SymmetricCrypter()
|
c, cerr := backend.GetStackCrypter(s)
|
||||||
if cerr != nil {
|
if cerr != nil {
|
||||||
return cerr
|
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.
|
// By default, we will use a blinding decrypter to show '******'. If requested, display secrets in plaintext.
|
||||||
var decrypter config.Decrypter
|
var decrypter config.Decrypter
|
||||||
if cfg.HasSecureValue() && showSecrets {
|
if cfg.HasSecureValue() && showSecrets {
|
||||||
decrypter, err = state.SymmetricCrypter()
|
decrypter, err = backend.GetStackCrypter(stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -326,7 +326,7 @@ func getConfig(stack backend.Stack, key tokens.ModuleMember) error {
|
||||||
var d config.Decrypter
|
var d config.Decrypter
|
||||||
if v.Secure() {
|
if v.Secure() {
|
||||||
var err error
|
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")
|
return errors.Wrap(err, "could not create a decrypter")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -6,6 +6,7 @@ package backend
|
||||||
import (
|
import (
|
||||||
"github.com/pulumi/pulumi/pkg/engine"
|
"github.com/pulumi/pulumi/pkg/engine"
|
||||||
"github.com/pulumi/pulumi/pkg/operations"
|
"github.com/pulumi/pulumi/pkg/operations"
|
||||||
|
"github.com/pulumi/pulumi/pkg/resource/config"
|
||||||
"github.com/pulumi/pulumi/pkg/tokens"
|
"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 returns a list of stack summaries for all known stacks in the target backend.
|
||||||
ListStacks() ([]Stack, error)
|
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 initiates a preview of the current workspace's contents.
|
||||||
Preview(stackName tokens.QName, debug bool, opts engine.PreviewOptions) error
|
Preview(stackName tokens.QName, debug bool, opts engine.PreviewOptions) error
|
||||||
// Update updates the target stack with the current workspace's contents (config and code).
|
// 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.
|
// The name of the cloud used if the default was sent.
|
||||||
CloudName string `json:"cloudName"`
|
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"
|
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.
|
// UpdateProgramRequest is the request type for updating (aka deploying) a Pulumi program.
|
||||||
type UpdateProgramRequest struct {
|
type UpdateProgramRequest struct {
|
||||||
// Properties from the Project file. Subset of pack.Package.
|
// Properties from the Project file. Subset of pack.Package.
|
||||||
|
@ -11,7 +19,7 @@ type UpdateProgramRequest struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
|
||||||
// Configuration values.
|
// 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.
|
// UpdateProgramResponse is the result of an update program request.
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
package cloud
|
package cloud
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -25,6 +26,7 @@ import (
|
||||||
"github.com/pulumi/pulumi/pkg/engine"
|
"github.com/pulumi/pulumi/pkg/engine"
|
||||||
"github.com/pulumi/pulumi/pkg/operations"
|
"github.com/pulumi/pulumi/pkg/operations"
|
||||||
"github.com/pulumi/pulumi/pkg/pack"
|
"github.com/pulumi/pulumi/pkg/pack"
|
||||||
|
"github.com/pulumi/pulumi/pkg/resource/config"
|
||||||
"github.com/pulumi/pulumi/pkg/tokens"
|
"github.com/pulumi/pulumi/pkg/tokens"
|
||||||
"github.com/pulumi/pulumi/pkg/util/archive"
|
"github.com/pulumi/pulumi/pkg/util/archive"
|
||||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
"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)
|
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.
|
// updateKind is an enum for describing the kinds of updates we support.
|
||||||
type updateKind string
|
type updateKind string
|
||||||
|
|
||||||
|
@ -319,29 +370,6 @@ func (b *cloudBackend) listCloudStacks() ([]apitype.Stack, error) {
|
||||||
return stacks, nil
|
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
|
// getCloudProjectIdentifier returns information about the current repository and project, based on the current
|
||||||
// working directory.
|
// working directory.
|
||||||
func getCloudProjectIdentifier() (*cloudProjectIdentifier, error) {
|
func getCloudProjectIdentifier() (*cloudProjectIdentifier, error) {
|
||||||
|
@ -391,11 +419,20 @@ func (b *cloudBackend) makeProgramUpdateRequest(stackName tokens.QName) (apitype
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather up configuration.
|
// Convert the configuration into its wire form.
|
||||||
// TODO(pulumi-service/issues/221): Have pulumi.com handle the encryption/decryption.
|
cfg, err := state.Configuration(b.d, stackName)
|
||||||
textConfig, err := b.getDecryptedConfig(stackName)
|
|
||||||
if err != nil {
|
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{
|
return apitype.UpdateProgramRequest{
|
||||||
|
@ -403,7 +440,7 @@ func (b *cloudBackend) makeProgramUpdateRequest(stackName tokens.QName) (apitype
|
||||||
Runtime: pkg.Runtime,
|
Runtime: pkg.Runtime,
|
||||||
Main: pkg.Main,
|
Main: pkg.Main,
|
||||||
Description: valueOrEmpty(pkg.Description),
|
Description: valueOrEmpty(pkg.Description),
|
||||||
Config: textConfig,
|
Config: wireConfig,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/pulumi/pulumi/pkg/encoding"
|
"github.com/pulumi/pulumi/pkg/encoding"
|
||||||
"github.com/pulumi/pulumi/pkg/engine"
|
"github.com/pulumi/pulumi/pkg/engine"
|
||||||
"github.com/pulumi/pulumi/pkg/operations"
|
"github.com/pulumi/pulumi/pkg/operations"
|
||||||
|
"github.com/pulumi/pulumi/pkg/resource/config"
|
||||||
"github.com/pulumi/pulumi/pkg/tokens"
|
"github.com/pulumi/pulumi/pkg/tokens"
|
||||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||||
"github.com/pulumi/pulumi/pkg/workspace"
|
"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)
|
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 {
|
func (b *localBackend) Preview(stackName tokens.QName, debug bool, opts engine.PreviewOptions) error {
|
||||||
pulumiEngine, err := b.getEngine(stackName)
|
pulumiEngine, err := b.getEngine(stackName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -208,7 +213,7 @@ func (b *localBackend) getEngine(stackName tokens.QName) (engine.Engine, error)
|
||||||
return engine.Engine{}, err
|
return engine.Engine{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
decrypter, err := state.DefaultCrypter(cfg)
|
decrypter, err := defaultCrypter(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return engine.Engine{}, err
|
return engine.Engine{}, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
||||||
|
|
||||||
package state
|
package local
|
||||||
|
|
||||||
import (
|
import (
|
||||||
cryptorand "crypto/rand"
|
cryptorand "crypto/rand"
|
||||||
|
@ -24,19 +24,19 @@ func readPassphrase(prompt string) (string, error) {
|
||||||
return cmdutil.ReadConsoleNoEcho(prompt)
|
return cmdutil.ReadConsoleNoEcho(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultCrypter gets the right value encrypter/decrypter given the project configuration.
|
// defaultCrypter gets the right value encrypter/decrypter given the project configuration.
|
||||||
func DefaultCrypter(cfg config.Map) (config.Crypter, error) {
|
func defaultCrypter(cfg config.Map) (config.Crypter, error) {
|
||||||
// If there is no config, we can use a standard panic crypter.
|
// If there is no config, we can use a standard panic crypter.
|
||||||
if !cfg.HasSecureValue() {
|
if !cfg.HasSecureValue() {
|
||||||
return config.NewPanicCrypter(), nil
|
return config.NewPanicCrypter(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, we will use an encrypted one.
|
// Otherwise, we will use an encrypted one.
|
||||||
return SymmetricCrypter()
|
return symmetricCrypter()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SymmetricCrypter gets the right value encrypter/decrypter for this project.
|
// 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.
|
// First, read the package to see if we've got a key.
|
||||||
pkg, err := workspace.GetPackage()
|
pkg, err := workspace.GetPackage()
|
||||||
if err != nil {
|
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)
|
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.
|
// 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) {
|
func GetStackLogs(s Stack, query operations.LogQuery) ([]operations.LogEntry, error) {
|
||||||
return s.Backend().GetLogs(s.Name(), query)
|
return s.Backend().GetLogs(s.Name(), query)
|
||||||
|
|
|
@ -21,9 +21,9 @@ type Encrypter interface {
|
||||||
EncryptValue(plaintext string) (string, error)
|
EncryptValue(plaintext string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypter decrypts encrypted cyphertext to its plaintext representation.
|
// Decrypter decrypts encrypted ciphertext to its plaintext representation.
|
||||||
type Decrypter interface {
|
type Decrypter interface {
|
||||||
DecryptValue(cypertext string) (string, error)
|
DecryptValue(ciphertext string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crypter can both encrypt and decrypt values.
|
// Crypter can both encrypt and decrypt values.
|
||||||
|
@ -32,6 +32,15 @@ type Crypter interface {
|
||||||
Decrypter
|
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
|
// 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
|
// 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.
|
// so secrets will not be decrypted.
|
||||||
|
|
Loading…
Reference in a new issue