AWS IAM Access keys rotation using Lambda function
Architecutre Diagram

AWS IAM Access keys rotation using Lambda function

As per AWS security best practice, we should regularly rotate our IAM access keys to improve our AWS accounts security.

In this article I will teach you how we?automate?the process of IAM access keys rotation according to the access key ages using Lambda function.

Use this link to rotate the keys manually using AWS document?https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_RotateAccessKey

AWS IAM (Identity and access management)?provides find-grained access control across all of AWS.

AWS Access keys?are long term credentials for an IAM user. You can use access keys to sign programmatic requests to the AWS CLI or AWS API (directly or using the AWS SDK).

AWS Lambda?is a serverless, event-driven compute service that lets you run code for virtually any type of application or backend service without provisioning or managing servers. You can trigger Lambda from over 200 AWS services and software as a service (SaaS) applications, and only pay for what you use.

Architecture Diagram

No alt text provided for this image

IAM Access Keys Rotation Using AWS Lambda Function and Send Notifications Using AWS SES

The diagram shows the following workflow:

1- IAM users will login programmatically using IAM access keys

2- CloudWatch event initiates a Lambda function every 24 hours

3- The Lambda function initiates a Lambda function for each AWS account ID and passes it the metadata for additional processing. It will check all users access key ages and initiate the email to the account’s owner

4- In my case I have setup the three conditions to send emails using AWS SES.

Python script in Lambda Function:

I have written the access keys rotation script in python. This python script will first get the each user information/metadata from AWS IAM. As you know AWS IAM user could be human or could be machines, so I have properly tag each user with the following information:

No alt text provided for this image
No alt text provided for this image

Lambda function will get this above information from the IAM user tags and if the UserType is “employee” and the key age 70th, 80th or 90th day then it will send the notification email using AWS SES and inactive the access key. If the UserType is “machine” and the key age 70th, 80th or 90th day then it will just send the notification email to the concerned team (like IT, Ops etc.).

Send alerts and inactivate access keys conditions:

First condition if IAM access key age of the user is 70th day then send the alert email?“Today is the 70th day of your access keys. Please rotate your access key before it turns 90 days”.

Second condition if IAM access key age of the user is 80th day then send the alert email?“Today is the 80th day of your access keys. Please rotate your access key before it turns 90 days”.

Third condition if IAM access key age of the user is 90th day then send the alert email?“Today is the 90th day of your access keys and your key has been inactivate. Please generate new key or reach IT support team”.

Lambda function code:

Get the proper formated code from my github repository: https://github.com/engr-usman/blogs.git

import boto3, os, time, datetime, sys, jso
from datetime import date
from botocore.exceptions import ClientError
iam = boto3.client('iam')
def lambda_handler(event, context):
email_70_list = []
email_80_list = []
email_90_list = []
# print("All IAM user emails that have AccessKeys 70 days or older")
unique_user_list = (iam.list_users()['Users'])
for userlist in unique_user_list:
userKeys = iam.list_access_keys(UserName=userlist['UserName'])
for keyValue in userKeys['AccessKeyMetadata']:
UserAccessKeyID = keyValue['AccessKeyId']
IAMUserName = keyValue['UserName']
#print(f"IAMUserName IAM Users:{len(IAMUserName)}: {IAMUserName}")
if keyValue['Status'] == 'Active':
currentdate = date.today()
active_days = currentdate - keyValue['CreateDate'].date()
#print ("The active days details are: ", active_days)
#print ("datetime details are: ", datetime.timedelta(days=15))
# if Access key age is greater then or equal to 70 days, send warning
if active_days == datetime.timedelta(days=int(os.environ['days_70'])):
userTags = iam.list_user_tags(UserName=keyValue['UserName'])
email_tag = list(filter(lambda tag: tag['Key'] == 'email', userTags['Tags']))
if(len(email_tag) == 1):
email = email_tag[0]['Value']
email_70_list.append(email)
print("This User: ", IAMUserName, ", with the email: ", email, ", is having access key age is 70 days")
email_unique = list(set(email_70_list))
print("Email list: ", email_unique)
RECIPIENTS = email_unique
SENDER = os.environ['sender_email']
AWS_REGION = os.environ['region']
SUBJECT_70 = os.environ['SUBJECT_70']
BODY_TEXT_70 = os.environ['BODY_TEXT_70']
BODY_HTML_70 = os.environ['BODY_HTML_70']
CHARSET = "UTF-8"
client = boto3.client('ses',region_name=AWS_REGION)
try:
response = client.send_email(
Destination={
'ToAddresses': RECIPIENTS,
},
Message={
'Body': {
'Html': {
'Charset': CHARSET,
'Data': BODY_HTML_70,
},
'Text': {
'Charset': CHARSET,
'Data': BODY_TEXT_70,
},
},
'Subject': {
'Charset': CHARSET,
'Data': SUBJECT_70,
},
},
Source=SENDER,
)
except ClientError as e:
print(e.response['Error']['Message'])
else:
print("Email sent! Message ID:"),
print(response['MessageId'])
# if Access Key Age is greater then 80 days, send email alert
if active_days == datetime.timedelta(days=int(os.environ['days_80'])):
userTags = iam.list_user_tags(UserName=keyValue['UserName'])
email_tag = list(filter(lambda tag: tag['Key'] == 'email', userTags['Tags']))
if(len(email_tag) == 1):
email = email_tag[0]['Value']
email_80_list.append(email)
print("The User: ", IAMUserName, ", with the email: ", email, ", is having access key age is 80 days")
email_unique = list(set(email_80_list))
print("Email list: ", email_unique)
RECIPIENTS = email_unique
SENDER = os.environ['sender_email']
print("Sender: ", SENDER)
AWS_REGION = os.environ['region']
SUBJECT_80 = os.environ['SUBJECT_80']
BODY_TEXT_80 = os.environ['BODY_TEXT_80']
BODY_HTML_80 = os.environ['BODY_HTML_80']
CHARSET = "UTF-8"
client = boto3.client('ses',region_name=AWS_REGION)
try:
response = client.send_email(
Destination={
'ToAddresses': RECIPIENTS,
},
Message={
'Body': {
'Html': {
'Charset': CHARSET,
'Data': BODY_HTML_80,
},
'Text': {
'Charset': CHARSET,
'Data': BODY_TEXT_80,
},
},
'Subject': {
'Charset': CHARSET,
'Data': SUBJECT_80,
},
},
Source=SENDER,
)
except ClientError as e:
print(e.response['Error']['Message'])
else:
print("Email sent! Message ID:"),
print(response['MessageId'])
# if Access Key Age is greater then 90 days, send email alert and inactive access keys
if active_days >= datetime.timedelta(days=int(os.environ['days_90'])):
userTags = iam.list_user_tags(UserName=keyValue['UserName'])
email_tag = list(filter(lambda tag: tag['Key'] == 'email', userTags['Tags']))
user1_tag = list(filter(lambda tag: tag['Key'] == 'UserType', userTags['Tags']))
if(len(email_tag) == 1):
email = email_tag[0]['Value']
email_90_list.append(email)
print("The User: ", IAMUserName, ", with the email: ", email, ", is having access key age is greater then 90 days")
if(len(user1_tag) == 1):
user1tag = user1_tag[0]['Value']
if user1tag == "Employee":
iam.update_access_key(AccessKeyId=UserAccessKeyID,Status='Inactive',UserName=IAMUserName)
print("Status has been updated to Inactive")
email_unique = list(set(email_90_list))
print("Email list: ", email_unique)
RECIPIENTS = email_unique
SENDER = os.environ['sender_email']
print("Sender: ", SENDER)
AWS_REGION = os.environ['region']
SUBJECT_90 = os.environ['SUBJECT_90']
BODY_TEXT_90 = os.environ['BODY_TEXT_90']
BODY_HTML_90 = os.environ['BODY_HTML_90']
CHARSET = "UTF-8"
client = boto3.client('ses',region_name=AWS_REGION)
try:
response = client.send_email(
Destination={
'ToAddresses': RECIPIENTS,
},
Message={
'Body': {
'Html': {
'Charset': CHARSET,
'Data': BODY_HTML_90,
},
'Text': {
'Charset': CHARSET,
'Data': BODY_TEXT_90,
},
},
'Subject': {
'Charset': CHARSET,
'Data': SUBJECT_90,
},
},
Source=SENDER,
)
except ClientError as e:
print(e.response['Error']['Message'])
else:
print("Email sent! Message ID:"),
print(response['MessageId'])n        

Note: Indentation is not correct of the above code :( and I know there is lot of code improvement its just for an idea.

Enable CloudWatch event rule:

Now we just need to create a CloudWatch event rule (now AWS EventBridge) to trigger this Lambda function at specific time. In my case I have enabled this cron job at 9am morning.

No alt text provided for this image

AWS EventBridge Scheduler and Target

Conclusion

This is AWS security best practice to rotate IAM access keys so no one can able to access your account resources. Lambda function is the best tool to perform these kind of tasks and trigger it from other services like CloudWatch.

I hope you enjoyed reading this article, please feel free to contact me?if you have any questions.

Saif Ali

Data Engineer | Skilled in Data Processing and Big Data Technologies | SQL | Python |2x AWS | Hadoop | Spark

1 年

Usman Ahmad what are the roles you provided your lambda function ?

回复
Madhu Sudhan N

Senior SRE Devops

2 年

Hi Usman Ahmad I am trying to fetch the details of both sender and region details here. bot its not happening. pls let me know whats this sender refers to? how i should try in my code. as you said in the post i have created the mail in ses and mentioned the VAULE in the IAM user tags. can you help me what i am missing inorder to get to get the region and sender details

  • 该图片无替代文字

Hi Usman! Can you publish script in the correct format (at least screenshot), please? I'm not Python specialist for now, maybe in the future. Thank you.

回复
Uzair Azmat

DevOps Engineer | AWS Community Builder | AWS Certified Solutions Architect Associate | AWS Certified Solutions Architect Professional | AWS Technical Trainer

3 年

Amazing

要查看或添加评论,请登录

Usman Ahmad的更多文章

社区洞察

其他会员也浏览了