Skip to content

Commit

Permalink
feat: add secrets manager provider (#12)
Browse files Browse the repository at this point in the history
* add secrets manager provider

* put secrets in secrets manager

* implement get many

* get secrets by path

* fix put secret

* force delete without recovery

* update readme
  • Loading branch information
adikari authored Nov 22, 2022
1 parent b4f1f9d commit 4981b22
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 66 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 📦 SafeBox

SafeBox is a command line tool for managing secrets for your application. Currently it supports AWS Parameter Store.
SafeBox is a command line tool for managing secrets for your application. Currently it supports AWS Parameter Store as AWS Secrets Manager.

## Installation

Expand Down Expand Up @@ -60,15 +60,16 @@ You can then run list command to view the pushed configurations.

The variables under
1. `defaults` is deployed with path prefix of `/<stage>/<service>`
1. `shared` is deployed with path prefix of `/shared/`
1. `shared` is deployed with path prefix of `/<stage>/shared/`

### Config File

Following is the configuration file will all possible options:

```yaml
service: my-service
provider: ssm # Only supports ssm for now.
provider: secrets-manager # ssm OR secrets-manager
prefix: "/custom/prefix/{{.stage}}/" # Optional. Defaults to /<stage>/<service>/. Prefix all parameters. Does not apply for shared
stacks: # Outputs from cloudformation stacks that needs to be interpolated.
- some-cloudformation-stack
Expand All @@ -77,16 +78,25 @@ config:
defaults: # Default parameters. Can be overwritten in different environments.
DB_NAME: my-database
DB_HOST: 3200
KEY_VALUE_SECRET: '{"hello": "world"}' # JSON body can be passed when provider is secrets-manager. This will create key value secret
production: # If keys are deployed to production stage, its value will be overwritten by following
DB_NAME: my-production-database
shared: # shared configuartions deployed under /shared/ path
shared: # shared configuartions deployed under /<stage>/shared/ path
DB_TABLE: "table-{{.stage}}"
secret:
defaults:
DB_PASSWORD: "secret database password" # Value in quote is deployed as description of the ssm parameter.
```

**Variables available for interpolation**
- stage - Stage used for deployment
- service - Name of service as configured in the config file
- account - AWS Account number
- region - AWS Region

If using `stacks` then the outputs of that Cloudformation stack is also available for interpolation.

### CLI

Following is all options available in `safebox` CLI.
Expand Down
13 changes: 1 addition & 12 deletions aws/cloudformation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,9 @@ import (
"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/service/cloudformation"
)

var (
numberOfRetries = 10
throttleDelay = client.DefaultRetryerMinRetryDelay
)

var cfClient *cloudformation.CloudFormation

type Cloudformation struct {
Expand Down Expand Up @@ -59,13 +53,8 @@ func (c *Cloudformation) GetOutputs(stacknames []string) (map[string]string, err

func NewCloudformation() Cloudformation {
if cfClient == nil {
retryer := client.DefaultRetryer{
NumMaxRetries: numberOfRetries,
MinThrottleDelay: throttleDelay,
}

cfClient = cloudformation.New(Session, &aws.Config{
Retryer: retryer,
Retryer: Retryer,
})
}

Expand Down
15 changes: 14 additions & 1 deletion aws/session.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
package aws

import "github.com/aws/aws-sdk-go/aws/session"
import (
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/session"
)

var Session = session.Must(session.NewSession())

var (
numberOfRetries = 10
throttleDelay = client.DefaultRetryerMinRetryDelay
)

var Retryer = client.DefaultRetryer{
NumMaxRetries: numberOfRetries,
MinThrottleDelay: throttleDelay,
}
7 changes: 2 additions & 5 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cmd

import (
"fmt"
"log"

"github.com/adikari/safebox/v2/store"
"github.com/manifoldco/promptui"
Expand Down Expand Up @@ -115,7 +114,7 @@ func deploy(cmd *cobra.Command, args []string) error {
if removeOrphans {
orphans, err := doRemoveOrphans(st, config.Prefix, config.All)
if err != nil {
log.Print("failed to remove orphans")
fmt.Printf("%s\n", errors.Wrap(err, "Error: failed to remove orphan"))
}

fmt.Printf("%d orphans removed.\n", len(orphans))
Expand Down Expand Up @@ -149,9 +148,7 @@ func doRemoveOrphans(st store.Store, prefix string, all []store.ConfigInput) ([]
}
}

err = st.DeleteMany(orphans)

if err != nil {
if err = st.DeleteMany(orphans); err != nil {
return nil, err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func printList(configs []store.Config) {
fmt.Fprintln(w, "")

for _, config := range configs {
fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s",
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s",
*config.Name,
*config.Value,
config.Type,
Expand Down
24 changes: 24 additions & 0 deletions example/secretsmanager.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
service: safebox
provider: secrets-manager

cloudformation-stacks:
- "{{.stage}}-shared-infra-SharedInfraServerless"
- "{{.stage}}-user-debug-stack"

config:
defaults:
DB_NAME: '{"hello": "world"}'
CF_OUTPUT_API_ENDPOINT: "{{.internalDomainName}}"
ENDPOINT: "endpoint-{{.stage}}"
AWS_REGION: "{{.region}} {{.account}}"
CF_OUTPUT_BUCKET_ARN: "{{.BucketArn}}"

shared:
SHARED_KEY: "shared key"

secret:
defaults:
API_KEY: "key of the api endpoint"

shared:
APOLLO_KEY: "apollo key"
182 changes: 182 additions & 0 deletions store/secrets-manager-store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package store

import (
a "github.com/adikari/safebox/v2/aws"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
"github.com/pkg/errors"
)

var _ Store = &SecretsManagerStore{}

type SecretsManagerStore struct {
svc secretsmanageriface.SecretsManagerAPI
}

var secretsmanagerService *secretsmanager.SecretsManager

func NewSecretsManagerStore() (*SecretsManagerStore, error) {
if secretsmanagerService == nil {
secretsmanagerService = secretsmanager.New(a.Session, &aws.Config{
Retryer: a.Retryer,
})
}

return &SecretsManagerStore{
svc: secretsmanagerService,
}, nil
}

func (s *SecretsManagerStore) Create(input ConfigInput) error {
param := &secretsmanager.CreateSecretInput{
Name: aws.String(input.Name),
SecretString: aws.String(input.Value),
}

if _, err := s.svc.CreateSecret(param); err != nil {
return errors.Wrap(err, input.Name)
}

return nil
}

func (s *SecretsManagerStore) Update(input ConfigInput) error {
param := &secretsmanager.UpdateSecretInput{
SecretId: aws.String(input.Name),
SecretString: aws.String(input.Value),
}

if _, err := s.svc.UpdateSecret(param); err != nil {
return errors.Wrap(err, input.Name)
}

return nil
}

func (s *SecretsManagerStore) Put(input ConfigInput) error {
found, _ := s.Get(input)

var err error
if found != nil {
err = s.Update(input)
} else {
err = s.Create(input)
}

if err != nil {
return err
}

return nil
}

func (s *SecretsManagerStore) PutMany(inputs []ConfigInput) error {
for _, config := range inputs {
if err := s.Put(config); err != nil {
return err
}
}

return nil
}

func (s *SecretsManagerStore) Get(input ConfigInput) (*Config, error) {
param := &secretsmanager.GetSecretValueInput{
SecretId: aws.String(input.Name),
}

result, err := s.svc.GetSecretValue(param)

if err != nil {
return nil, err
}

return &Config{
Name: result.Name,
Value: result.SecretString,
Version: *result.VersionId,
Type: "SecureString",
DataType: "SecureString",
Modified: *result.CreatedDate,
}, nil
}

func (s *SecretsManagerStore) GetMany(inputs []ConfigInput) ([]Config, error) {
if len(inputs) <= 0 {
return []Config{}, nil
}

result := []Config{}

for _, input := range inputs {
res, _ := s.Get(input)
if res != nil {
result = append(result, *res)
}
}

return result, nil
}

func (s *SecretsManagerStore) GetByPath(path string) ([]Config, error) {
var result []Config

input := &secretsmanager.ListSecretsInput{
Filters: []*secretsmanager.Filter{
{
Key: aws.String("name"),
Values: []*string{aws.String(path)},
},
},
}

var recursiveGet func()
recursiveGet = func() {
resp, err := s.svc.ListSecrets(input)

if err != nil {
return
}

for _, secret := range resp.SecretList {
result = append(result, Config{Name: secret.Name})
}

if resp.NextToken != nil {
input.NextToken = resp.NextToken
recursiveGet()
}
}

recursiveGet()

return result, nil
}

func (s *SecretsManagerStore) Delete(input ConfigInput) error {
param := &secretsmanager.DeleteSecretInput{
ForceDeleteWithoutRecovery: aws.Bool(true),
SecretId: aws.String(input.Name),
}

if _, err := s.svc.DeleteSecret(param); err != nil {
return errors.Wrap(err, input.Name)
}

return nil
}

func (s *SecretsManagerStore) DeleteMany(inputs []ConfigInput) error {
if len(inputs) <= 0 {
return nil
}

for _, input := range inputs {
if err := s.Delete(input); err != nil {
return err
}
}

return nil
}
Loading

0 comments on commit 4981b22

Please sign in to comment.