-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
275 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters