Skip to content
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

Pass a default EncryptionContext on calls to KMS #136

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ When you want to fetch the credential, for example as part of the bootstrap proc
### Controlling and Auditing Secrets
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)`

As of version 2.0.0 credstash sets a default EncryptionContext with the credential name and version number. This is used as "Additional Authentication Data" in KMS, mitigating ciphertext replacement attacks in DynamoDB.

### Versioning Secrets
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.

Expand Down
34 changes: 24 additions & 10 deletions credstash.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,13 @@ def putSecret(name, secret, version="", kms_key="alias/credstash",
put a secret called `name` into the secret-store,
protected by the key kms_key
'''
if not context:
context = {}
version = paddedInt(version)
encryption_context = {'name': name, 'version': version}
if context:
encryption_context.update(context)
session = get_session(**kwargs)
kms = session.client('kms', region_name=region)
key_service = KeyService(kms, kms_key, context)
key_service = KeyService(kms, kms_key, encryption_context)
sealed = seal_aes_ctr_legacy(
key_service,
secret,
Expand All @@ -286,15 +288,15 @@ def putSecret(name, secret, version="", kms_key="alias/credstash",

data = {
'name': name,
'version': paddedInt(version),
'version': version,
}
data.update(sealed)

return secrets.put_item(Item=data, ConditionExpression=Attr('name').not_exists())


def getAllSecrets(version="", region=None, table="credential-store",
context=None, credential=None, session=None, **kwargs):
context=None, credential=None, session=None, set_default_context=True, **kwargs):
'''
fetch and decrypt all secrets
'''
Expand Down Expand Up @@ -324,6 +326,7 @@ def getAllSecrets(version="", region=None, table="credential-store",
context,
dynamodb,
kms,
set_default_context,
**kwargs)
except:
pass
Expand Down Expand Up @@ -423,12 +426,10 @@ def getSecretAction(args, region, **session_params):

def getSecret(name, version="", region=None,
table="credential-store", context=None,
dynamodb=None, kms=None, **kwargs):
dynamodb=None, kms=None, set_default_context=True, **kwargs):
'''
fetch and decrypt the secret called `name`
'''
if not context:
context = {}

# Can we cache
if dynamodb is None or kms is None:
Expand Down Expand Up @@ -456,9 +457,22 @@ def getSecret(name, version="", region=None,
"Item {'name': '%s', 'version': '%s'} couldn't be found." % (name, version))
material = response["Item"]

key_service = KeyService(kms, None, context)
encryption_context = {'name': name, 'version': material['version']} if set_default_context else {}
fallback_context = {}
if context != None:
fallback_context = context
encryption_context.update(context)
key_service = KeyService(kms, None, encryption_context)

try:
return open_aes_ctr_legacy(key_service, material)
except KmsError as e:
key_service = KeyService(kms, None, fallback_context)
secret = open_aes_ctr_legacy(key_service, material)
printStdErr("Secret is encrypted without a default EncryptionContext. "
"Run credstash-migrate-encryption-context.py to update.")
return secret

return open_aes_ctr_legacy(key_service, material)


@clean_fail
Expand Down
5 changes: 4 additions & 1 deletion credstash-migrate-autoversion.py → migrations/credstash-migrate-autoversion.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#!/usr/bin/env python
from __future__ import print_function
import sys, os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),"..")))

import boto3
import credstash
Expand Down Expand Up @@ -34,7 +37,7 @@ def updateVersions(region="us-east-1", table="credential-store"):
secrets.put_item(Item=new_item)
secrets.delete_item(Key={'name': old_item['name'], 'version': old_item['version']})
else:
print "Skipping item: %s, %s" % (old_item['name'], old_item['version'])
print("Skipping item: %s, %s" % (old_item['name'], old_item['version']))


if __name__ == "__main__":
Expand Down
20 changes: 20 additions & 0 deletions migrations/credstash-migrate-encryption-context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env python
import sys, os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),"..")))

import credstash

def migrateSettingEncryptionContext(table="credential-store"):
""" Re-encrypt all credentials which have no EncryptionContext.
Sets a default EncryptionContext with the credential name and version number.

No-op on credentials which have an EncryptionContext.
"""
secrets = credstash.getAllSecrets(context={}, set_default_context=False)
for name, secret in secrets.items():
latestVersion = credstash.getHighestVersion(name, table=table)
version = credstash.paddedInt(int(latestVersion) + 1)
credstash.putSecret(name, secret, version, table=table)

if __name__ == "__main__":
migrateSettingEncryptionContext()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name='credstash',
version='1.13.2',
version='2.0.0',
description='A utility for managing secrets in the cloud using AWS KMS and DynamoDB',
license='Apache2',
url='https://github.com/LuminalOSS/credstash',
Expand Down