Skip to content

Commit 7ef3f9e

Browse files
committed
Merge pull request #18 from gene1wood/add-encryption-context
Adding support for KMS "encryption context"
2 parents 1881012 + 71caa0e commit 7ef3f9e

File tree

3 files changed

+122
-45
lines changed

3 files changed

+122
-45
lines changed

README.md

+54-25
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Whenever you want to store/share a credential, such as a database password, you
2121

2222
When you want to fetch the credential, for example as part of the bootstrap process on your web-server, you simply do `credstash get [credential-name]`. For example, `export DB_PASSWORD=$(credstash get myapp.db.prod)`. When you run `get`, credstash will go and fetch the encrypted credential and the wrapped encryption key from the credential store (DynamoDB). It will then send the wrapped encryption key to KMS, where it is decrypted with the master key. credstash then uses the decrypted data encryption key to decrypt the credential. The credential is printed to `stdout`, so you can use it in scripts or assign environment variables to it.
2323

24+
Optionally you can include any number of [Encryption Context](http://docs.aws.amazon.com/kms/latest/developerguide/encrypt-context.html) key value pairs to associate with the credential. The exact set of encryption context key value pairs that were associated with the credential when it was `put` in DynamoDB must be provided in the `get` request to successfully decrypt the credential. These encryption context key value pairs are useful to provide auditing context to the encryption and decryption operations in your CloudTrail logs. They are also useful for constraining access to a given credstash stored credential by using KMS Key Policy conditions and KMS Grant conditions. Doing so allows you to, for example, make sure that your database servers and web-servers can read the web-server DB user password but your database servers can not read your web-servers TLS/SSL certificate's private key. A `put` request with encryption context would look like `credstash put myapp.db.prod supersecretpassword1234 app.tier=db environment=prod`. In order for your web-servers to read that same credential they would execute a `get` call like `export DB_PASSWORD=$(credstash get myapp.db.prod environment=prod app.tier=db)`
25+
2426
Credentials stored in the credential-store are versioned and immutable. That is, if you `put` a credential called `foo` with a version of `1` and a value of `bar`, then foo version 1 will always have a value of bar, and there is no way in `credstash` to change its value (although you could go fiddle with the bits in DDB, but you shouldn't do that). Credential rotation is handed through versions. Suppose you do `credstash put foo bar`, and then decide later to rotate `foo`, you can put version 2 of `foo` by doing `credstash put foo baz -v `. The next time you do `credstash get foo`, it will return `baz`. You can get specific credential versions as well (with the same `-v` flag). You can fetch a list of all credentials in the credential-store and their versions with the `list` command.
2527

2628
## Dependencies
@@ -64,43 +66,70 @@ Once credentials are in place, run `credstash setup`. This will create the DDB t
6466

6567
## Usage
6668
```
67-
usage: credstash [-h] [-i INFILE] [-k KEY] [-n] [-r REGION] [-t TABLE]
68-
[-v VERSION]
69-
{delete,get,list,put,setup} [credential] [value]
69+
usage: credstash [-h] [-r REGION] [-t TABLE] {delete,get,list,put,setup} ...
7070
7171
A credential/secret storage system
7272
73-
positional arguments:
74-
{delete,get,list,put,setup}
75-
Put, Get, or Delete a credential from the store, list
76-
credentials and their versions, or setup the
77-
credential store
78-
credential the name of the credential to store/get
79-
value the value of the credential to put (ignored if action
80-
is 'get')
73+
delete
74+
usage: credstash delete [-h] [-r REGION] [-t TABLE] credential
75+
76+
positional arguments:
77+
credential the name of the credential to delete
78+
79+
get
80+
usage: credstash get [-h] [-r REGION] [-t TABLE] [-k KEY] [-n] [-v VERSION]
81+
credential [context [context ...]]
82+
83+
positional arguments:
84+
credential the name of the credential to get
85+
context encryption context key/value pairs associated with the
86+
credential in the form of "key=value"
87+
88+
optional arguments:
89+
-k KEY, --key KEY the KMS key-id of the master key to use. See the
90+
README for more information. Defaults to
91+
alias/credstash
92+
-n, --noline Don't append newline to returned value (useful in
93+
scripts or with binary files)
94+
-v VERSION, --version VERSION
95+
Get a specific version of the credential (defaults to
96+
the latest version).
97+
98+
list
99+
usage: credstash list [-h] [-r REGION] [-t TABLE]
100+
101+
put
102+
usage: credstash put [-h] [-r REGION] [-t TABLE] [-i INFILE] [-k KEY] [-v VERSION]
103+
credential value [context [context ...]]
104+
105+
positional arguments:
106+
credential the name of the credential to store
107+
value the value of the credential to store
108+
context encryption context key/value pairs associated with the
109+
credential in the form of "key=value"
110+
111+
optional arguments:
112+
-i INFILE, --infile INFILE
113+
store the contents of `infile` rather than provide a
114+
value on the command line
115+
-k KEY, --key KEY the KMS key-id of the master key to use. See the
116+
README for more information. Defaults to
117+
alias/credstash
118+
-v VERSION, --version VERSION
119+
Put a specific version of the credential (update the
120+
credential; defaults to version `1`).
121+
122+
setup
123+
usage: credstash setup [-h] [-r REGION] [-t TABLE]
81124
82125
optional arguments:
83-
-h, --help show this help message and exit
84-
-i INFILE, --infile INFILE
85-
store the contents of `infile` rather than provide a
86-
value on the command line
87-
-k KEY, --key KEY the KMS key-id of the master key to use. See the
88-
README for more information. Defaults to
89-
alias/credstash
90-
-n, --noline Don't append newline to returned value (useful in
91-
scripts or with binary files)
92126
-r REGION, --region REGION
93127
the AWS region in which to operate. If a region is not
94128
specified, credstash will use the value of the
95129
AWS_DEFAULT_REGION env variable, or if that is not
96130
set, us-east-1
97131
-t TABLE, --table TABLE
98132
DynamoDB table to use for credential storage
99-
-v VERSION, --version VERSION
100-
If doing a `put`, put a specific version of the
101-
credential (update the credential; defaults to version
102-
`1`). If doing a `get`, get a specific version of the
103-
credential (defaults to the latest version).
104133
```
105134

106135
## Security Notes

credstash.py

+67-19
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import boto.kms
1919
import operator
2020
import os
21+
import os.path
2122
import sys
2223
import time
2324

@@ -45,11 +46,23 @@ def __init__(self, value=""):
4546
def __str__(self):
4647
return self.value
4748

49+
class KeyValueToDictionary(argparse.Action):
50+
def __call__(self, parser, namespace, values, option_string=None):
51+
setattr(namespace,
52+
self.dest,
53+
{x[0] : x[1] for x in values} if len(values) > 0 else None)
54+
4855

4956
def printStdErr(s):
5057
sys.stderr.write(str(s))
5158
sys.stderr.write("\n")
5259

60+
def is_key_value_pair(string):
61+
output = string.split('=')
62+
if len(output) != 2:
63+
msg = "%r is not the form of \"key=value\"" % string
64+
raise argparse.ArgumentTypeError(msg)
65+
return output
5366

5467
def listSecrets(region="us-east-1", table="credential-store"):
5568
'''
@@ -59,14 +72,14 @@ def listSecrets(region="us-east-1", table="credential-store"):
5972
rs = secretStore.scan(attributes=("name", "version"))
6073
return [secret for secret in rs]
6174

62-
def putSecret(name, secret, version, kms_key="alias/credstash", region="us-east-1", table="credential-store"):
75+
def putSecret(name, secret, version, kms_key="alias/credstash", region="us-east-1", table="credential-store", context=None):
6376
'''
6477
put a secret called `name` into the secret-store, protected by the key kms_key
6578
'''
6679
kms = boto.kms.connect_to_region(region)
6780
# generate a a 64 byte key. Half will be for data encryption, the other half for HMAC
6881
try:
69-
kms_response = kms.generate_data_key(kms_key, number_of_bytes=64)
82+
kms_response = kms.generate_data_key(kms_key, context, 64)
7083
except:
7184
raise KmsError("Could not generate key using KMS key %s" % kms_key)
7285
data_key = kms_response['Plaintext'][:32]
@@ -91,7 +104,7 @@ def putSecret(name, secret, version, kms_key="alias/credstash", region="us-east-
91104
data['hmac'] = b64hmac
92105
return secretStore.put_item(data=data)
93106

94-
def getSecret(name, version="", region="us-east-1", table="credential-store"):
107+
def getSecret(name, version="", region="us-east-1", table="credential-store", context=None):
95108
'''
96109
fetch and decrypt the secret called `name`
97110
'''
@@ -108,9 +121,19 @@ def getSecret(name, version="", region="us-east-1", table="credential-store"):
108121
kms = boto.kms.connect_to_region(region)
109122
# Check the HMAC before we decrypt to verify ciphertext integrity
110123
try:
111-
kms_response = kms.decrypt(b64decode(material['key']))
112-
except:
113-
raise KmsError("Could not decrypt hmac key with KMS")
124+
kms_response = kms.decrypt(b64decode(material['key']), context)
125+
except boto.kms.exceptions.InvalidCiphertextException:
126+
if context is None:
127+
msg = ("Could not decrypt hmac key with KMS. The credential may "
128+
"require that an encryption context be provided to decrypt "
129+
"it.")
130+
else:
131+
msg = ("Could not decrypt hmac key with KMS. The encryption "
132+
"context provided may not match the one used when the "
133+
"credential was stored.")
134+
raise KmsError(msg)
135+
except Exception as e:
136+
raise KmsError("Decryption error %s" % e)
114137
key = kms_response['Plaintext'][:32]
115138
hmac_key = kms_response['Plaintext'][32:]
116139
hmac = HMAC(hmac_key, msg=b64decode(material['contents']), digestmod=SHA256)
@@ -153,21 +176,46 @@ def createDdbTable(region="us-east-1", table="credential-store"):
153176

154177

155178
def main():
156-
parser = argparse.ArgumentParser(description="A credential/secret storage system")
179+
parsers = {}
180+
parsers['super'] = argparse.ArgumentParser(description="A credential/secret storage system")
157181

158-
parser.add_argument("action", type=str, choices=["delete", "get", "list", "put", "setup"], help="Put, Get, or Delete a credential from the store, list credentials and their versions, or setup the credential store")
159-
parser.add_argument("credential", type=str, help="the name of the credential to store/get", nargs='?')
160-
parser.add_argument("value", type=str, help="the value of the credential to put (ignored if action is 'get')", nargs='?', default="")
182+
parsers['super'].add_argument("-r", "--region", help="the AWS region in which to operate. If a region is not specified, credstash will use the value of the AWS_DEFAULT_REGION env variable, or if that is not set, us-east-1")
183+
parsers['super'].add_argument("-t", "--table", default="credential-store", help="DynamoDB table to use for credential storage")
184+
subparsers = parsers['super'].add_subparsers(help='Try commands like "{name} get -h" or "{name} put --help" to get each sub command\'s options'.format(name=os.path.basename(__file__)))
185+
186+
action = 'delete'
187+
parsers[action] = subparsers.add_parser(action, help='Delete a credential from the store')
188+
parsers[action].add_argument("credential", type=str, help="the name of the credential to delete")
189+
parsers[action].set_defaults(action=action)
190+
191+
action = 'get'
192+
parsers[action] = subparsers.add_parser(action, help='Get a credential from the store')
193+
parsers[action].add_argument("credential", type=str, help="the name of the credential to get")
194+
parsers[action].add_argument("context", type=is_key_value_pair, action=KeyValueToDictionary, nargs='*', help="encryption context key/value pairs associated with the credential in the form of \"key=value\"")
195+
parsers[action].add_argument("-k", "--key", default="alias/credstash", help="the KMS key-id of the master key to use. See the README for more information. Defaults to alias/credstash")
196+
parsers[action].add_argument("-n", "--noline", action="store_true", help="Don't append newline to returned value (useful in scripts or with binary files)")
197+
parsers[action].add_argument("-v", "--version", default="", help="Get a specific version of the credential (defaults to the latest version).")
198+
parsers[action].set_defaults(action=action)
199+
200+
action = 'list'
201+
parsers[action] = subparsers.add_parser(action, help='list credentials and their versions')
202+
parsers[action].set_defaults(action=action)
161203

162-
parser.add_argument("-i", "--infile", default="", help="store the contents of `infile` rather than provide a value on the command line")
163-
parser.add_argument("-k", "--key", default="alias/credstash", help="the KMS key-id of the master key to use. See the README for more information. Defaults to alias/credstash")
164-
parser.add_argument("-n", "--noline", action="store_true", help="Don't append newline to returned value (useful in scripts or with binary files)")
165-
parser.add_argument("-r", "--region", help="the AWS region in which to operate. If a region is not specified, credstash will use the value of the AWS_DEFAULT_REGION env variable, or if that is not set, us-east-1")
166-
parser.add_argument("-t", "--table", default="credential-store", help="DynamoDB table to use for credential storage")
167-
parser.add_argument("-v", "--version", default="", help="If doing a `put`, put a specific version of the credential (update the credential; defaults to version `1`). If doing a `get`, get a specific version of the credential (defaults to the latest version).")
204+
action = 'put'
205+
parsers[action] = subparsers.add_parser(action, help='Put a credential into the store')
206+
parsers[action].add_argument("credential", type=str, help="the name of the credential to store")
207+
parsers[action].add_argument("value", type=str, help="the value of the credential to store", default="")
208+
parsers[action].add_argument("context", type=is_key_value_pair, action=KeyValueToDictionary, nargs='*', help="encryption context key/value pairs associated with the credential in the form of \"key=value\"")
209+
parsers[action].add_argument("-i", "--infile", default="", help="store the contents of `infile` rather than provide a value on the command line")
210+
parsers[action].add_argument("-k", "--key", default="alias/credstash", help="the KMS key-id of the master key to use. See the README for more information. Defaults to alias/credstash")
211+
parsers[action].add_argument("-v", "--version", default="", help="Put a specific version of the credential (update the credential; defaults to version `1`).")
212+
parsers[action].set_defaults(action=action)
168213

214+
action = 'setup'
215+
parsers[action] = subparsers.add_parser(action, help='setup the credential store')
216+
parsers[action].set_defaults(action=action)
169217

170-
args = parser.parse_args()
218+
args = parsers['super'].parse_args()
171219
region = os.getenv("AWS_DEFAULT_REGION", DEFAULT_REGION) if not args.region else args.region
172220
if args.action == "delete":
173221
deleteSecrets(args.credential, region=region, table=args.table)
@@ -189,7 +237,7 @@ def main():
189237
else:
190238
value_to_put = args.value
191239
try:
192-
if putSecret(args.credential, value_to_put, args.version, kms_key=args.key, region=region, table=args.table):
240+
if putSecret(args.credential, value_to_put, args.version, kms_key=args.key, region=region, table=args.table, context=args.context):
193241
print("{0} has been stored".format(args.credential))
194242
except KmsError as e:
195243
printStdErr(e)
@@ -198,7 +246,7 @@ def main():
198246
return
199247
if args.action == "get":
200248
try:
201-
sys.stdout.write(getSecret(args.credential, args.version, region=region, table=args.table))
249+
sys.stdout.write(getSecret(args.credential, args.version, region=region, table=args.table, context=args.context))
202250
if not args.noline:
203251
sys.stdout.write("\n")
204252
except ItemNotFound as e:

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name='credstash',
5-
version='1.2',
5+
version='1.3',
66
description='A utility for managing secrets in the cloud using AWS KMS and DynamoDB',
77
license='Apache2',
88
url='https://github.com/LuminalOSS/credstash',

0 commit comments

Comments
 (0)