Skip to content

Commit

Permalink
Support for multiple mail services (#1)
Browse files Browse the repository at this point in the history
* add support for mailgun

* add resources for mailgun support

* add mailgun mapping and mail client env var

* update folder structure and fix source code upload

* fix new key creation logic

* add support for multiple mail provider for destructor function

* add missing permission and requests library

* add requests library

* minor changes

* update readme and diagram
  • Loading branch information
paliwalvimal authored Jun 3, 2021
1 parent 3605e2f commit 50eefcd
Show file tree
Hide file tree
Showing 17 changed files with 353 additions and 167 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ __pycache__
*.json
*.sh
header.tf
creator.zip
destructor.zip
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ This tool is responsible for generating a new IAM access key pair every X days a
- IAM

### Process:
![aws-iam-key-rotator](iam-key-rotator.jpg "AWS IAM Key Rotator")
![aws-iam-key-rotator](iam-key-rotator.jpeg "AWS IAM Key Rotator")

- CloudWatch triggers lambda function which checks the age of access key for all IAM users who has **Email**(case-insensitive) tag attached.
- If access key age is greater than `ACCESS_KEY_AGE` environment variable and if the user has ONLY 1 key pair associated a new key pair is generated and the same is mailed to user via AWS SES.
- The existing key is stored in DynamoDB table with user details and an expiration timestamp.
- DynamoDB stream triggers another lambda function which is responsible for deleting the old access key associated to IAM user.
- If it fails to delete the old access key pair the entry is added back to DynamoDB table so that the same can be picked up later for retry.
- CloudWatch triggers lambda function which checks the age of access key for all the IAM users who have **Email**(case-insensitive) tag attached.
- If existing access key age is greater than `ACCESS_KEY_AGE` environment variable or `rotate_after` tag associated to the IAM user and if the user has a key pair associated a new key pair is generated and the same is mailed to the user via your selected mail service.
- The existing access key is than stored in DynamoDB table with user details and an expiration timestamp.
- DynamoDB stream triggers another lambda function which is responsible for deleting the old access key associated to IAM user if the stream event is `delete`.
- In case it fails to delete the existing key pair the entry is added back to the DynamoDB table so that the same can be picked up later for retry.
87 changes: 0 additions & 87 deletions destructor.py

This file was deleted.

2 changes: 1 addition & 1 deletion iam-key-rotator.drawio
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<mxfile host="app.diagrams.net" modified="2021-03-16T14:50:31.406Z" agent="5.0 (Macintosh; Intel Mac OS X 11_2_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36" etag="KInsNf2GbHXMx5XZcnsk" version="14.4.8" type="device"><diagram id="9zp9oJUibIssuBAXWE32" name="Page-1">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</diagram></mxfile>
<mxfile host="Electron" modified="2021-06-03T05:34:09.166Z" agent="5.0 (Macintosh; Intel Mac OS X 11_3_0) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36" etag="4mfJHJVs-9AIu1A0B5z_" version="14.6.13" type="device"><diagram id="9zp9oJUibIssuBAXWE32" name="Page-1">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</diagram></mxfile>
Binary file added iam-key-rotator.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed iam-key-rotator.jpg
Binary file not shown.
38 changes: 0 additions & 38 deletions ses_mailer.py

This file was deleted.

87 changes: 58 additions & 29 deletions creator.py → src/creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@
from datetime import datetime, date
from botocore.exceptions import ClientError

# No. of days to wait before existing key pair is deleted once a new key pair is generated
DAYS_FOR_DELETION = os.environ.get('DAYS_FOR_DELETION', 5)
IAM_KEY_ROTATOR_TABLE = os.environ.get('IAM_KEY_ROTATOR_TABLE')

# Table name which holds existing access key pair details to be deleted
IAM_KEY_ROTATOR_TABLE = os.environ.get('IAM_KEY_ROTATOR_TABLE', None)

# Days after which a new access key pair should be generated
ACCESS_KEY_AGE = os.environ.get('ACCESS_KEY_AGE', 85)

# Mail client to use for sending new key creation or existing key deletion mail
MAIL_CLIENT = os.environ.get('MAIL_CLIENT', 'ses')

# From address to be used while sending mail
MAIL_FROM = os.environ.get('MAIL_FROM', None)

# AWS_REGION environment variable is by default available within lambda environment
iam = boto3.client('iam', region_name=os.environ.get('AWS_REGION'))
dynamodb = boto3.client('dynamodb', region_name=os.environ.get('AWS_REGION'))
Expand Down Expand Up @@ -68,6 +78,7 @@ def fetch_user_details():
params['Marker'] = resp['Marker']
except Exception:
break
logging.info('User count: {}'.format(len(users)))

logger.info('Fetching tags for users individually')
with concurrent.futures.ThreadPoolExecutor(10) as executor:
Expand All @@ -79,6 +90,7 @@ def fetch_user_details():
users.pop(userName)
else:
users[userName]['attributes'] = userAttributes
logger.info('User with email tag: {}'.format([user for user in users]))

logger.info('Fetching keys for users individually')
with concurrent.futures.ThreadPoolExecutor(10) as executor:
Expand All @@ -93,37 +105,49 @@ def fetch_user_details():
return users

def create_user_key(userName, user):
if len(user['keys']) == 0:
logger.info('Skipping key creation for {} because no existing key found'.format(userName))
elif len(user['keys']) == 2:
logger.warn('Skipping key creation {} because 2 keys already exist. Please delete anyone to create new key'.format(userName))
else:
for k in user['keys']:
rotationAge = user['attributes']['rotate_after'] if 'rotate_after' in user['attributes'] else ACCESS_KEY_AGE
if k['ak_age_days'] <= rotationAge:
logger.info('Skipping key creation for {} because existing key is only {} day(s) old and the rotation is set for {} days'.format(userName, k['ak_age_days'], rotationAge))
else:
logger.info('Creating new access key for {}'.format(userName))
resp = iam.create_access_key(
UserName=userName
)

# Email keys to user
send_email(MAIL_CLIENT, user['email'], userName, resp['AccessKey']['AccessKeyId'], resp['AccessKey']['SecretAccessKey'], user['keys'][0]['ak'])

# Mark exisiting key to destory after X days
mark_key_for_destroy(userName, user['keys'][0]['ak'], user['email'])
try:
if len(user['keys']) == 0:
logger.info('Skipping key creation for {} because no existing key found'.format(userName))
elif len(user['keys']) == 2:
logger.warn('Skipping key creation {} because 2 keys already exist. Please delete anyone to create new key'.format(userName))
else:
for k in user['keys']:
rotationAge = user['attributes']['rotate_after'] if 'rotate_after' in user['attributes'] else ACCESS_KEY_AGE
if k['ak_age_days'] <= rotationAge:
logger.info('Skipping key creation for {} because existing key is only {} day(s) old and the rotation is set for {} days'.format(userName, k['ak_age_days'], rotationAge))
else:
logger.info('Creating new access key for {}'.format(userName))
resp = iam.create_access_key(
UserName=userName
)
logger.info('New key pair generated for user {}'.format(userName))

# Email keys to user
send_email(user['attributes']['email'], userName, resp['AccessKey']['AccessKeyId'], resp['AccessKey']['SecretAccessKey'], user['keys'][0]['ak'])

# Mark exisiting key to destory after X days
mark_key_for_destroy(userName, user['keys'][0]['ak'], user['attributes']['email'])
except (Exception, ClientError) as ce:
logger.error('Failed to create new key pair. Reason: {}'.format(ce))

def create_user_keys(users):
with concurrent.futures.ThreadPoolExecutor(10) as executor:
[executor.submit(create_user_key, user, users[user]) for user in users]

def send_email(mailClient, email, userName, accessKey, secretKey, existingAccessKey):
if mailClient == 'ses':
import ses_mailer
ses_mailer.send_email(email, userName, accessKey, secretKey, existingAccessKey)
else:
logger.error('{}: Invalid mailer client. Supported mail clients: AWS SES'.format(mailClient))
def send_email(email, userName, accessKey, secretKey, existingAccessKey):
mailBody = '<html><head><title>{}</title></head><body>Hey &#x1F44B; {},<br/><br/>A new access key pair has been generated for you. Please update the same wherever necessary.<br/><br/>Access Key: <strong>{}</strong><br/>Secret Access Key: <strong>{}</strong><br/><br/><strong>Note:</strong> Existing key pair <strong>{}</strong> will be deleted after {} days so please update the new key pair wherever required.<br/><br/>Thanks,<br/>Your Security Team</body></html>'.format('New Access Key Pair', userName, accessKey, secretKey, existingAccessKey, DAYS_FOR_DELETION)
try:
logger.info('Using {} as mail client'.format(MAIL_CLIENT))
if MAIL_CLIENT == 'ses':
import ses_mailer
ses_mailer.send_email(email, userName, MAIL_FROM, mailBody)
elif MAIL_CLIENT == 'mailgun':
import mailgun_mailer
mailgun_mailer.send_email(email, userName, MAIL_FROM, mailBody)
else:
logger.error('{}: Invalid mail client. Supported mail clients: AWS SES and Mailgun'.format(MAIL_CLIENT))
except (Exception, ClientError) as ce:
logger.error('Failed to send mail to user {} ({}). Reason: {}'.format(userName, email, ce))

def mark_key_for_destroy(userName, ak, email):
try:
Expand All @@ -150,5 +174,10 @@ def mark_key_for_destroy(userName, ak, email):
logger.error('Failed to mark key {} for deletion. Reason: {}'.format(ak, ce))

def handler(event, context):
users = fetch_user_details()
create_user_keys(users)
if IAM_KEY_ROTATOR_TABLE is None:
logger.error('IAM_KEY_ROTATOR_TABLE is required. Current value: {}'.format(IAM_KEY_ROTATOR_TABLE))
elif MAIL_FROM is None:
logger.error('MAIL_FROM is required. Current value: {}'.format(MAIL_FROM))
else:
users = fetch_user_details()
create_user_keys(users)
Loading

0 comments on commit 50eefcd

Please sign in to comment.