From cbb994d54de424aba4729b81223c5114c91aea71 Mon Sep 17 00:00:00 2001 From: Mitchell Riedstra Date: Sun, 20 Nov 2022 21:08:25 -0500 Subject: Initial --- .gitignore | 1 + LICENSE | 13 ++++ build.sh | 20 ++++++ go.mod | 8 +++ go.sum | 38 +++++++++++ main.go | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 24 +++++++ store/store.go | 207 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 501 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100755 build.sh create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 readme.md create mode 100644 store/store.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..891f012 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/dpw-ssm diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5e96e73 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2022 Mitchell Riedstra + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..e6ef70e --- /dev/null +++ b/build.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +LICENSE="$(cat LICENSE)" + +version="$(git log --format="%h %d" -1) +$(go version) +Build Date: $(date +%m.%d.%Y) +Source code can be found here: +https://git.riedstra.dev/go/dpw-ssm + +$LICENSE" + +if ! git diff-index --quiet HEAD ; then + version="dirty: $version" +fi + +export CGO_ENABLED=0 + +go build -ldflags="-X 'main.VersionString=$version'" . diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..618f55a --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module riedstra.dev/go/dpw-ssm + +go 1.19 + +require ( + github.com/aws/aws-sdk-go v1.44.142 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..88edd18 --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +github.com/aws/aws-sdk-go v1.44.142 h1:KZ1/FDwCSft1DuNllFaBtWpcG0CW2NgQjvOrE1TdlXE= +github.com/aws/aws-sdk-go v1.44.142/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4e11d21 --- /dev/null +++ b/main.go @@ -0,0 +1,190 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + "regexp" + "strings" + + "riedstra.dev/go/dpw-ssm/store" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ssm" +) + +const SSM_MAX_SIZE = 4096 + +// ((16^4)*4096)/1024/1024 +// If we ever need more than 256 MB in parameter store, we've done something +// very wrong. +const SSM_KEY_FORMAT = "%s-%04X" // + +var ( + KMS_KEY_ID *string = nil + VersionString = "development" + svc *ssm.SSM + Logger = log.New(os.Stderr, "", 0) + trimRegex = regexp.MustCompile("-[0-9A-E][0-9A-E][0-9A-E][0-9A-E]$") + keyPrefix = os.Getenv("DPW_SSM_PREFIX") +) + +func listParams(params []string) { + info, err := store.GetInfo(svc) + if err != nil { + Logger.Fatal(err) + } + + for key, _ := range info.ByKey { + // Skip over things that aren't prefixed... + if keyPrefix != "" && !strings.HasPrefix(key, keyPrefix) { + continue + } + fmt.Println(strings.TrimPrefix(key, keyPrefix)) + } + + os.Exit(0) +} + +func insertParam(params []string) { + if len(params) != 1 { + Logger.Printf("Params provided: '%s'", params) + Logger.Fatal("Expected exactly one parameter, the path") + } + path := keyPrefix + params[0] + + err := store.InsertParam(svc, os.Stdin, path) + if err != nil { + Logger.Fatalf("While inserting: '%s': %s", path, err) + } + + os.Exit(0) +} + +func showParam(params []string) { + if len(params) != 1 { + Logger.Printf("Params provided: '%s'", params) + Logger.Fatal("Expected exactly one parameter, the path") + } + path := keyPrefix + params[0] + + err := store.GetParam(svc, os.Stdout, path) + if err != nil { + Logger.Fatalf("Encountered: %s\n", err) + } + + os.Exit(0) +} + +func removeParam(params []string) { + if len(params) != 1 { + Logger.Printf("Params provided: '%s'", params) + Logger.Fatal("Expected exactly one parameter, the path") + } + path := keyPrefix + params[0] + + err := store.RemoveParam(svc, path) + if err != nil { + Logger.Fatalf("Encountered: %s\n", err) + } + + os.Exit(0) +} + +func help() { + fmt.Printf(` +dpw-ssm: An AWS SSM backend for the dynamic password manager. +https://git.riedstra.dev/mitch/dpw/about/ + +This can be used directly, but for interactive use 'dpw' is encouraged. + +Available commands: + +list +insert +show +rm + +Debugging environment variables: + +DPW_SSM_DEBUG=YES # Enable extended logging + +Environment variables: + +DPW_SSM_PREFIX= +DPW_SSM_KMS_KEY_ID= # Optional +DPW_SSM_TAGS='{"json":"encoded","set":"of","key":"value","pairs":"..."}' + +version: %s +`, VersionString) + os.Exit(0) +} + +func setRegion() { + if os.Getenv("AWS_REGION") == "" { + // Default to us-east-2 + os.Setenv("AWS_REGION", "us-east-2") + + // But if a default is set, respect that, since the AWS SDK for Go + // doesn't, normally. + if os.Getenv("AWS_DEFAULT_REGION") != "" { + os.Setenv("AWS_REGION", os.Getenv("AWS_DEFAULT_REGION")) + } + } +} + +func main() { + if os.Getenv("DPW_SSM_DEBUG") != "" { + Logger = log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile) + } + + if os.Getenv("DPW_SSM_KMS_KEY_ID") != "" { + store.KMS_KEY_ID = aws.String(os.Getenv("DPW_SSM_KMS_KEY_ID")) + } + + ssm_tags_json := os.Getenv("DPW_SSM_TAGS") + if ssm_tags_json != "" { + tags := map[string]string{} + err := json.Unmarshal([]byte(ssm_tags_json), &tags) + if err != nil { + Logger.Println("Warning, failed to decode DPW_SSM_TAGS: %s\n", err) + } else { + for k, v := range tags { + store.Tags = append(store.Tags, &ssm.Tag{ + Key: aws.String(k), + Value: aws.String(v), + }) + } + } + } + + setRegion() + + ses := session.Must(session.NewSession()) + svc = ssm.New(ses) + + for n, arg := range os.Args[1:] { + switch arg { + case "list": + listParams(os.Args[n+2:]) + break + case "insert": + insertParam(os.Args[n+2:]) + break + case "show": + showParam(os.Args[n+2:]) + break + case "rm": + removeParam(os.Args[n+2:]) + break + case "init": + fmt.Fprintln(os.Stderr, "No init process is necessary") + break + default: + fmt.Fprintf(os.Stderr, "Unknown argument: '%s'\n", arg) + help() + } + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..112a970 --- /dev/null +++ b/readme.md @@ -0,0 +1,24 @@ +# dpw-ssm or a basic tool to make storing secrets in SSM a littl easier. + +This is fundamentally a plugin for https://git.riedstra.dev/mitch/dpw/about/ + +That being said, it can be used on its own. + +I've only spent a few hours on it, so expect an edge case or two, but for my +purposes of easily using the parameter store for things larger than 4K and small +binaries it works wonderfully. + +## Building + +``` +go build . +``` + +Or `sh ./build.sh` + + +## Configuration options: + +Pretty much all through environment variables, simply run `./dpw-ssm -h` +and it'll give you some info + diff --git a/store/store.go b/store/store.go new file mode 100644 index 0000000..fc42193 --- /dev/null +++ b/store/store.go @@ -0,0 +1,207 @@ +package store + +import ( + "bytes" + "encoding/base64" + "fmt" + "io" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" +) + +const SSM_MAX_SIZE = 4096 + +// ((16^4)*4096)/1024/1024 +// If we ever need more than 256 MB in parameter store, we've done something +// very wrong. +const SSM_KEY_FORMAT = "%s-%04X" // + +var ( + // TrimRegex is used to group the SSM keys inside of the Info struct under + // ByKey. This will only be used for params that exceed 4KB. + TrimRegex = regexp.MustCompile("-[0-9A-E][0-9A-E][0-9A-E][0-9A-E]$") + + // Optional, can be set set to utilize a specific KMS key if desired. + KMS_KEY_ID *string = nil + + Tags = []*ssm.Tag{} +) + +type Info struct { + ByKey map[string]*Entry + ByFullKey map[string]*Entry +} + +func (i *Info) init() { + if i.ByKey == nil { + i.ByKey = map[string]*Entry{} + } + if i.ByFullKey == nil { + i.ByFullKey = map[string]*Entry{} + } +} + +func (i *Info) add(e *ssm.ParameterMetadata) { + name := TrimRegex.ReplaceAllString(*e.Name, "") + var entry *Entry + + // Doesn't exist, make a new entry + if _, ok := i.ByKey[name]; !ok { + entry = &Entry{ + Name: name, + Keys: []*ssm.ParameterMetadata{e}, + } + + i.ByKey[name] = entry + i.ByFullKey[*e.Name] = entry + return + } + + // Otherwise let's just update the one that's there + entry = i.ByKey[name] + entry.Keys = append(entry.Keys, e) + i.ByFullKey[*e.Name] = entry +} + +type Entry struct { + Name string + Keys []*ssm.ParameterMetadata +} + +// GetInfo returns a populated Info struct from the SSM +func GetInfo(svc *ssm.SSM) (*Info, error) { + ret := &Info{ + ByKey: map[string]*Entry{}, + ByFullKey: map[string]*Entry{}, + } + + var out = &ssm.DescribeParametersOutput{} + var err error + for { + out, err = svc.DescribeParameters(&ssm.DescribeParametersInput{ + NextToken: out.NextToken, + }) + if err != nil { + return nil, err + } + for _, entry := range out.Parameters { + ret.add(entry) + } + if out.NextToken == nil { + break + } + } + + return ret, nil +} + +func InsertParam(svc *ssm.SSM, rdr io.Reader, key string) error { + buf := &bytes.Buffer{} + enc := base64.NewEncoder(base64.StdEncoding, buf) + + _, err := io.Copy(enc, rdr) + if err != nil { + return fmt.Errorf("Error while reading stdin: %w", err) + } + + err = enc.Close() + if err != nil { + return fmt.Errorf("While closing encoder: %w", err) + } + + n := 1 + for { + key := fmt.Sprintf(SSM_KEY_FORMAT, key, n) + n++ + + paramBuf := &bytes.Buffer{} + + n, err := io.CopyN(paramBuf, buf, SSM_MAX_SIZE) + if err != io.EOF && err != nil { + return fmt.Errorf("While writing output: %w", err) + } + // fmt.Fprintf(os.Stderr, "Wrote '%d' bytes to '%s'\n", n, key) + + _, err = svc.PutParameter(&ssm.PutParameterInput{ + KeyId: KMS_KEY_ID, + Name: aws.String(key), + Type: aws.String("SecureString"), + Value: aws.String(paramBuf.String()), + Tags: Tags, + }) + + if err != nil { + return fmt.Errorf("Failed putting prameter: %w", err) + } + + if err == io.EOF || n < SSM_MAX_SIZE { + break + } + } + + return nil +} + +func GetParam(svc *ssm.SSM, wrtr io.Writer, key string) error { + n := 1 + buf := &bytes.Buffer{} + + for { + key := fmt.Sprintf(SSM_KEY_FORMAT, key, n) + n++ + out, err := svc.GetParameter(&ssm.GetParameterInput{ + Name: aws.String(key), + WithDecryption: aws.Bool(true), + }) + if err != nil { + return fmt.Errorf("While fetching: '%s': %w", key, err) + } + + w, err := buf.WriteString(*out.Parameter.Value) + if err != nil { + return fmt.Errorf("While writing to buffer: %w", err) + } + + if w != SSM_MAX_SIZE { + break + } + } + + dec := base64.NewDecoder(base64.StdEncoding, buf) + _, err := io.Copy(wrtr, dec) + if err != nil { + return fmt.Errorf("Error writing output: %w", err) + } + + return nil +} + +func RemoveParam(svc *ssm.SSM, key string) error { + info, err := GetInfo(svc) + if err != nil { + return fmt.Errorf("When fetching info: %w", err) + } + + entry, ok := info.ByKey[key] + if !ok { + return fmt.Errorf("Entry '%s' not found in parameter store", key) + } + + var e error + for _, key := range entry.Keys { + _, err := svc.DeleteParameter(&ssm.DeleteParameterInput{ + Name: key.Name, + }) + if err != nil { + if e == nil { + e = err + } else { + e = fmt.Errorf("%s, %w", err, e) + } + } + } + + return e +} -- cgit v1.2.3