Skip to content

Commit

Permalink
AWS Secrets Manager (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergey Novikov authored Dec 16, 2018
1 parent db15749 commit f3dbaa6
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 14 deletions.
5 changes: 4 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
96 changes: 96 additions & 0 deletions provider/awssecretsmanager/awsecretsmanager.go
Original file line number Diff line number Diff line change
@@ -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
}
135 changes: 135 additions & 0 deletions provider/awssecretsmanager/awsecretsmanager_test.go
Original file line number Diff line number Diff line change
@@ -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"])
}
}
6 changes: 6 additions & 0 deletions provider/awssecretsmanager/noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// +build !awssecretsmanager

package awssecretsmanager

func init() {
}
6 changes: 3 additions & 3 deletions provider/awsssm/awsssm.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// +build awsssm

package awskms
package awsssm

import (
"errors"
Expand All @@ -12,7 +12,7 @@ import (
)

type SsmProvider struct {
awsSSmClient *ssm.SSM
awsSsmClient *ssm.SSM
}

const prefix = "{aws-ssm}"
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions provider/awsssm/awsssm_test.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// +build awsssm

package awskms
package awsssm

import (
"github.com/aws/aws-sdk-go-v2/service/ssm"
"testing"
)

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")
}
}
Expand Down
2 changes: 1 addition & 1 deletion provider/awsssm/noop.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// +build !awsssm

package awskms
package awsssm

func init() {
}

0 comments on commit f3dbaa6

Please sign in to comment.