Skip to content

Commit

Permalink
Merge pull request #72 from l2ol33rt/assumerole_concept
Browse files Browse the repository at this point in the history
Adding IAM Assumerole Support
  • Loading branch information
alex-luminal committed Apr 2, 2016
2 parents 36405d4 + 15c8322 commit 81c6aaa
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 39 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ optional arguments:
-t TABLE, --table TABLE
DynamoDB table to use for credential storage
```

-n ARN, --arn ARN AWS IAM ARN for AssumeRole
## IAM Policies

### Secret Writer
Expand Down
115 changes: 77 additions & 38 deletions credstash.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,13 @@ def paddedInt(i):
pad = PAD_LEN - len(i_str)
return (pad * "0") + i_str


def getHighestVersion(name, region=None, table="credential-store",
profile_name=None):
**kwargs):
'''
Return the highest version of `name` in the table
'''
session = boto3.Session(profile_name=profile_name)
session = get_session(**kwargs)

dynamodb = session.resource('dynamodb', region_name=region)
secrets = dynamodb.Table(table)
Expand All @@ -163,12 +164,12 @@ def getHighestVersion(name, region=None, table="credential-store",
return response["Items"][0]["version"]


def listSecrets(region=None, table="credential-store", profile_name=None):
def listSecrets(region=None, table="credential-store", **kwargs):
'''
do a full-table scan of the credential-store,
and return the names and versions of every credential
'''
session = boto3.Session(profile_name=profile_name)
session = get_session(**kwargs)

dynamodb = session.resource('dynamodb', region_name=region)
secrets = dynamodb.Table(table)
Expand All @@ -180,14 +181,14 @@ def listSecrets(region=None, table="credential-store", profile_name=None):

def putSecret(name, secret, version, kms_key="alias/credstash",
region=None, table="credential-store", context=None,
profile_name=None):
**kwargs):
'''
put a secret called `name` into the secret-store,
protected by the key kms_key
'''
if not context:
context = {}
session = boto3.Session(profile_name=profile_name)
session = get_session(**kwargs)
kms = session.client('kms', region_name=region)
# generate a a 64 byte key.
# Half will be for data encryption, the other half for HMAC
Expand Down Expand Up @@ -220,35 +221,36 @@ def putSecret(name, secret, version, kms_key="alias/credstash",
return secrets.put_item(Item=data, ConditionExpression=Attr('name').not_exists())


def getAllSecrets(version="", region=None,
table="credential-store", context=None, profile_name=None):
def getAllSecrets(version="", region=None, table="credential-store",
context=None, **kwargs):
'''
fetch and decrypt all secrets
'''
output = {}
secrets = listSecrets(region, table, profile_name=profile_name)
secrets = listSecrets(region, table, **kwargs)
for credential in set([x["name"] for x in secrets]):
try:
output[credential] = getSecret(credential,
version,
region,
table,
context,
profile_name=profile_name)
**kwargs)
except:
pass
return output


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

session = boto3.Session(profile_name=profile_name)
session = get_session(**kwargs)
dynamodb = session.resource('dynamodb', region_name=region)
secrets = dynamodb.Table(table)

Expand Down Expand Up @@ -300,8 +302,8 @@ def getSecret(name, version="", region=None,


def deleteSecrets(name, region=None, table="credential-store",
profile_name=None):
session = boto3.Session(profile_name=profile_name)
**kwargs):
session = get_session(**kwargs)
dynamodb = session.resource('dynamodb', region_name=region)
secrets = dynamodb.Table(table)

Expand All @@ -314,11 +316,11 @@ def deleteSecrets(name, region=None, table="credential-store",
secrets.delete_item(Key=secret)


def createDdbTable(region=None, table="credential-store", profile_name=None):
def createDdbTable(region=None, table="credential-store", **kwargs):
'''
create the secret store table in DDB in the specified region
'''
session = boto3.Session(profile_name=profile_name)
session = get_session(**kwargs)
dynamodb = session.resource("dynamodb", region_name=region)
if table in (t.name for t in dynamodb.tables.all()):
print("Credential Store table already exists")
Expand Down Expand Up @@ -361,6 +363,28 @@ def createDdbTable(region=None, table="credential-store", profile_name=None):
"Go read the README about how to create your KMS key")


def get_session(aws_access_key_id=None, aws_secret_access_key=None,
aws_session_token=None, profile_name=None):
if get_session._cached_session is None:
get_session._cached_session = boto3.Session(aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
aws_session_token=aws_session_token,
profile_name=profile_name)
return get_session._cached_session
get_session._cached_session = None


def get_assumerole_credentials(arn):
sts_client = boto3.client('sts')
# Use client object and pass the role ARN
assumedRoleObject = sts_client.assume_role(RoleArn=arn,
RoleSessionName="AssumeRoleCredstashSession1")
credentials = assumedRoleObject['Credentials']
return dict(aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken'])


def main():
parsers = {}
parsers['super'] = argparse.ArgumentParser(
Expand All @@ -377,9 +401,12 @@ def main():
parsers['super'].add_argument("-t", "--table", default="credential-store",
help="DynamoDB table to use for "
"credential storage")
parsers['super'].add_argument("-p", "--profile", default=None,
help="Boto config profile to use when "
"connecting to AWS")
role_parse = parsers['super'].add_mutually_exclusive_group()
role_parse.add_argument("-p", "--profile", default=None,
help="Boto config profile to use when "
"connecting to AWS")
role_parse.add_argument("-n", "--arn", default=None,
help="AWS IAM ARN for AssumeRole")
subparsers = parsers['super'].add_subparsers(help='Try commands like '
'"{name} get -h" or "{name}'
'put --help" to get each'
Expand Down Expand Up @@ -468,11 +495,11 @@ def main():
"credential (update the credential; "
"defaults to version `1`).")
parsers[action].add_argument("-a", "--autoversion", action="store_true",
help="Automatically increment the version of "
"the credential to be stored. This option "
"causes the `-v` flag to be ignored. "
"(This option will fail if the currently stored "
"version is not numeric.)")
help="Automatically increment the version of "
"the credential to be stored. This option "
"causes the `-v` flag to be ignored. "
"(This option will fail if the currently stored "
"version is not numeric.)")
parsers[action].set_defaults(action=action)

action = 'setup'
Expand All @@ -482,22 +509,32 @@ def main():

args = parsers['super'].parse_args()

# Check for assume role and set session params
session_params = {}
if args.profile is None and args.arn:
session_params = get_assumerole_credentials(args.arn)
elif args.profile:
session_params = dict(profile_name=args.profile)

try:
region = args.region
session = boto3.Session(profile_name=args.profile)
session = get_session(**session_params)
session.resource('dynamodb', region_name=region)
except botocore.exceptions.NoRegionError:
if not 'AWS_DEFAULT_REGION' in os.environ:
region = DEFAULT_REGION

if "action" in vars(args):
if args.action == "delete":
deleteSecrets(args.credential, region=region, table=args.table,
profile_name=args.profile)
deleteSecrets(args.credential,
region=region,
table=args.table,
**session_params)
return
if args.action == "list":
credential_list = listSecrets(region=region, table=args.table,
profile_name=args.profile)
credential_list = listSecrets(region=region,
table=args.table,
**session_params)
if credential_list:
# print list of credential names and versions,
# sorted by name and then by version
Expand All @@ -510,9 +547,10 @@ def main():
return
if args.action == "put":
if args.autoversion:
latestVersion = getHighestVersion(args.credential, region,
latestVersion = getHighestVersion(args.credential,
region,
args.table,
profile_name=args.profile)
**session_params)
try:
version = paddedInt(int(latestVersion) + 1)
except ValueError:
Expand All @@ -523,15 +561,16 @@ def main():
try:
if putSecret(args.credential, args.value, version,
kms_key=args.key, region=region, table=args.table,
context=args.context, profile_name=args.profile):
context=args.context,
**session_params):
print("{0} has been stored".format(args.credential))
except KmsError as e:
fatal(e)
except botocore.exceptions.ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
latestVersion = getHighestVersion(args.credential, region,
args.table,
profile_name=args.profile)
**session_params)
fatal("%s version %s is already in the credential store. "
"Use the -v flag to specify a new version" %
(args.credential, latestVersion))
Expand All @@ -545,20 +584,20 @@ def main():
for x
in listSecrets(region=region,
table=args.table,
profile_name=args.profile)])
**session_params)])
print(json.dumps(dict((name,
getSecret(name,
args.version,
region=region,
table=args.table,
context=args.context,
profile_name=args.profile))
**session_params))
for name in names)))
else:
sys.stdout.write(getSecret(args.credential, args.version,
region=region, table=args.table,
context=args.context,
profile_name=args.profile))
**session_params))
if not args.noline:
sys.stdout.write("\n")
except ItemNotFound as e:
Expand All @@ -573,7 +612,7 @@ def main():
region=region,
table=args.table,
context=args.context,
profile_name=args.profile)
**session_params)
if args.format == "json":
output_func = json.dumps
output_args = {"sort_keys": True,
Expand All @@ -589,7 +628,7 @@ def main():
return
if args.action == "setup":
createDdbTable(region=region, table=args.table,
profile_name=args.profile)
**session_params)
return
else:
parsers['super'].print_help()
Expand Down

0 comments on commit 81c6aaa

Please sign in to comment.