diff --git a/Gopkg.lock b/Gopkg.lock index 555f4b6..08c9c10 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,7 +2,7 @@ [[projects]] - digest = "1:be208c551e1739aa79df6f81cf3fd8855e4ab8ef8a8b98037005852b3d2d4543" + digest = "1:e0c0de03e77d09061d32e4f382cd88c491c212fa3457199d51385f46bf91b2d4" name = "github.com/aws/aws-sdk-go-v2" packages = [ "aws", @@ -26,6 +26,7 @@ "private/protocol/rest", "private/protocol/xml/xmlutil", "service/kms", + "service/secretsmanager", "service/ssm", "service/sts", ] @@ -44,8 +45,10 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ + "github.com/aws/aws-sdk-go-v2/aws", "github.com/aws/aws-sdk-go-v2/aws/external", "github.com/aws/aws-sdk-go-v2/service/kms", + "github.com/aws/aws-sdk-go-v2/service/secretsmanager", "github.com/aws/aws-sdk-go-v2/service/ssm", ] solver-name = "gps-cdcl" diff --git a/Makefile b/Makefile index e0513fb..1dbf577 100644 --- a/Makefile +++ b/Makefile @@ -8,11 +8,11 @@ clean: rm ./secure-exec || true test: - go test -v -tags "awskms awsssm" ./... -coverprofile=coverage.txt -covermode=atomic + go test -v -tags "awskms awssecretsmanager awsssm" ./... -coverprofile=coverage.txt -covermode=atomic build: - GOOS=linux GOARCH=amd64 go build -i -tags 'awskms awsssm' -o "secure-exec-linux-amd64" - GOOS=darwin GOARCH=amd64 go build -i -tags 'awskms awsssm' -o "secure-exec-darwin-amd64" + GOOS=linux GOARCH=amd64 go build -i -tags 'awskms awssecretsmanager awsssm' -o "secure-exec-linux-amd64" + GOOS=darwin GOARCH=amd64 go build -i -tags 'awskms awssecretsmanager awsssm' -o "secure-exec-darwin-amd64" docker: docker build -t secure-exec-example . diff --git a/README.md b/README.md index a7f9d27..7f8405b 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,10 @@ `secure-exec` populates secrets using AWS KMS or SSM into your app. It looks for prefixed variables in environment and replaces them: - - `{aws-kms}encrypted-text` - decrypts the value using AWS KMS - - `{aws-ssm}parameter-name` - loads parameters from AWS Systems Manager Parameter Store + - `{aws-kms}AQICAHjA3mwbmf...` - decrypts the value using AWS KMS + - `{aws-ssm}/app/staging/param` - loads parameter `/app/staging/param` from AWS Systems Manager Parameter Store + - `{aws-sm}/app/staging/param` - loads secret `/app/staging/param` from AWS Secrets Manager + - `{aws-sm}/app/staging/param{prop1}` - loads secret `/app/staging/param` from AWS Secrets Manager and takes `prop1` property Then it runs `exec` system call and replaces itself with your app. diff --git a/main.go b/main.go index 66ad69f..9dbb55b 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/s12v/secure-exec/provider" _ "github.com/s12v/secure-exec/provider/awskms" + _ "github.com/s12v/secure-exec/provider/awssecretsmanager" + _ "github.com/s12v/secure-exec/provider/awssecretsmanager" _ "github.com/s12v/secure-exec/provider/awsssm" "os" "syscall" diff --git a/provider/awssecretsmanager/awsecretsmanager.go b/provider/awssecretsmanager/awsecretsmanager.go new file mode 100644 index 0000000..8c6ba38 --- /dev/null +++ b/provider/awssecretsmanager/awsecretsmanager.go @@ -0,0 +1,96 @@ +// +build awssecretsmanager + +package awssecretsmanager + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/external" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "github.com/s12v/secure-exec/provider" + "regexp" + "strings" +) + +type SecretsManagerProvider struct { + awsClient *secretsmanager.SecretsManager +} + +const prefix = "{aws-sm}" + +var postfix = regexp.MustCompile("{[^{^}]+}$") + +var fetch func( + awsClient *secretsmanager.SecretsManager, + input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) + +func init() { + cfg, err := external.LoadDefaultAWSConfig() + if err != nil { + panic("unable to load AWS-SDK config, " + err.Error()) + } + + fetch = awsFetch + provider.Register(&SecretsManagerProvider{secretsmanager.New(cfg)}) +} + +func awsFetch( + awsClient *secretsmanager.SecretsManager, + input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) { + if resp, err := awsClient.GetSecretValueRequest(input).Send(); err != nil { + return nil, errors.New(fmt.Sprintf("AWS SecretsManager error: %v", err)) + } else { + return resp, nil + } +} + +func (p *SecretsManagerProvider) Match(val string) bool { + return strings.HasPrefix(val, prefix) && len(val) > len(prefix) +} + +func (p *SecretsManagerProvider) Decode(val string) (string, error) { + name := val[len(prefix):] + property := postfix.FindString(name) + if property != "" { + return p.decodeJson(name, strings.Trim(property, "{}")) + } + return p.fetchString(name) +} + +func (p *SecretsManagerProvider) decodeJson(val string, property string) (string, error) { + name := val[:len(val)-len(property) - 2] + jsobj, err := p.fetchString(name) + if err != nil { + return "", err + } + + properties, _ := unmarshal(jsobj) + value, ok := properties[property] + if !ok { + return "", errors.New(fmt.Sprintf("property '%v' does not exist", property)) + } + return value, nil +} + +func (p *SecretsManagerProvider) fetchString(name string) (string, error) { + input := &secretsmanager.GetSecretValueInput{ + SecretId: aws.String(name), + } + if err := input.Validate(); err != nil { + return "", err + } + + if output, err := fetch(p.awsClient, input); err != nil { + return "", err + } else { + return *output.SecretString, nil + } +} + +func unmarshal(val string) (map[string]string, error) { + var omap map[string]string + err := json.Unmarshal([]byte(val), &omap) + return omap, err +} diff --git a/provider/awssecretsmanager/awsecretsmanager_test.go b/provider/awssecretsmanager/awsecretsmanager_test.go new file mode 100644 index 0000000..08e7da0 --- /dev/null +++ b/provider/awssecretsmanager/awsecretsmanager_test.go @@ -0,0 +1,135 @@ +// +build awssecretsmanager + +package awssecretsmanager + +import ( + "errors" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "testing" +) + +func TestSecretsManagerProvider_Match(t *testing.T) { + provider := SecretsManagerProvider{} + + if provider.Match("{aws-sm}something") != true { + t.Fatal("expected to match") + } + + if provider.Match("https://example.com") != false { + t.Fatal("not expected to match") + } +} + +func TestSecretsManagerProvider_Decode(t *testing.T) { + provider := SecretsManagerProvider{} + + value := "boom" + fetch = func( + awsClient *secretsmanager.SecretsManager, + input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) { + if *input.SecretId != "/foo/bar" { + t.Fatalf("unexpected SecretId %v", input.SecretId) + } + + return &secretsmanager.GetSecretValueOutput{SecretString: &value}, nil + } + + if r, _ := provider.Decode("{aws-sm}/foo/bar"); r != "boom" { + t.Fatalf("unexpected value %v", r) + } +} + +func TestSecretsManagerProvider_DecodeJson(t *testing.T) { + provider := SecretsManagerProvider{} + + value := `{"prop1": "aaa", "prop2": "bbb"}` + fetch = func( + awsClient *secretsmanager.SecretsManager, + input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) { + if *input.SecretId != "/foo/bar" { + t.Fatalf("unexpected SecretId %v", *input.SecretId) + } + + return &secretsmanager.GetSecretValueOutput{SecretString: &value}, nil + } + + if r, _ := provider.Decode("{aws-sm}/foo/bar{prop2}"); r != "bbb" { + t.Fatalf("unexpected value %v", r) + } +} + +func TestSecretsManagerProvider_DecodeJson_MissingProperty(t *testing.T) { + provider := SecretsManagerProvider{} + + value := `{"prop1": "foo", "prop2": "bar"}` + fetch = func( + awsClient *secretsmanager.SecretsManager, + input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) { + if *input.SecretId != "/foo/bar" { + t.Fatalf("unexpected SecretId %v", *input.SecretId) + } + + return &secretsmanager.GetSecretValueOutput{SecretString: &value}, nil + } + + if _, err := provider.Decode("{aws-sm}/foo/bar{prop3}"); err == nil { + t.Fatal("expected an error") + } +} + +func TestSecretsManagerProvider_Decode_FetchError(t *testing.T) { + provider := SecretsManagerProvider{} + + fetch = func( + awsClient *secretsmanager.SecretsManager, + input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) { + + return nil, errors.New("test error") + } + + if _, err := provider.Decode("{aws-sm}/foo/bar"); err == nil { + t.Fatal("expected an error") + } +} + +func TestSecretsManagerProvider_DecodeJson_FetchError(t *testing.T) { + provider := SecretsManagerProvider{} + + fetch = func( + awsClient *secretsmanager.SecretsManager, + input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) { + + return nil, errors.New("test error") + } + + if _, err := provider.Decode("{aws-sm}/foo/bar{prop1}"); err == nil { + t.Fatal("expected an error") + } +} + +func TestSecretsManagerProvider_Decode_InvalidInput(t *testing.T) { + provider := SecretsManagerProvider{} + r, err := provider.Decode("{aws-sm}") + if err == nil { + t.Fatal("expected an error", r) + } + if r != "" { + t.Fatalf("unexpected result: '%v'", r) + } +} + +func Test_Unmarshal(t *testing.T) { + jsonobj, err := unmarshal(`{"prop1": "foo", "prop2": "bar"}`) + + if err != nil { + t.Fatal("expected error: ", err) + } + + if jsonobj["prop1"] != "foo" { + t.Fatalf("unexpected value '%v'", jsonobj["prop1"]) + } + + if jsonobj["prop2"] != "bar" { + t.Fatalf("unexpected value '%v'", jsonobj["prop2"]) + } +} diff --git a/provider/awssecretsmanager/noop.go b/provider/awssecretsmanager/noop.go new file mode 100644 index 0000000..8847f6d --- /dev/null +++ b/provider/awssecretsmanager/noop.go @@ -0,0 +1,6 @@ +// +build !awssecretsmanager + +package awssecretsmanager + +func init() { +} diff --git a/provider/awsssm/awsssm.go b/provider/awsssm/awsssm.go index f15eb2d..4d0bcc8 100644 --- a/provider/awsssm/awsssm.go +++ b/provider/awsssm/awsssm.go @@ -1,6 +1,6 @@ // +build awsssm -package awskms +package awsssm import ( "errors" @@ -12,7 +12,7 @@ import ( ) type SsmProvider struct { - awsSSmClient *ssm.SSM + awsSsmClient *ssm.SSM } const prefix = "{aws-ssm}" @@ -49,7 +49,7 @@ func (p *SsmProvider) Decode(val string) (string, error) { return "", err } - if output, err := fetch(p.awsSSmClient, input); err != nil { + if output, err := fetch(p.awsSsmClient, input); err != nil { return "", err } else { return *output.Parameter.Value, nil diff --git a/provider/awsssm/awsssm_test.go b/provider/awsssm/awsssm_test.go index 3f6e7b6..760d104 100644 --- a/provider/awsssm/awsssm_test.go +++ b/provider/awsssm/awsssm_test.go @@ -1,6 +1,6 @@ // +build awsssm -package awskms +package awsssm import ( "github.com/aws/aws-sdk-go-v2/service/ssm" @@ -8,13 +8,13 @@ import ( ) func TestSsmProvider_Match(t *testing.T) { - kmsProvider := SsmProvider{} + ssmProvider := SsmProvider{} - if kmsProvider.Match("{aws-ssm}something") != true { + if ssmProvider.Match("{aws-ssm}something") != true { t.Fatal("expected to match") } - if kmsProvider.Match("https://example.com") != false { + if ssmProvider.Match("https://example.com") != false { t.Fatal("not expected to match") } } diff --git a/provider/awsssm/noop.go b/provider/awsssm/noop.go index 0629e46..b7d3162 100644 --- a/provider/awsssm/noop.go +++ b/provider/awsssm/noop.go @@ -1,6 +1,6 @@ // +build !awsssm -package awskms +package awsssm func init() { }