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.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
{
  "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:*"
    }
  ]
}