Skip to content

Commit

Permalink
ssm parameter store support (#512)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssoroka authored Oct 22, 2021
1 parent ac3d69e commit 4007347
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 34 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
11 changes: 8 additions & 3 deletions secrets/kms.go → secrets/awskms.go
Original file line number Diff line number Diff line change
@@ -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
}
Expand All @@ -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{
Expand All @@ -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
Expand All @@ -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{
Expand Down
3 changes: 0 additions & 3 deletions secrets/kms_test.go → secrets/awskms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions secrets/awssecretsmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion secrets/awssecretsmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
80 changes: 80 additions & 0 deletions secrets/awssystemmanagerparameterstore.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 4 additions & 2 deletions secrets/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"k8s.io/client-go/kubernetes"
)

var _ SecretStorage = &KubernetesSecretProvider{}

type KubernetesSecretProvider struct {
Namespace string
client *kubernetes.Clientset
Expand Down Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions secrets/kubernetes_test.go

This file was deleted.

24 changes: 16 additions & 8 deletions secrets/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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() {
Expand All @@ -61,7 +62,7 @@ func setup() {
},
nil, // cmd
[]string{
"SERVICES=secretsmanager",
"SERVICES=secretsmanager,ssm",
},
)
containerIDs = append(containerIDs, containerID)
Expand Down Expand Up @@ -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",
Expand All @@ -106,7 +107,6 @@ func setup() {
})).
WithEndpoint("http://localhost:4566").
WithRegion("us-east-1")
awsssm = secretsmanager.New(sess, cfg2)
}

func teardown() {
Expand Down Expand Up @@ -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

Expand Down
14 changes: 10 additions & 4 deletions secrets/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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{
Expand Down
6 changes: 0 additions & 6 deletions secrets/vault_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit 4007347

Please sign in to comment.