New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial implementation #1
Changes from 9 commits
93a8642
e32f8fd
06dee79
4774d2f
7e28504
b6d0288
dbb0801
957cb1a
f579d9c
c4dc96a
c1c9320
4a8bc2f
cc1f3d4
ea51558
96ccca6
ac0ea38
dc905c3
e1e88cd
af90c89
343f709
fef2575
8d57158
c99ad37
0e2f7e7
a760602
233ff52
8818ebd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/build/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
FROM golang:1.12-alpine | ||
ENV GO111MODULE=on | ||
WORKDIR /go/src/github.com/envato/ejsonkms | ||
COPY . . | ||
RUN apk add git gcc musl-dev | ||
RUN go get |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
NAME=ejsonkms | ||
PACKAGE=github.com/envato/ejsonkms | ||
VERSION=$(shell cat VERSION) | ||
GOFILES=$(shell find . -type f -name '*.go') | ||
|
||
.PHONY: default all binaries clean | ||
|
||
default: all | ||
all: binaries | ||
binaries: build/bin/linux-amd64 build/bin/darwin-amd64 | ||
|
||
build/bin/linux-amd64: $(GOFILES) | ||
mkdir -p "$(@D)" | ||
GOOS=linux GOARCH=amd64 go build \ | ||
-ldflags '-s -w -X main.version="$(VERSION)"' \ | ||
-o "$@" | ||
|
||
build/bin/darwin-amd64: $(GOFILES) | ||
GOOS=darwin GOARCH=amd64 go build \ | ||
-ldflags '-s -w -X main.version="$(VERSION)"' \ | ||
-o "$@" | ||
|
||
clean: | ||
rm -rf build |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,52 @@ | ||
# ejsonkms | ||
|
||
`ejsonkms` is a tool to based on the [ejson library](https://github.com/Shopify/ejson) with integration with AWS KMS. | ||
`ejsonkms` combines the [ejson library](https://github.com/Shopify/ejson) with [AWS Key Management | ||
Service](https://aws.amazon.com/kms/) to simplify deployments on AWS. The EJSON private key is encrypted with | ||
KMS and stored inside the EJSON file as `_private_key_enc`. Access to decrypt secrets can be controlled with IAM | ||
permissions on the KMS key. | ||
|
||
## Usage | ||
|
||
Generating an EJSON file: | ||
|
||
``` | ||
$ ejsonkms keygen --aws-region us-east-1 --kms-key-id bc436485-5092-42b8-92a3-0aa8b93536dc -o secrets.ejson | ||
Private Key: ae5969d1fb70faab76198ee554bf91d2fffc44d027ea3d804a7c7f92876d518b | ||
$ cat secrets.ejson | ||
{ | ||
"_public_key": "6b8280f86aff5f48773f63d60e655e2f3dd0dd7c14f5fecb5df22936e5a3be52", | ||
"_private_key_enc": "S2Fybjphd3M6a21zOnVzLWVhc3QtMToxMTExMjIyMjMzMzM6a2V5L2JjNDM2NDg1LTUwOTItNDJiOC05MmEzLTBhYThiOTM1MzZkYwAAAAAycRX5OBx6xGuYOPAmDJ1FombB1lFybMP42s7PGmoa24bAesPMMZtI9V0w0p0lEgLeeSvYdsPuoPROa4bwnQxJB28eC6fHgfWgY7jgDWY9uP/tgzuWL3zuIaq+9Q==" | ||
} | ||
``` | ||
|
||
Encrypting: | ||
|
||
``` | ||
$ ejsonkms encrypt secrets.ejson | ||
``` | ||
|
||
Decrypting: | ||
|
||
``` | ||
$ ejsonkms decrypt secrets.ejson | ||
{ | ||
"_public_key": "6b8280f86aff5f48773f63d60e655e2f3dd0dd7c14f5fecb5df22936e5a3be52", | ||
"_private_key_enc": "S2Fybjphd3M6a21zOnVzLWVhc3QtMToxMTExMjIyMjMzMzM6a2V5L2JjNDM2NDg1LTUwOTItNDJiOC05MmEzLTBhYThiOTM1MzZkYwAAAAAycRX5OBx6xGuYOPAmDJ1FombB1lFybMP42s7PGmoa24bAesPMMZtI9V0w0p0lEgLeeSvYdsPuoPROa4bwnQxJB28eC6fHgfWgY7jgDWY9uP/tgzuWL3zuIaq+9Q==", | ||
"environment": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It appears like the format is the same as the ejson2env tool, which has a top level What's the thinking behind using the top level IMHO, I don't think we need the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned in Slack, I though you wrote ejson2env 🤦♂ . And so i thought you could change the way it worked. But it makes sense that you can add top level key/value pairs but only the key/value pairs under |
||
"my_secret": "secret123" | ||
} | ||
} | ||
``` | ||
|
||
Exporting shell variables (from [ejson2env](https://github.com/Shopify/ejson2env)): | ||
|
||
``` | ||
$ exports=$(ejsonkms env secrets.ejson) | ||
$ echo $exports | ||
export my_secret=secret123 | ||
$ eval $exports | ||
$ echo my_secret | ||
secret123 | ||
``` | ||
|
||
Note that only secrets under the "environment" key will be exported using the `env` command. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❤️ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
0.1.0 |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,89 @@ | ||||||
package main | ||||||
|
||||||
import ( | ||||||
"fmt" | ||||||
"os" | ||||||
|
||||||
"github.com/Shopify/ejson" | ||||||
) | ||||||
|
||||||
func encryptAction(args []string) error { | ||||||
if len(args) < 1 { | ||||||
return fmt.Errorf("at least one file path must be given") | ||||||
} | ||||||
for _, filePath := range args { | ||||||
n, err := ejson.EncryptFileInPlace(filePath) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
fmt.Printf("Wrote %d bytes to %s.\n", n, filePath) | ||||||
} | ||||||
return nil | ||||||
} | ||||||
|
||||||
func decryptAction(args []string, awsRegion string, outFile string) error { | ||||||
if len(args) != 1 { | ||||||
return fmt.Errorf("exactly one file path must be given") | ||||||
} | ||||||
ejsonFilePath := args[0] | ||||||
|
||||||
decrypted, err := Decrypt(ejsonFilePath, awsRegion) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
target := os.Stdout | ||||||
if outFile != "" { | ||||||
target, err = os.Create(outFile) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
defer func() { _ = target.Close() }() | ||||||
stevehodgkiss marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
|
||||||
_, err = target.Write(decrypted) | ||||||
return err | ||||||
} | ||||||
|
||||||
func keygenAction(args []string, kmsKeyID string, awsRegion string, outFile string) error { | ||||||
pub, priv, privKeyEnc, err := Keygen(kmsKeyID, awsRegion) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
fmt.Printf("Private Key: %s\n", priv) | ||||||
stevehodgkiss marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
target := os.Stdout | ||||||
if outFile != "" { | ||||||
target, err = os.Create(outFile) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
defer func() { _ = target.Close() }() | ||||||
} else { | ||||||
fmt.Printf("EJSON File:\n") | ||||||
stevehodgkiss marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
|
||||||
_, err = fmt.Fprintf(target, "{\n \"_public_key\": \"%s\",\n \"_private_key_enc\": \"%s\"\n}", pub, privKeyEnc) | ||||||
stevehodgkiss marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
return nil | ||||||
} | ||||||
|
||||||
func envAction(ejsonFilePath string, quiet bool, awsRegion string) error { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More of a personal thing but you can also combine the type definitions to save some duplication if you don't care about re-ordering the parameters.
Suggested change
|
||||||
exportFunc := ExportEnv | ||||||
if quiet { | ||||||
exportFunc = ExportQuiet | ||||||
} | ||||||
privateKeyEnc, err := findPrivateKeyEnc(ejsonFilePath) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
kmsDecryptedPrivateKey, err := decryptPrivateKeyWithKMS(privateKeyEnc, awsRegion) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
return ExportSecrets(ejsonFilePath, kmsDecryptedPrivateKey, exportFunc) | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"os" | ||
"testing" | ||
|
||
. "github.com/smartystreets/goconvey/convey" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not something I've used but looks good 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inherited from ejson's codebase - https://github.com/Shopify/ejson/blob/master/ejson_test.go#L10 |
||
) | ||
|
||
func TestEnv(t *testing.T) { | ||
outputBuffer := new(bytes.Buffer) | ||
output = outputBuffer | ||
|
||
// ensure that output returns to os.Stdout | ||
defer func() { | ||
output = os.Stdout | ||
}() | ||
|
||
Convey("Env", t, func() { | ||
err := envAction("testdata/test.ejson", false, "us-east-1") | ||
|
||
Convey("should return decrypted values as shell exports", func() { | ||
So(err, ShouldBeNil) | ||
actualOutput := outputBuffer.String() | ||
So(actualOutput, ShouldContainSubstring, "export my_secret=secret123") | ||
}) | ||
}) | ||
|
||
Convey("Env with no private key", t, func() { | ||
err := envAction("testdata/test_no_private_key.ejson", false, "us-east-1") | ||
|
||
Convey("should fail", func() { | ||
So(err, ShouldNotBeNil) | ||
So(err.Error(), ShouldContainSubstring, "Missing _private_key_enc") | ||
}) | ||
}) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can achieve similar functionality with Example Tests which check the standard out is what is expected. But this works too |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
version: "3" | ||
services: | ||
awskms: | ||
image: "nsmithuk/local-kms" | ||
environment: | ||
REGION: us-east-1 | ||
volumes: | ||
- "./local_kms:/init" | ||
expose: | ||
- 8080 | ||
tests: | ||
build: . | ||
volumes: | ||
- "./:/go/src/github.com/envato/ejsonkms" | ||
command: ["go", "test"] | ||
environment: | ||
AWS_ACCESS_KEY_ID: '123' | ||
AWS_SECRET_ACCESS_KEY: xyz | ||
FAKE_AWSKMS_URL: http://awskms:8080 | ||
links: | ||
- awskms |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,78 @@ | ||||||||||
package main | ||||||||||
|
||||||||||
import ( | ||||||||||
"encoding/json" | ||||||||||
"errors" | ||||||||||
"io/ioutil" | ||||||||||
"os" | ||||||||||
|
||||||||||
"github.com/Shopify/ejson" | ||||||||||
) | ||||||||||
|
||||||||||
// Keygen generates keys and prepares an EJSON file with them | ||||||||||
func Keygen(kmsKeyID string, awsRegion string) (string, string, string, error) { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can combine the type definitions here
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something else to note here is that I will generally look at lots of return values like this ( This isn't a big deal as this will work, it's just not using the type system to the fullest. |
||||||||||
pub, priv, err := ejson.GenerateKeypair() | ||||||||||
if err != nil { | ||||||||||
return "", "", "", err | ||||||||||
} | ||||||||||
|
||||||||||
privKeyEnc, err := encryptPrivateKeyWithKMS(priv, kmsKeyID, awsRegion) | ||||||||||
if err != nil { | ||||||||||
return "", "", "", err | ||||||||||
} | ||||||||||
|
||||||||||
return pub, priv, privKeyEnc, nil | ||||||||||
} | ||||||||||
|
||||||||||
// Decrypt decrypts an EJSON file | ||||||||||
func Decrypt(ejsonFilePath string, awsRegion string) ([]byte, error) { | ||||||||||
privateKeyEnc, err := findPrivateKeyEnc(ejsonFilePath) | ||||||||||
if err != nil { | ||||||||||
return nil, err | ||||||||||
} | ||||||||||
|
||||||||||
kmsDecryptedPrivateKey, err := decryptPrivateKeyWithKMS(privateKeyEnc, awsRegion) | ||||||||||
if err != nil { | ||||||||||
return nil, err | ||||||||||
} | ||||||||||
|
||||||||||
decrypted, err := ejson.DecryptFile(ejsonFilePath, "", kmsDecryptedPrivateKey) | ||||||||||
if err != nil { | ||||||||||
return nil, err | ||||||||||
} | ||||||||||
|
||||||||||
return decrypted, nil | ||||||||||
} | ||||||||||
|
||||||||||
func findPrivateKeyEnc(ejsonFilePath string) (key string, err error) { | ||||||||||
var ( | ||||||||||
obj map[string]interface{} | ||||||||||
ks string | ||||||||||
) | ||||||||||
|
||||||||||
file, err := os.Open(ejsonFilePath) | ||||||||||
if err != nil { | ||||||||||
return "", err | ||||||||||
} | ||||||||||
defer file.Close() | ||||||||||
|
||||||||||
data, err := ioutil.ReadAll(file) | ||||||||||
if err != nil { | ||||||||||
return "", err | ||||||||||
} | ||||||||||
|
||||||||||
err = json.Unmarshal(data, &obj) | ||||||||||
if err != nil { | ||||||||||
return "", err | ||||||||||
} | ||||||||||
|
||||||||||
k, ok := obj["_private_key_enc"] | ||||||||||
if !ok { | ||||||||||
return "", errors.New("Missing _private_key_enc field") | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. errors in Go generally start lowercased (which ends up matching stack traces, etc. nicely)
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check can be avoided by making type ejsonFile {
encryptedPrivateKey string `json:"_private_key_enc"`
} Then when you unmarshal, it will throw an error if that field is not present. |
||||||||||
} | ||||||||||
ks, ok = k.(string) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, I would use the shorthand version of the Go error handling seeing how the line is quite short.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above, json unmarshal will do the type coercion for you |
||||||||||
if !ok { | ||||||||||
return "", errors.New("Couldn't cast _private_key_enc to string") | ||||||||||
} | ||||||||||
return ks, nil | ||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package main | ||
|
||
import ( | ||
"testing" | ||
|
||
. "github.com/smartystreets/goconvey/convey" | ||
) | ||
|
||
func TestKeygen(t *testing.T) { | ||
Convey("Keygen", t, func() { | ||
pub, priv, privEnc, err := Keygen("bc436485-5092-42b8-92a3-0aa8b93536dc", "us-east-1") | ||
Convey("should return three strings that look key-like", func() { | ||
So(err, ShouldBeNil) | ||
So(pub, ShouldNotEqual, priv) | ||
So(pub, ShouldNotContainSubstring, "00000") | ||
So(priv, ShouldNotContainSubstring, "00000") | ||
So(privEnc, ShouldNotContainSubstring, "00000") | ||
}) | ||
}) | ||
} | ||
|
||
func TestDecrypt(t *testing.T) { | ||
Convey("Decrypt", t, func() { | ||
decrypted, err := Decrypt("testdata/test.ejson", "us-east-1") | ||
Convey("should return decrypted values", func() { | ||
So(err, ShouldBeNil) | ||
json := string(decrypted[:]) | ||
So(json, ShouldContainSubstring, "\"my_secret\": \"secret123\"") | ||
stevehodgkiss marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}) | ||
}) | ||
Convey("Decrypt with no private key", t, func() { | ||
_, err := Decrypt("testdata/test_no_private_key.ejson", "us-east-1") | ||
Convey("should fail", func() { | ||
So(err, ShouldNotBeNil) | ||
So(err.Error(), ShouldContainSubstring, "Missing _private_key_enc") | ||
}) | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is also
mitchellh/gox
if you don't want to roll this by hand or start to use cross platform libraries.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approach here is basically a bare bones version of ejson's Makefile. Will check out gox.