diff --git a/go.mod b/go.mod index 236008f17d..0a81e67b01 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.16 require ( github.com/AlecAivazis/survey/v2 v2.3.2 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 - github.com/aws/aws-sdk-go v1.41.8 + github.com/aws/aws-sdk-go v1.41.9 github.com/aws/aws-sdk-go-v2/credentials v1.4.3 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/cli/browser v1.1.0 diff --git a/go.sum b/go.sum index 3aab8d1f52..181942954f 100644 --- a/go.sum +++ b/go.sum @@ -205,6 +205,8 @@ github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zK github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.41.8 h1:j6imzwVyWQYuQxbkPmg2MdMmLB+Zw+U3Ewi59YF8Rwk= github.com/aws/aws-sdk-go v1.41.8/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/aws/aws-sdk-go v1.41.9 h1:Xb4gWjA90ju0u6Fr2lMAsMOGuhw1g4sTFOqh9SUHgN0= +github.com/aws/aws-sdk-go v1.41.9/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= github.com/aws/aws-sdk-go-v2 v1.9.2 h1:dUFQcMNZMLON4BOe273pl0filK9RqyQMhCK/6xssL6s= github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= diff --git a/secrets/kms.go b/secrets/awskms.go similarity index 84% rename from secrets/kms.go rename to secrets/awskms.go index 9735512461..78c21bf569 100644 --- a/secrets/kms.go +++ b/secrets/awskms.go @@ -1,11 +1,16 @@ package secrets import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/kms" "github.com/aws/aws-sdk-go/service/kms/kmsiface" ) +// ensure this interface is implemented properly +var _ SecretSymmetricKeyProvider = &AWSKMSSecretProvider{} + type AWSKMSSecretProvider struct { kms kmsiface.KMSAPI } @@ -23,7 +28,7 @@ func (k *AWSKMSSecretProvider) DecryptDataKey(rootKeyID string, keyData []byte) CiphertextBlob: keyData, }) if err := req.Send(); err != nil { - return nil, err + return nil, fmt.Errorf("kms: decrypt data key: %w", err) } return &SymmetricKey{ @@ -48,7 +53,7 @@ func (k *AWSKMSSecretProvider) GenerateDataKey(name, rootKeyID string) (*Symmetr if rootKeyID == "" { ko, err := k.generateRootKey(name + ":root") if err != nil { - return nil, err + return nil, fmt.Errorf("kms: generate root key: %w", err) } rootKeyID = *ko.KeyMetadata.KeyId @@ -59,7 +64,7 @@ func (k *AWSKMSSecretProvider) GenerateDataKey(name, rootKeyID string) (*Symmetr KeyId: aws.String(rootKeyID), }) if err != nil { - return nil, err + return nil, fmt.Errorf("kms: generate data key: %w", err) } return &SymmetricKey{ diff --git a/secrets/kms_test.go b/secrets/awskms_test.go similarity index 66% rename from secrets/kms_test.go rename to secrets/awskms_test.go index 83144d1fb7..d1dc1691d8 100644 --- a/secrets/kms_test.go +++ b/secrets/awskms_test.go @@ -4,7 +4,4 @@ package secrets // though not required, you can run a local kms with: // docker run -p 8380:8080 nsmithuk/local-kms -// ensure this interface is implemented properly -var _ SecretSymmetricKeyProvider = &AWSKMSSecretProvider{} - // see secrets_test.go for all shared tests diff --git a/secrets/awssecretsmanager.go b/secrets/awssecretsmanager.go index 68d9c6c940..1745e88a49 100644 --- a/secrets/awssecretsmanager.go +++ b/secrets/awssecretsmanager.go @@ -47,14 +47,14 @@ func (s *AWSSecretsManager) SetSecret(name string, secret []byte) error { SecretId: &name, }) if err != nil { - return fmt.Errorf("update secret: %w", err) + return fmt.Errorf("aws sm: update secret: %w", err) } return nil } } - return fmt.Errorf("creating secret: %w", err) + return fmt.Errorf("aws sm: creating secret: %w", err) } return nil @@ -77,7 +77,7 @@ func (s *AWSSecretsManager) GetSecret(name string) (secret []byte, err error) { } } - return nil, fmt.Errorf("get secret: %w", err) + return nil, fmt.Errorf("aws sm: get secret: %w", err) } return sec.SecretBinary, nil diff --git a/secrets/awssecretsmanager_test.go b/secrets/awssecretsmanager_test.go index c1231e6e66..bd8abc6a9c 100644 --- a/secrets/awssecretsmanager_test.go +++ b/secrets/awssecretsmanager_test.go @@ -11,7 +11,7 @@ import ( "github.com/aws/aws-sdk-go/service/secretsmanager" ) -func waitForSecretsManagerReady(t *testing.T, ssm *secretsmanager.SecretsManager) { +func waitForLocalstackReady(t *testing.T, ssm *secretsmanager.SecretsManager) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() diff --git a/secrets/awssystemmanagerparameterstore.go b/secrets/awssystemmanagerparameterstore.go new file mode 100644 index 0000000000..94ca03abb4 --- /dev/null +++ b/secrets/awssystemmanagerparameterstore.go @@ -0,0 +1,80 @@ +package secrets + +import ( + "context" + "errors" + "fmt" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ssm" +) + +var _ SecretStorage = &AWSSystemManagerParameterStore{} + +type AWSSystemManagerParameterStore struct { + KeyID string // KMS key to use for decryption + client *ssm.SSM +} + +func NewAWSSystemManagerParameterStore(client *ssm.SSM) *AWSSystemManagerParameterStore { + return &AWSSystemManagerParameterStore{ + client: client, + } +} + +var invalidSecretNameChars = regexp.MustCompile(`[^a-zA-Z0-9_.-/]`) + +// SetSecret +// must have the secretsmanager:CreateSecret permission +// if using tags, must have secretsmanager:TagResource +// if using kms customer-managed keys, also need: +// - kms:GenerateDataKey +// - kms:Decrypt +func (s *AWSSystemManagerParameterStore) SetSecret(name string, secret []byte) error { + name = invalidSecretNameChars.ReplaceAllString(name, "_") + secretStr := string(secret) + + var keyID *string + if len(s.KeyID) > 0 { + keyID = &s.KeyID + } + + _, err := s.client.PutParameterWithContext(context.TODO(), &ssm.PutParameterInput{ + KeyId: keyID, // the kms key to use to encrypt. empty = default key + Name: &name, + Overwrite: aws.Bool(true), + Type: aws.String("SecureString"), + Value: &secretStr, + }) + if err != nil { + return fmt.Errorf("ssm: creating secret: %w", err) + } + + return nil +} + +// GetSecret +// must have permission secretsmanager:GetSecretValue +// kms:Decrypt - required only if you use a customer-managed Amazon Web Services KMS key to encrypt the secret +func (s *AWSSystemManagerParameterStore) GetSecret(name string) (secret []byte, err error) { + name = invalidSecretNameChars.ReplaceAllString(name, "_") + + p, err := s.client.GetParameterWithContext(context.TODO(), &ssm.GetParameterInput{ + Name: &name, + WithDecryption: aws.Bool(true), + }) + if err != nil { + var aerr awserr.Error + if errors.As(err, &aerr) { + if aerr.Code() == ssm.ErrCodeParameterNotFound { + return nil, nil + } + } + + return nil, fmt.Errorf("ssm: get secret: %w", err) + } + + return []byte(*p.Parameter.Value), nil +} diff --git a/secrets/kubernetes.go b/secrets/kubernetes.go index 798fecc32f..9a8e4f35f9 100644 --- a/secrets/kubernetes.go +++ b/secrets/kubernetes.go @@ -14,6 +14,8 @@ import ( "k8s.io/client-go/kubernetes" ) +var _ SecretStorage = &KubernetesSecretProvider{} + type KubernetesSecretProvider struct { Namespace string client *kubernetes.Clientset @@ -67,10 +69,10 @@ func (k *KubernetesSecretProvider) SetSecret(name string, secret []byte) error { Data: data, }, metav1.CreateOptions{}) if err != nil { - return fmt.Errorf("creating secret: %w", err) + return fmt.Errorf("k8s: creating secret: %w", err) } } else if err != nil { - return fmt.Errorf("patching secret: %w", err) + return fmt.Errorf("k8s: patching secret: %w", err) } return nil diff --git a/secrets/kubernetes_test.go b/secrets/kubernetes_test.go deleted file mode 100644 index 9e412c4db6..0000000000 --- a/secrets/kubernetes_test.go +++ /dev/null @@ -1,3 +0,0 @@ -package secrets - -var _ SecretStorage = &KubernetesSecretProvider{} diff --git a/secrets/secrets_test.go b/secrets/secrets_test.go index 10cbe4d27d..906768744f 100644 --- a/secrets/secrets_test.go +++ b/secrets/secrets_test.go @@ -13,6 +13,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/kms" "github.com/aws/aws-sdk-go/service/secretsmanager" + "github.com/aws/aws-sdk-go/service/ssm" "github.com/hashicorp/vault/api" "github.com/infrahq/infra/testutil/docker" "github.com/stretchr/testify/require" @@ -41,9 +42,9 @@ func TestMain(m *testing.M) { } var ( - awskms *kms.KMS - awsssm *secretsmanager.SecretsManager - containerIDs []string + localstackCfg *aws.Config + awskms *kms.KMS + containerIDs []string ) func setup() { @@ -61,7 +62,7 @@ func setup() { }, nil, // cmd []string{ - "SERVICES=secretsmanager", + "SERVICES=secretsmanager,ssm", }, ) containerIDs = append(containerIDs, containerID) @@ -97,7 +98,7 @@ func setup() { awskms = kms.New(sess, cfg) // for localstack (secrets manager, etc) - cfg2 := aws.NewConfig(). + localstackCfg = aws.NewConfig(). WithCredentials(credentials.NewCredentials(&credentials.StaticProvider{ Value: credentials.Value{ AccessKeyID: "test", @@ -106,7 +107,6 @@ func setup() { })). WithEndpoint("http://localhost:4566"). WithRegion("us-east-1") - awsssm = secretsmanager.New(sess, cfg2) } func teardown() { @@ -145,9 +145,17 @@ func eachProvider(t *testing.T, eachFunc func(t *testing.T, p interface{})) { providers["awskms"] = k // add AWS Secrets Manager - ssm := NewAWSSecretsManager(awsssm) + sess := session.Must(session.NewSession()) + awssm := secretsmanager.New(sess, localstackCfg) + sm := NewAWSSecretsManager(awssm) + + waitForLocalstackReady(t, awssm) + + providers["awssm"] = sm - waitForSecretsManagerReady(t, awsssm) + // add AWS SSM (Systems Manager Parameter Store) + awsssm := ssm.New(sess, localstackCfg) + ssm := NewAWSSystemManagerParameterStore(awsssm) providers["awsssm"] = ssm diff --git a/secrets/vault.go b/secrets/vault.go index 7f0ca97989..963753182a 100644 --- a/secrets/vault.go +++ b/secrets/vault.go @@ -10,6 +10,12 @@ import ( var DefaultVaultAlgorithm = "aes256-gcm96" +// ensure these interfaces are implemented properly +var ( + _ SecretSymmetricKeyProvider = &VaultSecretProvider{} + _ SecretStorage = &VaultSecretProvider{} +) + type VaultSecretProvider struct { TransitMount string `yaml:"transit_mount"` // mounting point. defaults to /transit SecretMount string `yaml:"secret_mount"` // mounting point. defaults to /secret @@ -63,14 +69,14 @@ func (v *VaultSecretProvider) GetSecret(name string) ([]byte, error) { data, ok := sec.Data["data"].(map[string]interface{}) if !ok { - return nil, fmt.Errorf("secret data is unexpected not stored in a map") + return nil, fmt.Errorf("vault: secret data is unexpected not stored in a map") } if data, ok := data["data"].(string); ok { return []byte(data), nil } - return nil, fmt.Errorf("secret data is not a string") + return nil, fmt.Errorf("vault: secret data is not a string") } func (v *VaultSecretProvider) SetSecret(name string, secret []byte) error { @@ -96,13 +102,13 @@ func (v *VaultSecretProvider) GenerateDataKey(name, rootKeyID string) (*Symmetri // generate a new data key dataKey, err := cryptoRandRead(32) // 256 bit if err != nil { - return nil, fmt.Errorf("generating data key: %w", err) + return nil, fmt.Errorf("vault: generating data key: %w", err) } // encrypt the data key encrypted, err := v.RemoteEncrypt(rootKeyID, dataKey) if err != nil { - return nil, fmt.Errorf("remote encrypt: %w", err) + return nil, fmt.Errorf("vault: remote encrypt: %w", err) } return &SymmetricKey{ diff --git a/secrets/vault_test.go b/secrets/vault_test.go index 6322c088ad..8e36ebc212 100644 --- a/secrets/vault_test.go +++ b/secrets/vault_test.go @@ -8,12 +8,6 @@ import ( // though not required, you can run a vault server locally for these tests: // vault server -dev -dev-root-token-id="root" -// ensure these interfaces are implemented properly -var ( - _ SecretSymmetricKeyProvider = &VaultSecretProvider{} - _ SecretStorage = &VaultSecretProvider{} -) - func waitForVaultReady(t *testing.T, v *VaultSecretProvider) { deadline := time.Now().Add(10 * time.Second)