Skip to content

Commit

Permalink
Merge pull request #76 from dictcp/ft-encryptionContext
Browse files Browse the repository at this point in the history
Support AWS KMS Encryption Context
  • Loading branch information
jvehent authored Aug 22, 2016
2 parents a68b43d + c480e2e commit 06b1c13
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 8 deletions.
35 changes: 35 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,41 @@ appending it to the ARN of the master key, separated by a **+** sign::
<KMS ARN>+<ROLE ARN>
arn:aws:kms:us-west-2:927034868273:key/fe86dd69-4132-404c-ab86-4269956b4500+arn:aws:iam::927034868273:role/sops-dev-xyz

AWS KMS Encryption Context
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

SOPS has the ability to use AWS KMS key policy and encryption context
<http://docs.aws.amazon.com/kms/latest/developerguide/encryption-context.html>
to refine the access control of a given KMS master key.
Encryption contexts can be used in conjunction with KMS Key Policies to define
roles that can only access a given context. An example policy is shown below:

.. code:: json
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/RoleForExampleApp"
},
"Action": "kms:Decrypt",
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:EncryptionContext:AppName": "ExampleApp",
"kms:EncryptionContext:FilePath": "/var/opt/secrets/"
}
}
}
When creating a new file, you can specify encryption context in the
`--encryption-context` flag by comma separated list of key-value pairs:

<EncryptionContext Key>:<EncryptionContext Value>,<EncryptionContext Key>:<EncryptionContext Value>
eg.Environment:production,Role:web-server

The encryption context will be stored in the file metadata and not need to be provided at decryption.


Key Rotation
~~~~~~~~~~~~

Expand Down
46 changes: 39 additions & 7 deletions sops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ def main():
help="path to config file, disable recursive search"
" (default: {default})"
.format(default=DEFAULT_CONFIG_FILE))
argparser.add_argument('--encryption-context', dest='kmscontext',
help="comma separated list of KMS encryption "
"context key-value pairs")
argparser.add_argument('-V', '-v', '--version', action=ShowVersion,
version='%(prog)s ' + str(VERSION))
args = argparser.parse_args()
Expand Down Expand Up @@ -206,10 +209,16 @@ def main():
else:
otype = itype

# encryption context if any
kms_context = ""
if args.kmscontext:
kms_context = args.kmscontext

tree, need_key, existing_file = initialize_tree(args.file, itype,
kms_arns=kms_arns,
pgp_fps=pgp_fps,
configloc=args.config_loc)
configloc=args.config_loc,
kms_context=kms_context)
if not existing_file:
# can't use add/rm keys on new files, they don't yet have keys
if args.add_kms or args.add_pgp or args.rm_kms or args.rm_pgp:
Expand Down Expand Up @@ -361,7 +370,8 @@ def detect_filetype(filename):
return 'bytes'


def initialize_tree(path, itype, kms_arns=None, pgp_fps=None, configloc=None):
def initialize_tree(path, itype, kms_arns=None, pgp_fps=None, configloc=None,
kms_context=None):
""" Try to load the file from path in a tree, and failing that,
initialize a new tree using default data
"""
Expand All @@ -378,7 +388,8 @@ def initialize_tree(path, itype, kms_arns=None, pgp_fps=None, configloc=None):
kms_arns=kms_arns,
pgp_fps=pgp_fps,
path=path,
configloc=configloc)
configloc=configloc,
kms_context=kms_context)
# try to set the input version to the one set in the file
try:
global INPUT_VERSION
Expand Down Expand Up @@ -406,7 +417,8 @@ def initialize_tree(path, itype, kms_arns=None, pgp_fps=None, configloc=None):
if config:
kms_arns = config.get("kms", None)
pgp_fps = config.get("pgp", None)
tree, need_key = verify_or_create_sops_branch(tree, kms_arns, pgp_fps)
tree, need_key = verify_or_create_sops_branch(tree, kms_arns, pgp_fps,
kms_context=kms_context)
return tree, need_key, existing_file


Expand Down Expand Up @@ -499,7 +511,7 @@ def find_config_for_file(filename, configloc):


def verify_or_create_sops_branch(tree, kms_arns=None, pgp_fps=None,
path=None, configloc=None):
path=None, configloc=None, kms_context=None):
"""Verify or create the sops branch in the tree.
If the current tree doesn't have a sops branch with either kms or pgp
Expand Down Expand Up @@ -545,6 +557,8 @@ def verify_or_create_sops_branch(tree, kms_arns=None, pgp_fps=None,
tree, has_at_least_one_method = parse_kms_arn(tree, kms_arns)
if pgp_fps:
tree, has_at_least_one_method = parse_pgp_fp(tree, pgp_fps)
if kms_context:
tree = parse_kms_context(tree, kms_context)
if not has_at_least_one_method:
panic("Error: No KMS ARN or PGP Fingerprint found to encrypt the data "
"key, read the help (-h) for more information.", 111)
Expand Down Expand Up @@ -583,6 +597,20 @@ def parse_pgp_fp(tree, pgp_fps):
return tree, has_at_least_one_method


def parse_kms_context(tree, kms_context):
context = dict()
for item in kms_context.split(','):
item = item.replace(" ", "")
valuepos = item.find(":")
if valuepos > 0:
context[item[:valuepos]] = item[valuepos + 1:]

for entry in tree['sops']['kms']:
entry["context"] = context

return tree


def update_master_keys(tree, key):
""" If master keys have been added to the SOPS branch, encrypt the data key
with them, and store the new encrypted values.
Expand Down Expand Up @@ -1068,8 +1096,10 @@ def get_key_from_kms(tree):
errors.append("no kms client could be obtained for entry %s" %
entry['arn'])
continue
context = entry['context'] if 'context' in entry else {}
try:
kms_response = kms.decrypt(CiphertextBlob=b64decode(enc))
kms_response = kms.decrypt(CiphertextBlob=b64decode(enc),
EncryptionContext=context)
except Exception as e:
errors.append("kms %s failed with error: %s " % (entry['arn'], e))
continue
Expand All @@ -1090,8 +1120,10 @@ def encrypt_key_with_kms(key, entry):
print("ERROR: failed to initialize AWS KMS client for entry: %s" % err,
file=sys.stderr)
return None
context = entry['context'] if 'context' in entry else {}
try:
kms_response = kms.encrypt(KeyId=entry['arn'], Plaintext=key)
kms_response = kms.encrypt(KeyId=entry['arn'], Plaintext=key,
EncryptionContext=context)
except Exception as e:
print("ERROR: failed to encrypt key using kms arn %s: %s" %
(entry['arn'], e), file=sys.stderr)
Expand Down
8 changes: 7 additions & 1 deletion tests/test_sops.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,21 @@ def test_verify_or_create_sops_branch(self):
"656532927350:key/9006a8aa-0fa6-4c14-930e-a2dfb916de1d"
pgp_fps = "85D77543B3D624B63CEA9E6DBC17301B491B3F21," + \
"C9CAB0AF1165060DB58D6D6B2653B624D620786D"
kms_context = "Environment:production,Role:web-server"
tree = OrderedDict()
tree, ign = sops.verify_or_create_sops_branch(tree,
kms_arns=kms_arns,
pgp_fps=pgp_fps)
pgp_fps=pgp_fps,
kms_context=kms_context)
log.debug("%s", tree)
assert len(tree['sops']['kms']) == 2
assert tree['sops']['kms'][0]['arn'] == "arn:aws:kms:us-east-1:656532927350:key/920aff2e-c5f1-4040-943a-047fa387b27e"
assert tree['sops']['kms'][0]['role'] == "arn:aws:iam::927034868273:role/sops-dev"
assert tree['sops']['kms'][0]['context']['Environment'] == "production"
assert tree['sops']['kms'][0]['context']['Role'] == "web-server"
assert tree['sops']['kms'][1]['arn'] == "arn:aws:kms:ap-southeast-1:656532927350:key/9006a8aa-0fa6-4c14-930e-a2dfb916de1d"
assert tree['sops']['kms'][1]['context']['Environment'] == "production"
assert tree['sops']['kms'][1]['context']['Role'] == "web-server"
assert len(tree['sops']['pgp']) == 2
assert tree['sops']['pgp'][0]['fp'] == "85D77543B3D624B63CEA9E6DBC17301B491B3F21"
assert tree['sops']['pgp'][1]['fp'] == "C9CAB0AF1165060DB58D6D6B2653B624D620786D"
Expand Down

0 comments on commit 06b1c13

Please sign in to comment.