Skip to content
This repository has been archived by the owner on Jun 11, 2019. It is now read-only.

Commit

Permalink
Merge pull request #19 from meltwater/exec_command
Browse files Browse the repository at this point in the history
Add exec command
  • Loading branch information
jcsorvasi committed Oct 15, 2018
2 parents b1bcdde + 495365c commit 5134958
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 77 deletions.
27 changes: 7 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ variables:
override:
env:
SERVICE_PUBLIC_KEY: "%{secretary.service.publickey}"
SERVICE_PRIVATE_KEY: /service/keys/service-private-key.pem
container:
volumes:
- containerPath: "/service/keys"
Expand All @@ -212,6 +213,7 @@ An runtime config automatically expanded by Lighter might look like
"DEPLOY_PUBLIC_KEY": "0k+v11LV3SOr+XiFJ/ug0KcPPhwkXnVirmO65nAd1LI=",
"DEPLOY_PRIVATE_KEY": "rEmz7Rt6tUnlC4TKYeNzePYg+p1ePAw4BAtfJAY4zzs=",
"SERVICE_PUBLIC_KEY": "/1fbWGMTaR+lLQJnEsmxdfwWybKOpPQpyWB3FpNmOF4=",
"SERVICE_PRIVATE_KEY": "/service/keys/service-private-key.pem"
"DATABASE_USERNAME": "myservice",
"DATABASE_PASSWORD": "ENC[NACL,SLXf+O9iG48uyojT0Zg30Q8/uRV8DizuDWMWtgL5PmTU54jxp5cTGrYeLpd86rA=]",
"DATABASE_URL": "jdbc:mysql://hostname:3306/schema"
Expand All @@ -222,34 +224,19 @@ An runtime config automatically expanded by Lighter might look like
```

## Container Startup Sequence
Docker images should embed the `secretary` executable. Call it at container startup to decrypt
environment variables, before starting the actual service.
Docker images should embed the `secretary` executable. Use it as an entrypoint
with `secretary exec --". It will then decrypt all command line arguments and
environment variables.

*Dockerfile*
```
# Install secretary
ENV SECRETARY_VERSION x.y.z
RUN curl -fsSLo /usr/bin/secretary "https://github.com/meltwater/secretary/releases/download/${SECRETARY_VERSION}/secretary-`uname -s`-`uname -m`" && \
chmod +x /usr/bin/secretary
```

Container startup examples
```
#!/bin/sh
set -e
# Decrypt secrets
if [ "$SERVICE_PUBLIC_KEY" != "" ]; then
SECRETS=$(secretary decrypt -e --service-key=/service/keys/service-private-key.pem)
else
SECRETS=$(secretary decrypt -e)
fi
eval "$SECRETS"
unset SECRETS
# Start the service
exec ...
ENTRYPOINT ["/usr/bin/secretary", "exec", "--"]
CMD ["/bin/echo", "ENC[NACL,XXXXXX==]"] # This can be replaced in Dockerfiles that inherit from this one
```

The complete decryption sequence could be described as
Expand Down
37 changes: 32 additions & 5 deletions box.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,21 @@ func findKey(locations ...string) *[32]byte {
continue
}

encoded := os.Getenv(location)
if encoded != "" {
key, err := pemDecode(encoded)
// First try to interpret the argument as an environment variable
env := os.Getenv(location)
if env != "" {
// If the environment variable is the path to an existing file, use that
if _, err := os.Stat(env); err == nil {
return pemRead(env)
}

// Otherwise, try to decode the key as a pem-formatted string
key, err := pemDecode(env)
check(err, "Failed to decode key in $%s", location)
return key
}

// If there is no such environment variable, check whether it is a path to a file
if _, err := os.Stat(location); err == nil {
return pemRead(location)
}
Expand Down Expand Up @@ -150,8 +158,27 @@ func genkey(publicKeyFile string, privateKeyFile string) {
pemWrite(privateKey, privateKeyFile, "NACL PRIVATE KEY", 0600)
}

func extractEnvelopes(payload string) []string {
return envelopeRegexp.FindAllString(payload, -1)
func decryptEnvelopes(input string, crypto DecryptionStrategy) (output string, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
err, ok = r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
}
}()

repl := func(envelope string) string {
bytes, err := crypto.Decrypt(stripWhitespace(envelope))
if err != nil {
panic(err)
}
return string(bytes)
}

output = envelopeRegexp.ReplaceAllStringFunc(input, repl)
return
}

func extractEnvelopeType(envelope string) string {
Expand Down
81 changes: 56 additions & 25 deletions box_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (

"github.com/stretchr/testify/assert"
"golang.org/x/crypto/nacl/box"
"io/ioutil"
"os"
)

const privateKey = `Q1PuWtB1E7F1sLpvfBGjL+ZuH+fSCOvMDqTyRQE4GTg=`
Expand Down Expand Up @@ -47,30 +49,17 @@ func TestFindKey(t *testing.T) {
assert.Nil(t, findKey("", "RANDOM_ENVVAR_THAT_DOESNT_EXIST", "./resources/test/keys/nonexist-public-key.pem"))
}

func TestExtractEnvelopes(t *testing.T) {
envelopes := extractEnvelopes("amqp://ENC[NACL,uSr123+/=]:ENC[NACL,pWd123+/=]@rabbit:5672/")
assert.Equal(t, 2, len(envelopes))
assert.Equal(t, []string{"ENC[NACL,uSr123+/=]", "ENC[NACL,pWd123+/=]"}, envelopes)

envelopes = extractEnvelopes("amqp://ENC[NACL,uSr123+/=]:ENC[NACL,pWd123+/=]@rabbit:5672/ENC[NACL,def123+/=]")
assert.Equal(t, 3, len(envelopes))
assert.Equal(t, []string{"ENC[NACL,uSr123+/=]", "ENC[NACL,pWd123+/=]", "ENC[NACL,def123+/=]"}, envelopes)

envelopes = extractEnvelopes("amqp://ENC[NACL,]:ENC[NACL,pWd123+/=]@rabbit:5672/")
assert.Equal(t, 1, len(envelopes))
assert.Equal(t, []string{"ENC[NACL,pWd123+/=]"}, envelopes)

envelopes = extractEnvelopes("amqp://ENC[NACL,:ENC[NACL,pWd123+/=]@rabbit:5672/")
assert.Equal(t, 1, len(envelopes))
assert.Equal(t, []string{"ENC[NACL,pWd123+/=]"}, envelopes)

envelopes = extractEnvelopes("amqp://NC[NACL,]:ENC[NACL,pWd123+/=]@rabbit:5672/")
assert.Equal(t, 1, len(envelopes))
assert.Equal(t, []string{"ENC[NACL,pWd123+/=]"}, envelopes)
func TestFindKeyEnvironmentPem(t *testing.T) {
bytes, _ := ioutil.ReadFile("./resources/test/keys/config-public-key.pem")
os.Setenv("PUBLIC_KEY", string(bytes))
expected, _ := pemDecode(string(bytes))
assert.Equal(t, expected, findKey("PUBLIC_KEY"))
}

envelopes = extractEnvelopes("amqp://ENC[NACL,abc:ENC[NACL,pWd123+/=]@rabbit:5672/")
assert.Equal(t, 1, len(envelopes))
assert.Equal(t, []string{"ENC[NACL,pWd123+/=]"}, envelopes)
func TestFindKeyEnvironmentPath(t *testing.T) {
os.Setenv("PUBLIC_KEY", "./resources/test/keys/config-public-key.pem")
expected := pemRead("./resources/test/keys/config-public-key.pem")
assert.Equal(t, expected, findKey("PUBLIC_KEY"))
}

func TestExtractEnvelopeType(t *testing.T) {
Expand Down Expand Up @@ -134,8 +123,50 @@ func TestEncryptEnvelope(t *testing.T) {
assert.Equal(t, "secret", string(plaintext), "Should decrypt plaintext")
}

func BenchmarkExtractEnvelopes(b *testing.B) {
type noopDecryptionStrategyType struct{}

func (noopDecryptionStrategyType) Decrypt(envelope string) ([]byte, error) {
return []byte(envelope), nil
}

var NoopDecryptionStrategy DecryptionStrategy = noopDecryptionStrategyType{}

func BenchmarkDecryptEnvelopes(b *testing.B) {
for n := 0; n < b.N; n++ {
extractEnvelopes("amqp://ENC[NACL,uSr123+/=]:ENC[NACL,pWd123+/=]@rabbit:5672/")
decryptEnvelopes("amqp://ENC[NACL,uSr123+/=]:ENC[NACL,pWd123+/=]@rabbit:5672/", NoopDecryptionStrategy)
}
}

func TestDecryptEnvelopes(t *testing.T) {
privkey, err := pemDecode(privateKey)
assert.Nil(t, err)

pubkey, err := pemDecode(publicKey)
assert.Nil(t, err)

envelope, err := encryptEnvelope(pubkey, privkey, []byte("secret"))
crypto := newKeyDecryptionStrategy(pubkey, privkey)

result, err := decryptEnvelopes("This is a "+envelope+" message", crypto)
assert.Nil(t, err)
assert.Equal(t, "This is a secret message", result)
}

func TestDecryptEnvelopesStripsWhitespace(t *testing.T) {
privkey, err := pemDecode(privateKey)
assert.Nil(t, err)

pubkey, err := pemDecode(publicKey)
assert.Nil(t, err)

crypto := newKeyDecryptionStrategy(pubkey, privkey)

envelope, err := encryptEnvelope(pubkey, privkey, []byte("secret"))

// Add some whitespace to the middle of the envelope
envelope = envelope[0:len(envelope)/2] + " \t \n \t " + envelope[len(envelope)/2:len(envelope)]

result, err := decryptEnvelopes("This is a "+envelope+" message", crypto)
assert.Nil(t, err, "%v", err)
assert.Equal(t, "This is a secret message", result)
}
65 changes: 38 additions & 27 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"io/ioutil"
"os"
"path"
"regexp"
"strings"
)
Expand Down Expand Up @@ -35,18 +36,8 @@ func encryptCommand(input io.Reader, output io.Writer, crypto EncryptionStrategy
func decryptStream(input io.Reader, output io.Writer, crypto DecryptionStrategy) {
payload, err := ioutil.ReadAll(input)
check(err, "Failed to read encrypted data from standard input")
result := string(payload)

envelopes := extractEnvelopes(string(payload))
if len(envelopes) > 0 {
for _, envelope := range envelopes {
plaintext, err := crypto.Decrypt(stripWhitespace(envelope))
check(err)

result = strings.Replace(result, envelope, string(plaintext), 1)
}
}

result, err := decryptEnvelopes(string(payload), crypto)
check(err, "Failed to decrypt from standard input")
output.Write([]byte(result))
}

Expand All @@ -59,30 +50,50 @@ func decryptEnvironment(input []string, output io.Writer, crypto DecryptionStrat
keyval := strings.SplitN(item, "=", 2)
key, value := keyval[0], keyval[1]
result := value

envelopes := extractEnvelopes(value)
if len(envelopes) > 0 {
decryptedResult, suberr := decryptEnvelopes(result, crypto)
if suberr != nil {
ok = false
err = suberr
fmt.Fprintf(os.Stderr, "%s: %s\n", key, err)
continue
}
if decryptedResult != result {
if !shellIdentifierRegexp.Match([]byte(key)) {
ok = false
err = fmt.Errorf("the env var '%s' is not a valid shell script identifier. Only alphanumeric characters and underscores are supported, starting with an alphabetic or underscore character", key)
fmt.Fprintf(os.Stderr, "%s: %s\n", key, err)
}
fmt.Fprintf(output, "export %s='%s'\n", key, decryptedResult)
}
}

for _, envelope := range envelopes {
plaintext, suberr := crypto.Decrypt(stripWhitespace(envelope))
if suberr != nil {
ok = false
err = suberr
fmt.Fprintf(os.Stderr, "%s: %s\n", key, err)
continue
}
return ok, err
}

result = strings.Replace(result, envelope, string(plaintext), 1)
}
func createExecArgs(args []string, encryptedEnviron []string, crypto DecryptionStrategy) (cmd string, decryptedArgs []string, decryptedEnviron []string, err error) {

cmd = args[0]
decryptedArgs = make([]string, len(args))

fmt.Fprintf(output, "export %s='%s'\n", key, result)
decryptedArgs[0] = path.Base(cmd) // By unix convention argv[0] has to be set to basename of command
for i, arg := range args[1:] {
decryptedArg, subErr := decryptEnvelopes(arg, crypto)
if subErr != nil {
err = fmt.Errorf("Error while decrypting argument: %v", subErr)
}

decryptedArgs[i+1] = decryptedArg
}

return ok, err
decryptedEnviron = make([]string, len(encryptedEnviron))
for i, env := range encryptedEnviron {
decryptedEnv, subErr := decryptEnvelopes(env, crypto)
if subErr != nil {
err = fmt.Errorf("Error while decrypting environment variables: %v", subErr)
}

decryptedEnviron[i] = decryptedEnv
}

return
}
82 changes: 82 additions & 0 deletions commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,85 @@ func TestDecryptEnvironmentCommandSubstringsSpaces(t *testing.T) {

assert.Equal(t, "export b='blabla secretb la bla secret2'\n", output.String())
}

func TestCreateExecArgs(t *testing.T) {
configPublicKey := pemRead("./resources/test/keys/config-public-key.pem")
configPrivateKey := pemRead("./resources/test/keys/config-private-key.pem")
masterPublicKey := pemRead("./resources/test/keys/master-public-key.pem")
masterPrivateKey := pemRead("./resources/test/keys/master-private-key.pem")

encrypted, err := encryptEnvelope(masterPublicKey, configPrivateKey, []byte("Mellon"))
assert.Nil(t, err)

encrypted2, err := encryptEnvelope(masterPublicKey, configPrivateKey, []byte("hunter2"))
assert.Nil(t, err)

crypto := newKeyDecryptionStrategy(configPublicKey, masterPrivateKey)

cmd, argv, environ, err := createExecArgs(
[]string{"/bin/echo", encrypted},
[]string{"SECRET=" + encrypted2},
crypto,
)
assert.Nil(t, err)
assert.Equal(t, cmd, "/bin/echo")
assert.Equal(t, []string{"echo", "Mellon"}, argv)
assert.Equal(t, []string{"SECRET=hunter2"}, environ)
}

func TestCreateExecArgsInvalidEnvelope(t *testing.T) {
configPublicKey := pemRead("./resources/test/keys/config-public-key.pem")
// configPrivateKey := pemRead("./resources/test/keys/config-private-key.pem")
// masterPublicKey := pemRead("./resources/test/keys/master-public-key.pem")
masterPrivateKey := pemRead("./resources/test/keys/master-private-key.pem")

crypto := newKeyDecryptionStrategy(configPublicKey, masterPrivateKey)

_, _, _, err := createExecArgs(
[]string{"/bin/echo", "ENC[NACL,invalidenvelope]"},
[]string{},
crypto,
)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "Error while decrypting argument")
}

func TestCreateExecArgsInvalidEnvelopeInEnvironment(t *testing.T) {
configPublicKey := pemRead("./resources/test/keys/config-public-key.pem")
// configPrivateKey := pemRead("./resources/test/keys/config-private-key.pem")
// masterPublicKey := pemRead("./resources/test/keys/master-public-key.pem")
masterPrivateKey := pemRead("./resources/test/keys/master-private-key.pem")

crypto := newKeyDecryptionStrategy(configPublicKey, masterPrivateKey)

_, _, _, err := createExecArgs(
[]string{"/bin/echo"},
[]string{"ENC[NACL,invalidenvelope]"},
crypto,
)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "Error while decrypting environment")
}

func TestCreateExecArgsInvalidDecryptionKey(t *testing.T) {
configPublicKey := pemRead("./resources/test/keys/config-public-key.pem")
configPrivateKey := pemRead("./resources/test/keys/config-private-key.pem")
masterPublicKey := pemRead("./resources/test/keys/master-public-key.pem")
//masterPrivateKey := pemRead("./resources/test/keys/master-private-key.pem")

encrypted, err := encryptEnvelope(masterPublicKey, configPrivateKey, []byte("Mellon"))
assert.Nil(t, err)

// NB: Erroneously using configPrivateKey here
crypto := newKeyDecryptionStrategy(configPublicKey, configPrivateKey)

_, _, _, err = createExecArgs(
[]string{"/bin/echo", encrypted},
[]string{},
crypto,
)

assert.NotNil(t, err)
assert.Contains(t, err.Error(), "Error while decrypting argument")
assert.Contains(t, err.Error(), "incorrect keys?")
}
Loading

0 comments on commit 5134958

Please sign in to comment.