mirror of https://github.com/pulumi/pulumi.git
239 lines
8.2 KiB
Go
239 lines
8.2 KiB
Go
// Copyright 2016-2018, 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 cloud implements support for a generic cloud secret manager.
|
|
package cloud
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
netUrl "net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
gosecrets "gocloud.dev/secrets"
|
|
_ "gocloud.dev/secrets/awskms" // support for awskms://
|
|
_ "gocloud.dev/secrets/azurekeyvault" // support for azurekeyvault://
|
|
"gocloud.dev/secrets/gcpkms" // support for gcpkms://
|
|
_ "gocloud.dev/secrets/hashivault" // support for hashivault://
|
|
"google.golang.org/api/cloudkms/v1"
|
|
|
|
"github.com/pulumi/pulumi/pkg/v3/authhelpers"
|
|
"github.com/pulumi/pulumi/pkg/v3/secrets"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
|
)
|
|
|
|
// Type is the type of secrets managed by this secrets provider
|
|
const Type = "cloud"
|
|
|
|
type cloudSecretsManagerState struct {
|
|
URL string `json:"url"`
|
|
EncryptedKey []byte `json:"encryptedkey"`
|
|
}
|
|
|
|
// openKeeper opens the keeper, handling pulumi-specifc cases in the URL.
|
|
func openKeeper(ctx context.Context, url string) (*gosecrets.Keeper, error) {
|
|
u, err := netUrl.Parse(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse the secrets provider URL: %w", err)
|
|
}
|
|
|
|
switch u.Scheme {
|
|
case gcpkms.Scheme:
|
|
credentials, err := authhelpers.ResolveGoogleCredentials(ctx, cloudkms.CloudkmsScope)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("missing google credentials: %w", err)
|
|
}
|
|
|
|
kmsClient, _, err := gcpkms.Dial(ctx, credentials.TokenSource)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to gcpkms: %w", err)
|
|
}
|
|
opener := gcpkms.URLOpener{
|
|
Client: kmsClient,
|
|
}
|
|
|
|
return opener.OpenKeeperURL(ctx, u)
|
|
default:
|
|
return gosecrets.OpenKeeper(ctx, url)
|
|
}
|
|
}
|
|
|
|
// generateNewDataKey generates a new DataKey seeded by a fresh random 32-byte key and encrypted
|
|
// using the target cloud key management service.
|
|
func generateNewDataKey(url string) ([]byte, error) {
|
|
plaintextDataKey := make([]byte, 32)
|
|
_, err := rand.Read(plaintextDataKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keeper, err := openKeeper(context.Background(), url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return keeper.Encrypt(context.Background(), plaintextDataKey)
|
|
}
|
|
|
|
// newCloudSecretsManager returns a secrets manager that uses the target cloud key management
|
|
// service to encrypt/decrypt a data key used for envelope encryption of secrets values.
|
|
func newCloudSecretsManager(url string, encryptedDataKey []byte) (*Manager, error) {
|
|
keeper, err := openKeeper(context.Background(), url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// We're emulating gocloud.dev's old behaviour here. Pre v0.28.0 it used to have an inner wrapping, which
|
|
// we keep for compatibility (see above). However the newer version expects this to be unwrapped, before
|
|
// it's used, so let's do that here.
|
|
ciphertext := encryptedDataKey
|
|
if strings.HasPrefix(url, "azurekeyvault://") {
|
|
ciphertext, err = base64.RawURLEncoding.DecodeString(string(encryptedDataKey))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
plaintextDataKey, err := keeper.Decrypt(context.Background(), ciphertext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state, err := json.Marshal(cloudSecretsManagerState{
|
|
URL: url,
|
|
EncryptedKey: encryptedDataKey,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshalling state: %w", err)
|
|
}
|
|
crypter := config.NewSymmetricCrypter(plaintextDataKey)
|
|
return &Manager{
|
|
crypter: crypter,
|
|
state: state,
|
|
}, nil
|
|
}
|
|
|
|
// Manager is the secrets.Manager implementation for cloud key management services
|
|
type Manager struct {
|
|
state json.RawMessage
|
|
crypter config.Crypter
|
|
}
|
|
|
|
func (m *Manager) Type() string { return Type }
|
|
func (m *Manager) State() json.RawMessage { return m.state }
|
|
func (m *Manager) Encrypter() (config.Encrypter, error) { return m.crypter, nil }
|
|
func (m *Manager) Decrypter() (config.Decrypter, error) { return m.crypter, nil }
|
|
|
|
func EditProjectStack(info *workspace.ProjectStack, state json.RawMessage) error {
|
|
info.EncryptionSalt = ""
|
|
|
|
var s cloudSecretsManagerState
|
|
err := json.Unmarshal(state, &s)
|
|
if err != nil {
|
|
return fmt.Errorf("unmarshalling cloud state: %w", err)
|
|
}
|
|
|
|
info.SecretsProvider = s.URL
|
|
info.EncryptedKey = base64.StdEncoding.EncodeToString(s.EncryptedKey)
|
|
return nil
|
|
}
|
|
|
|
// NewCloudSecretsManagerFromState deserialize configuration from state and returns a secrets
|
|
// manager that uses the target cloud key management service to encrypt/decrypt a data key used for
|
|
// envelope encryption of secrets values.
|
|
func NewCloudSecretsManagerFromState(state json.RawMessage) (secrets.Manager, error) {
|
|
var s cloudSecretsManagerState
|
|
err := json.Unmarshal(state, &s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unmarshalling state: %w", err)
|
|
}
|
|
|
|
// We're emulating gocloud.dev's old behaviour here. Pre v0.28.0 it used to have an inner wrapping, which
|
|
// we keep for compatibility (see above). However the newer version expects this to be unwrapped, before
|
|
// it's used. newCloudSecretsManager will manage that but we need to check here as well to handle the
|
|
// #15329 regression.
|
|
dataKey := s.EncryptedKey
|
|
if strings.HasPrefix(s.URL, "azurekeyvault://") {
|
|
wrappedKey, err := base64.RawURLEncoding.DecodeString(string(dataKey))
|
|
if err != nil {
|
|
// https://github.com/pulumi/pulumi/issues/15329 resulted in some non-encoded keys being written
|
|
// to state. This checks that case to see if there valid base64 data.
|
|
firstErr := err
|
|
_, err := base64.StdEncoding.DecodeString(string(wrappedKey))
|
|
if err != nil {
|
|
// Wasn't valid base64 so probably just gibberish, return the first error we saw.
|
|
return nil, firstErr
|
|
}
|
|
// This is a valid base64 string so it's probably just a bad encoding, wrap it as expected and
|
|
// pass it on to newCloudSecretsManager
|
|
dataKey = []byte(base64.RawURLEncoding.EncodeToString(dataKey))
|
|
}
|
|
}
|
|
|
|
return newCloudSecretsManager(s.URL, dataKey)
|
|
}
|
|
|
|
func NewCloudSecretsManager(info *workspace.ProjectStack,
|
|
secretsProvider string, rotateSecretsProvider bool,
|
|
) (secrets.Manager, error) {
|
|
// Only a passphrase provider has an encryption salt. So changing a secrets provider
|
|
// from passphrase to a cloud secrets provider should ensure that we remove the enryptionsalt
|
|
// as it's a legacy artifact and needs to be removed
|
|
info.EncryptionSalt = ""
|
|
|
|
var secretsManager *Manager
|
|
|
|
// Allow per-execution override of the secrets provider via an environment
|
|
// variable. This allows a temporary replacement without updating the stack
|
|
// config, such a during CI.
|
|
if override := os.Getenv("PULUMI_CLOUD_SECRET_OVERRIDE"); override != "" {
|
|
secretsProvider = override
|
|
}
|
|
|
|
// If we're rotating then just clear the key so we create a fresh one below
|
|
if rotateSecretsProvider {
|
|
info.EncryptedKey = ""
|
|
}
|
|
|
|
// if there is no key OR the secrets provider is changing
|
|
// then we need to generate the new key based on the new secrets provider
|
|
if info.EncryptedKey == "" || info.SecretsProvider != secretsProvider {
|
|
dataKey, err := generateNewDataKey(secretsProvider)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// gocloud.dev versions before v0.28.0 wrapped the
|
|
// data key in a base64.RawURLEncoding before wrapping
|
|
// it again in base64.StdEncoding. We keep emulating
|
|
// this here for compatibility.
|
|
if strings.HasPrefix(secretsProvider, "azurekeyvault://") {
|
|
dataKey = []byte(base64.RawURLEncoding.EncodeToString(dataKey))
|
|
}
|
|
info.EncryptedKey = base64.StdEncoding.EncodeToString(dataKey)
|
|
}
|
|
info.SecretsProvider = secretsProvider
|
|
|
|
dataKey, err := base64.StdEncoding.DecodeString(info.EncryptedKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
secretsManager, err = newCloudSecretsManager(secretsProvider, dataKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return secretsManager, nil
|
|
}
|