diff --git a/README.md b/README.md index de80850..ac9c859 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/credstash.py b/credstash.py index ca1a1fd..fc26dfd 100755 --- a/credstash.py +++ b/credstash.py @@ -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, @@ -286,7 +288,7 @@ def putSecret(name, secret, version="", kms_key="alias/credstash", data = { 'name': name, - 'version': paddedInt(version), + 'version': version, } data.update(sealed) @@ -294,7 +296,7 @@ def putSecret(name, secret, version="", kms_key="alias/credstash", 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 ''' @@ -324,6 +326,7 @@ def getAllSecrets(version="", region=None, table="credential-store", context, dynamodb, kms, + set_default_context, **kwargs) except: pass @@ -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: @@ -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 diff --git a/credstash-migrate-autoversion.py b/migrations/credstash-migrate-autoversion.py old mode 100644 new mode 100755 similarity index 83% rename from credstash-migrate-autoversion.py rename to migrations/credstash-migrate-autoversion.py index 020541c..50a535e --- a/credstash-migrate-autoversion.py +++ b/migrations/credstash-migrate-autoversion.py @@ -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 @@ -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__": diff --git a/migrations/credstash-migrate-encryption-context.py b/migrations/credstash-migrate-encryption-context.py new file mode 100755 index 0000000..c8af3a0 --- /dev/null +++ b/migrations/credstash-migrate-encryption-context.py @@ -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() diff --git a/setup.py b/setup.py index 1927bea..59bb3f9 100644 --- a/setup.py +++ b/setup.py @@ -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',