Python script that checks the age of AWS IAM access keys and disabled them if they are paste the defined expiration. It has been build in a way that can be automatically called in an AWS Lambda function.

Input Parameters

The script takes the following parameters in as environment variables.

Name Example Description
USER_GROUP_TARGET Employees Users in the group will only checked
SOURCE_ADDRESS [email protected] Source email to send from
CC_EMAIL_ADDRESS [email protected] Email to carbon copy when key is disabled
WARN_DAYS 70 When to start warning about key expiration
DISABLE_DAY 90 When to disable the key
DELETE_DAY 120 When to delete the key

Python Script

The code has been tested with up to python3.11 and the entry point is lambda_handler.

import os
import boto3
from datetime import date

# Email stuff
SOURCE_ADDRESS = os.environ.get('SOURCE_ADDRESS')
CC_EMAIL_ADDRESS = os.environ.get('CC_EMAIL_ADDRESS')

USER_GROUP_TARGET = os.environ.get('USER_GROUP_TARGET')

# Day settings
WARN_DAYS = int(os.environ.get('WARN_DAYS'))
DISABLE_DAY = int(os.environ.get('DISABLE_DAY'))
DELETE_DAY = int(os.environ.get('DELETE_DAY'))

def findOldAccessKeys():
  iam = boto3.client('iam')
  currentdate = date.today()
  # List all users
  for user in iam.list_users()['Users']:
    emailBool = False
    userGroups = iam.list_groups_for_user(UserName=user['UserName'])
    # For each group
    for groupName in userGroups['Groups']:
      # For users in the group 'GroupName'
      if USER_GROUP_TARGET in groupName['GroupName']:
        res = iam.list_access_keys(UserName=user['UserName'])
        # For each access key they have
        for AccessKeys in res['AccessKeyMetadata']:
          active_days = currentdate - AccessKeys['CreateDate'].date()
          # If the key is active
          if (AccessKeys['Status'] == 'Active'):
            # If it is past its due date
            response = iam.list_user_tags(
              UserName = user['UserName'],
              MaxItems = 123
            )
            for tag in response['Tags']:
              if (tag['Key'] == 'email'):
                emailBool = True
                email = tag['Value']
            # Delete key if range
            if (active_days.days >= DELETE_DAY):
              print(f"Username: {user['UserName']} ({active_days.days}) is over {DELETE_DAY} days, deleting the access key.")
              delete_access_key(iam, AccessKeys['AccessKeyId'], user['UserName'])
            # Disable key
            elif (active_days.days >= DISABLE_DAY):
              print(f"Username: {user['UserName']} ({active_days.days}) is over {DISABLE_DAY} days, disabling access key & sending email.")
              disable_access_key(iam, AccessKeys['AccessKeyId'], user['UserName'])
              if (emailBool == True):
                send_disable_email(user['UserName'], email)
              else:
                print(f"[!] Email tag does not exist for: {user['UserName']}")
                send_email_dne(user['UserName'])
            # In the warning range
            elif (active_days.days >= WARN_DAYS):
              print(f"Username: {user['UserName']} ({active_days.days}) is over {WARN_DAYS} days, sending reminder email.")
              if (emailBool == True):
                sendWarningEmail(user['UserName'], email, active_days.days)
              else:
                print(f"[!] Email tag does not exist for: {user['UserName']}")
                send_email_dne(user['UserName'])

def delete_access_key(iam, keyID, username):
  iam.delete_access_key(
    AccessKeyId = keyID,
    UserName = username
  )

def disable_access_key(iam, keyID, username):
  iam.update_access_key(
    AccessKeyId = keyID,
    Status = 'Inactive',
    UserName = username
  )

def send_email_dne(username):
  ses = boto3.client('ses')
  bodyEmail = f"Could not find a email for the AWS user {username}"
  ses.send_email(
    Source = SOURCE_ADDRESS,
    Destination = {
      'ToAddresses': [
        SOURCE_ADDRESS,
      ]
    },
    Message = {
      'Subject': {
        'Data': 'Email Does Not Exist | AWS Access Key'
      },
      'Body': {
        'Html': {
          'Data': bodyEmail
        }
      }
    }
  )

def send_disable_email(username, email):
  ses = boto3.client('ses')
  bodyEmail = f"""Hello {username},<br><br>
    Your aws cli access key is outdated, and has been disabled.<br>
    Please login to aws and create a new access key.<br><br>
    Guide to update your access key<br>
    https://aws.amazon.com/blogs/security/how-to-find-update-access-keys-password-mfa-aws-management-console/<br>
    Linux / OSX cli tool<br>
    https://github.com/stefansundin/aws-rotate-key<br>
    """
  ses.send_email(
    Source = SOURCE_ADDRESS,
    Destination = {
      'ToAddresses': [
        email,
      ],
      'CcAddresses': [
        CC_EMAIL_ADDRESS,
      ]
    },
    Message = {
      'Subject': {
        'Data': 'Outdated AWS Access Key'
      },
      'Body': {
        'Html': {
          'Data': bodyEmail
        }
      }
    }
  )

def sendWarningEmail(username, email, days):
  ses = boto3.client('ses')
  bodyEmail = f"""Hello {username},<br><br>
    Your aws cli access key is reaching its expiration date.<br>
    Please replace your access key, it will be disabled in {DISABLE_DAY - days} days.<br><br>
    Guide to update your access key<br>
    https://aws.amazon.com/blogs/security/how-to-find-update-access-keys-password-mfa-aws-management-console/<br>
    Linux / OSX cli tool<br>
    https://github.com/stefansundin/aws-rotate-key<br>
    """
  ses.send_email(
    Source = SOURCE_ADDRESS,
    Destination = {
      'ToAddresses': [
        email,
      ]
    },
    Message = {
      'Subject': {
        'Data': 'Outdated AWS Access Key Warning'
      },
      'Body': {
        'Html': {
          'Data': bodyEmail
        }
      }
    }
  )

def lambda_handler(event, context):
  findOldAccessKeys()

IAM Permissions

Configure the following on the IAM role that the lambda assumes. Change the ARN and condition for the ses identity as well as the name of the function in the logging section (sid: VisualEditor3).

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": [
        "iam:DeleteAccessKey",
        "iam:GetAccessKeyLastUsed",
        "iam:GetUser",
        "iam:ListAccessKeys",
        "iam:ListAttachedGroupPolicies",
        "iam:ListGroupPolicies",
        "iam:ListGroupsForUser",
        "iam:ListUserTags",
        "iam:UpdateAccessKey"
      ],
      "Resource": [
        "arn:aws:iam::0123456789:group/*",
        "arn:aws:iam::0123456789:user/*"
      ]
    },
    {
      "Sid": "VisualEditor1",
      "Effect": "Allow",
      "Action": [
        "iam:ListUsers",
        "iam:ListGroups",
        "iam:GetAccountSummary"
      ],
      "Resource": "*"
    },
    {
      "Sid": "VisualEditor2",
      "Effect": "Allow",
      "Action": "ses:SendEmail",
      "Resource": "arn:aws:ses:us-east-1:0123456789:identity/[email protected]",
      "Condition": {
        "ForAllValues:StringLike": {
          "ses:Recipients": "*@example.com"
        }
      }
    },
    {
      "Sid": "VisualEditor1",
      "Effect": "Allow",
      "Action": "logs:CreateLogGroup",
      "Resource": "arn:aws:logs:us-east-1:0123456789:*"
    },
    {
      "Sid": "VisualEditor3",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:us-east-1:0123456789:log-group:/aws/lambda/AccessKeyUpdateReminder:*"
    }
  ]
}