AWS IAM Access Key Expiration Alerts And Key Deletion -Using Lambda

Amman Sheriff
9 min readMar 23, 2023

#####IAM Access Keys?#####

When you use AWS programmatically, you provide your AWS access keys so that AWS can verify your identity in programmatic calls. Your access keys consist of an access key ID (for example, AKIAIOSFODNN7EXAMPLE) and a secret access key (for example, wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY).

IAM Identity and access management (IAM) is the process used in businesses and organizations to grant or deny employees and others authorization to secure systems.

Refer Here for more details — https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html

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

The diagram shows the following workflow:
1. IAM users will log in programmatically using IAM access keys
2. AWS EventBridge event initiates a Lambda function every 24 hours.
3. The Lambda function initiates a Lambda function for each AWS account ID and passes it to the metadata for additional processing. It will check all users access key ages and initiate the email to the account’s owner, and inactive and delete only the keys that are assigned to IAM users. For service accounts, it will only be sending alerts.

Alerts and inactive/delete access key conditions:

First condition if the IAM access key age of the user is 70th day then send the alert email “Your AWS Access keys will be set to inactive form another 10 days. Please rotate your access key before it turns 90 days”. This applies for both ( IAM Employee and Service accounts)

Second condition if the IAM access key age of the user/employee is 80th day then send the alert email “Your AWS Access keys have been set to inactive and will be deleted from another 10 days, Please rotate your access key before it turns 90 days” else if IAM access key age for service account/machine is 80th day, send an alert email “ Warning Message to renew your service account access keys”

Third condition if the IAM access key age of the user/employee is 90th day then send the alert email “Your AWS Access keys have been deleted, Please rotate your access key before it turns 90 days.”, else if the IAM access keys age for service account/machine is 90th day, send an alert email “ Critical Message to renew your service account access keys”

Services Used:
1. AWS Lambda
2. AWS Event Bridge
3. AWS SES
4. AWS IAM

Implementation Instructions:

First Setup — IAM Tagging:

Each IAM users need to tag based on the user type. (Example: employee or machine)

Following tags need to be defined for each IAM User:
Name: (username)
UserType : (employee or machine)
Email: (user_01@sample.com)

IAM tags — example

Second, create the lambda function:

Go to Lambda Service Dashboard > Click on the left panel “Functions” and Click “Create Function” select the Author from scratch (Lambda>Functions>Create function>Author from scratch)

· Function Name: iam_key_rotation (You can give your own function name)

· Runtime: Python 3.9

· Execution role: Select > Create a new role with basic Lambda permissions

· Click > “Create function”

Third Updating the Python Code:

Copy the code from GitHub repo https://github.com/aasheriff/IAM_Key_Rotation and update the lambda_function.py and click “Deploy

import boto3, os, time, datetime, sys, json
from datetime import date
from botocore.exceptions import ClientError

iam = boto3.client("iam")


def lambda_handler(event, context):
email_70_list = []
email_80_employee = []
email_80_machine = []
email_90_employee = []
email_90_machine = []

# print("All IAM user emails that have AccessKeys 30 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 = os.environ["SUBJECT"]
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,
},
},
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"])
)
user1_tag = list(filter(lambda tag: tag['Key'] == 'UserType', userTags['Tags']))

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")

if len(email_tag) == 1:
email = email_tag[0]["Value"]
email_80_employee.append(email)
print(
"The User: ",
IAMUserName,
", with the email: ",
email,
", is having access key age is 80 days",
)


email_unique = list(set(email_80_employee))
print("Email list: ", email_unique)
RECIPIENTS = email_unique
SENDER = os.environ["sender_email"]
print("Sender: ", SENDER)
AWS_REGION = os.environ["region"]
SUBJECT = os.environ["SUBJECT"]
BODY_TEXT_80_employee = os.environ["BODY_TEXT_80_employee"]
BODY_HTML_80_employee = os.environ["BODY_HTML_80_employee"]
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_employee,
},
"Text": {
"Charset": CHARSET,
"Data": BODY_TEXT_80_employee,
},
},
"Subject": {
"Charset": CHARSET,
"Data": SUBJECT,
},
},
Source=SENDER,
)
except ClientError as e:
print(e.response["Error"]["Message"])
else:
print("Email sent! Message ID:"),
print(response["MessageId"])


elif user1tag == "Machine":
print(user1_tag)

if len(email_tag) == 1:
email = email_tag[0]["Value"]
email_80_machine.append(email)
print(
"The User: ",
IAMUserName,
", with the email: ",
email,
", is having access key age is greater then 90 days",
)
email_unique = list(set(email_80_machine))
print("Email list: ", email_unique)
RECIPIENTS = email_unique
SENDER = os.environ["sender_email"]
print("Sender: ", SENDER)
AWS_REGION = os.environ["region"]
SUBJECT = os.environ["SUBJECT"]
BODY_TEXT_80_machine = os.environ["BODY_TEXT_80_machine"]
BODY_HTML_80_machine = os.environ["BODY_HTML_80_machine"]
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_machine,
},
"Text": {
"Charset": CHARSET,
"Data": BODY_TEXT_80_machine,
},
},
"Subject": {
"Charset": CHARSET,
"Data": SUBJECT,
},
},
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"])
)
user2_tag = list(
filter(lambda tag: tag["Key"] == "UserType", userTags["Tags"])
)
if len(user2_tag) == 1:
user2tag = user2_tag[0]["Value"]
if user2tag == "Employee":
print(user2_tag)
iam.delete_access_key(
AccessKeyId=UserAccessKeyID,
UserName=IAMUserName,
)
print("Status has been updated to deleted")

if len(email_tag) == 1:
email = email_tag[0]["Value"]
email_90_employee.append(email)
print(
"The User: ",
IAMUserName,
", with the email: ",
email,
", is having access key age is greater then 90 days",
)
email_unique = list(set(email_90_employee))
print("Email list: ", email_unique)
RECIPIENTS = email_unique
SENDER = os.environ["sender_email"]
print("Sender: ", SENDER)
AWS_REGION = os.environ["region"]
SUBJECT = os.environ["SUBJECT"]
BODY_TEXT_90_employee = os.environ["BODY_TEXT_90_employee"]
BODY_HTML_90_employee = os.environ["BODY_HTML_90_employee"]
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_employee,
},
"Text": {
"Charset": CHARSET,
"Data": BODY_TEXT_90_employee,
},
},
"Subject": {
"Charset": CHARSET,
"Data": SUBJECT,
},
},
Source=SENDER,
)
except ClientError as e:
print(e.response["Error"]["Message"])
else:
print("Email sent! Message ID:"),
print(response["MessageId"])


elif user2tag == "Machine":
print(user2_tag)

if len(email_tag) == 1:
email = email_tag[0]["Value"]
email_90_machine.append(email)
print(
"The User: ",
IAMUserName,
", with the email: ",
email,
", is having access key age is greater then 90 days",
)
email_unique = list(set(email_90_machine))
print("Email list: ", email_unique)
RECIPIENTS = email_unique
SENDER = os.environ["sender_email"]
print("Sender: ", SENDER)
AWS_REGION = os.environ["region"]
SUBJECT = os.environ["SUBJECT"]
BODY_TEXT_90_machine = os.environ["BODY_TEXT_90_machine"]
BODY_HTML_90_machine = os.environ["BODY_HTML_90_machine"]
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_machine,
},
"Text": {
"Charset": CHARSET,
"Data": BODY_TEXT_90_machine,
},
},
"Subject": {
"Charset": CHARSET,
"Data": SUBJECT,
},
},
Source=SENDER,
)
except ClientError as e:
print(e.response["Error"]["Message"])
else:
print("Email sent! Message ID:"),
print(response["MessageId"])

Forth Updating the Environment Variables

Click on the Configuration Tab and update the environment variable as below

You can copy the Keys and Values here:
Key Value
BODY_HTML_70 Your AWS Access keys will be set to inactive from another 10 days. Please rotate your access key before it turns 90 days
BODY_HTML_80_employee Your AWS Access keys has been set to inactive and will be deleted from another 10 days, Please rotate your access key before it turns 90 days
BODY_HTML_80_machine Warning Message to renew your service account access keys
BODY_HTML_90_employee Your AWS Access keys has been deleted, Please rotate your access key before it turns 90 days.
BODY_HTML_90_machine Critical Message to renew your service account access keys
BODY_TEXT_70 Your AWS Access keys will be set to inactive from another 10 days
BODY_TEXT_80_employee Your AWS Access keys has been set to inactive and will be deleted from another 10 days, Please rotate your access key before it turns 90 days
BODY_TEXT_90_employee Your AWS Access keys has been deleted, Please rotate your access key before it turns 90 days.
BODY_TEXT_90_machine Critical Message to renew your service account access keys
SUBJECT AWS Access Key - Renewal - Alerts
days_70 70
days_80 80
days_90 90
region us-east-1
sender_email legionlenovointel@gmail.com

Fifth update the IAM role permissions

Select the Permissions tab and click on the Role Name.

Click Add Permissions > Select Attached policy and add IAMFullAccess (AWS Managed Policy.

Add another policy Click Add Permissions > Select “Create Inline Policy” and update the JSON Policy permission as below > Policy Name : SES_access_policy

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": "*"
}
]
}

Sixth step setting AWS SES

Open AWS SES Service in the Left panel > Select “Verified identities” > Click “Create Identity

Select “Email Address” > Update the Email address of the alert sender> Click “Create Identity

Note: We are going to update the alert's sender email ID and verify it

Now Logging to your email and authorize the email.

Seventh Step adding the trigger:

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.

Go to AWS EventBridge Service > Select “Rules” > “Create Rule” >

Update the schedule cron expression as below, and click next.

In > Select Target > Click All API’s and search (Lambda) > Click AWS Lambda.

Scroll Down and select > AWS Lambda Invoke

In the Invoke settings, select the iam_key_roation function and click Next by keeping the defaults > Click “Create schedule”.

Thank You

--

--

Amman Sheriff

Cloud Infra | AWS SA & SYSOPS | IBM Spectrum Protect (TSM) | EMC Storage's | Netapp | VMware | Linux | Windows