diff --git a/README.md b/README.md index 2670d2aab..ea1b7cc2a 100644 --- a/README.md +++ b/README.md @@ -423,3 +423,7 @@ We have two ways to add new images to the ecr. The first two requirements are th export SETH_LOG_LEVEL=info export RESTY_DEBUG=true ``` + +## Using AWS Secrets Manager + +Check the [docs](SECRETS.md) diff --git a/SECRETS.md b/SECRETS.md new file mode 100644 index 000000000..d407b1ac5 --- /dev/null +++ b/SECRETS.md @@ -0,0 +1,24 @@ +## Using AWSSecretsManager from code + +`client/secretsmanager.go` has a simple API to read/write/delete secrets. + +It uses a struct to protect such secrets from accidental printing or marshalling, see an [example](client/secretsmanager_test.go) test + +## Using AWSSecretsManager via CLI + +To create a static secret use `aws cli` + +``` +aws --region us-west-2 secretsmanager create-secret \ + --name MyTestSecret \ + --description "My test secret created with the CLI." \ + --secret-string "{\"user\":\"diegor\",\"password\":\"EXAMPLE-PASSWORD\"}" +``` + +Example of reading the secret + +``` +aws --region us-west-2 secretsmanager get-secret-value --secret-id MyTestSecret +``` + +For more information check [AWS CLI Reference](https://docs.aws.amazon.com/cli/v1/userguide/cli_secrets-manager_code_examples.html) diff --git a/client/secretsmanager.go b/client/secretsmanager.go new file mode 100644 index 000000000..b4706c4b0 --- /dev/null +++ b/client/secretsmanager.go @@ -0,0 +1,131 @@ +package client + +/* +This client helps us to store and load secrets in AWS Secrets Manager +It also prevents secrets from being printed by mistake +*/ + +import ( + "context" + "encoding" + "encoding/json" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" +) + +var ( + redacted = "***" + redactedJSON = "{\"key\":\"is_redacted\"}" + redactedTOML = "[Key]\n\nis = redacted" +) + +// AWSSecret is a wrapper preventing accidental printing or marshalling +type AWSSecret string + +// Value is used to return masked secret value +func (s AWSSecret) Value() string { return string(s) } + +// The String method is used to print values passed as an operand +// to any format that accepts a string or to an unformatted printer +// such as Print. +func (s AWSSecret) String() string { return redacted } + +// The GoString method is used to print values passed as an operand +// to a %#v format. +func (s AWSSecret) GoString() string { return redacted } + +// MarshalText encodes the receiver into UTF-8-encoded text and returns the result. +func (s AWSSecret) MarshalText() ([]byte, error) { return []byte(redactedTOML), nil } + +// MarshalJSON Marshaler is the interface implemented by types that +// can marshal themselves into valid JSON. +func (s AWSSecret) MarshalJSON() ([]byte, error) { return []byte(redactedJSON), nil } + +var ( + _ fmt.Stringer = (*AWSSecret)(nil) + _ fmt.GoStringer = (*AWSSecret)(nil) + _ encoding.TextMarshaler = (*AWSSecret)(nil) + _ json.Marshaler = (*AWSSecret)(nil) +) + +// AWSSecretsManager is an AWS Secrets Manager service wrapper +type AWSSecretsManager struct { + Client *secretsmanager.Client + RequestTimeout time.Duration + l zerolog.Logger +} + +// NewAWSSecretsManager create a new connection to AWS Secrets Manager +func NewAWSSecretsManager(requestTimeout time.Duration) (*AWSSecretsManager, error) { + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + return nil, fmt.Errorf("unable to load AWS SDK config, %v", err) + } + l := log.Logger.With().Str("Component", "AWSSecretsManager").Logger() + l.Info().Msg("Connecting to AWS Secrets Manager") + return &AWSSecretsManager{ + Client: secretsmanager.NewFromConfig(cfg), + RequestTimeout: requestTimeout, + l: l, + }, nil +} + +// CreateSecret creates a specific secret by key +func (sm *AWSSecretsManager) CreateSecret(key string, val string, override bool) error { + ctx, cancel := context.WithTimeout(context.Background(), sm.RequestTimeout) + defer cancel() + + sm.l.Debug().Str("Key", key).Msg("Creating secret by key") + k := &key + v := &val + _, err := sm.Client.CreateSecret(ctx, &secretsmanager.CreateSecretInput{ + Name: k, + SecretString: v, + ForceOverwriteReplicaSecret: override, + }) + if err != nil { + return errors.Wrapf(err, "failed to create a secret by key") + } + return nil +} + +// GetSecret gets a specific secret by key +func (sm *AWSSecretsManager) GetSecret(key string) (AWSSecret, error) { + ctx, cancel := context.WithTimeout(context.Background(), sm.RequestTimeout) + defer cancel() + + sm.l.Debug().Str("Key", key).Msg("Reading secret by key") + k := &key + out, err := sm.Client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ + SecretId: k, + }) + if err != nil { + return "", errors.Wrapf(err, "failed to read a secret by key") + } + return AWSSecret(*out.SecretString), nil +} + +// RemoveSecret removes a specific secret by key +func (sm *AWSSecretsManager) RemoveSecret(key string, noRecovery bool) error { + ctx, cancel := context.WithTimeout(context.Background(), sm.RequestTimeout) + defer cancel() + + sm.l.Debug().Str("Key", key).Msg("Removing secret by key") + k := &key + b := &noRecovery + _, err := sm.Client.DeleteSecret(ctx, &secretsmanager.DeleteSecretInput{ + SecretId: k, + ForceDeleteWithoutRecovery: b, + }) + if err != nil { + return errors.Wrapf(err, "failed to remove a secret by key") + } + return nil +} diff --git a/client/secretsmanager_test.go b/client/secretsmanager_test.go new file mode 100644 index 000000000..e4679c7a7 --- /dev/null +++ b/client/secretsmanager_test.go @@ -0,0 +1,67 @@ +package client + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/google/uuid" + "github.com/pelletier/go-toml/v2" + "github.com/stretchr/testify/require" +) + +func TestSecretsPrintMarshal(t *testing.T) { + s := AWSSecret("1") + t.Run("print the whole struct as string", func(t *testing.T) { + //nolint + output := fmt.Sprintf("%s", s) + require.Equal(t, redacted, output) + }) + t.Run("print the whole struct as +v%", func(t *testing.T) { + output := fmt.Sprintf("%+v", s) + require.Equal(t, redacted, output) + }) + t.Run("spew library should not work either", func(t *testing.T) { + output := spew.Sdump(s) + require.Equal(t, "(client.AWSSecret) (len=1) ***", output[:len(output)-1]) + }) + t.Run("marshal the whole struct as JSON", func(t *testing.T) { + d, err := json.Marshal(s) + require.NoError(t, err) + require.Equal(t, redactedJSON, string(d)) + }) + t.Run("marshal the whole struct as TOML results in err", func(t *testing.T) { + // github.com/pelletier/go-toml/v2 since version 2 does not allow any struct to implement MarshalText() + // so it results in error if we try to marshal secrets + _, err := toml.Marshal(s) + require.Error(t, err) + }) +} + +func TestManualSecretsCRUD(t *testing.T) { + t.Skip("Need AWS role to be enabled") + // fill .envrc with AWS auth values and run manually + // export AWS_REGION="us-west-2" + // export AWS_ACCESS_KEY_ID= + // export AWS_SECRET_ACCESS_KEY= + // export AWS_SESSION_TOKEN= + sm, err := NewAWSSecretsManager(1 * time.Minute) + require.NoError(t, err) + + t.Run("basic single value CRUD", func(t *testing.T) { + k := uuid.NewString() + v := uuid.NewString() + t.Cleanup(func() { + err = sm.RemoveSecret(k, true) + require.NoError(t, err) + }) + err = sm.CreateSecret(k, v, true) + require.NoError(t, err) + secret, err := sm.GetSecret(k) + require.NoError(t, err) + require.Equal(t, secret, AWSSecret(v)) + require.Equal(t, v, AWSSecret(v).Value()) + }) +} diff --git a/go.mod b/go.mod index 39693844c..e4e9d3e33 100644 --- a/go.mod +++ b/go.mod @@ -98,7 +98,7 @@ require ( github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 // indirect github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.3.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dennwc/varint v1.0.0 // indirect @@ -316,6 +316,26 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect ) +require ( + github.com/aws/aws-sdk-go-v2/config v1.27.28 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.5 +) + +require ( + github.com/aws/aws-sdk-go-v2 v1.30.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.28 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 // indirect + github.com/aws/smithy-go v1.20.4 // indirect +) + // avoids ambigious imports of indirect dependencies exclude github.com/hashicorp/consul v1.2.1 diff --git a/go.sum b/go.sum index 6f326b2fe..5774638a8 100644 --- a/go.sum +++ b/go.sum @@ -136,10 +136,38 @@ github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.45.25 h1:c4fLlh5sLdK2DCRTY1z0hyuJZU4ygxX8m1FswL6/nF4= github.com/aws/aws-sdk-go v1.45.25/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8= +github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= +github.com/aws/aws-sdk-go-v2/config v1.27.28 h1:OTxWGW/91C61QlneCtnD62NLb4W616/NM1jA8LhJqbg= +github.com/aws/aws-sdk-go-v2/config v1.27.28/go.mod h1:uzVRVtJSU5EFv6Fu82AoVFKozJi2ZCY6WRCXj06rbvs= +github.com/aws/aws-sdk-go-v2/credentials v1.17.28 h1:m8+AHY/ND8CMHJnPoH7PJIRakWGa4gbfbxuY9TGTUXM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.28/go.mod h1:6TF7dSc78ehD1SL6KpRIPKMA1GyyWflIkjqg+qmf4+c= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.5 h1:UDXu9dqpCZYonj7poM4kFISjzTdWI0v3WUusM+w+Gfc= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.5/go.mod h1:5NPkI3RsTOhwz1CuG7VVSgJCm3CINKkoIaUbUZWQ67w= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 h1:iAckBT2OeEK/kBDyN/jDtpEExhjeeA/Im2q4X0rJZT8= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.4/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0= github.com/aws/constructs-go/constructs/v10 v10.1.255 h1:5hARfEmhBqHSTQf/C3QLA3sWOxO2Dfja0iA1W7ZcI7g= github.com/aws/constructs-go/constructs/v10 v10.1.255/go.mod h1:DCdBSjN04Ck2pajCacTD4RKFqSA7Utya8d62XreYctI= github.com/aws/jsii-runtime-go v1.75.0 h1:NhpUfyiL7/wsRuUekFsz8FFBCYLfPD/l61kKg9kL/a4= github.com/aws/jsii-runtime-go v1.75.0/go.mod h1:TKCyrtM0pygEPo4rDZzbMSDNCDNTSYSN6/mGyHI6O3I= +github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= +github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM=