Skip to content

Commit

Permalink
aws secrets manager wrapper (#1081)
Browse files Browse the repository at this point in the history
  • Loading branch information
skudasov authored Aug 21, 2024
1 parent 3e7c014 commit 2738403
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 1 deletion.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
24 changes: 24 additions & 0 deletions SECRETS.md
Original file line number Diff line number Diff line change
@@ -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)
131 changes: 131 additions & 0 deletions client/secretsmanager.go
Original file line number Diff line number Diff line change
@@ -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
}
67 changes: 67 additions & 0 deletions client/secretsmanager_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
}
22 changes: 21 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
28 changes: 28 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down

0 comments on commit 2738403

Please sign in to comment.