Use AWS Config to detect old access keys and disable them with SSM runbook

Use AWS Config to detect old access keys and disable them with SSM runbook. SOA_EN

Use AWS Config to detect old access keys and disable them with SSM runbook

One of the AWS SOA topics is on monitoring, logging, and remediation.

On the following page, we showed you how to use AWS Config to detect old access keys.

In this page, we will see how to use the SSM runbook to disable old access keys that are detected automatically, as this is a development of the SSM runbook.


Diagram of using AWS Config to detect old access keys and disable them with SSM runbook.

The structure is generally the same as the aforementioned page.

The difference is that the remediation action is configured.
When an old access key is detected by an AWS Config rule and determined to be non-compliant, it is automatically disabled using the SSM runbook.

CloudFormation template files

The above configuration is built with CloudFormation.
The CloudFormation templates are placed at the following URL

awstut-soa/01/002 at main · awstut-an-r/awstut-soa
Contribute to awstut-an-r/awstut-soa development by creating an account on GitHub.

Explanation of key points of template files

This page focuses on how to automatically disable old access keys detected by AWS Config using SSM runbook.

For information on how to use AWS Config to detect outdated access keys, please see the following page.

AWS Config Rule

    Type: AWS::Config::ConfigRule
      - ConfigurationRecorder
      ConfigRuleName: !Sub "${Prefix}-IAM-Access-Keys-Rotated"
        maxAccessKeyAge: !Ref MaxCredentialUsageAge
        Owner: AWS
        SourceIdentifier: ACCESS_KEYS_ROTATED
Code language: YAML (yaml)

The managed rule access-key-rotated can be used to detect old access keys that have not been rotated.

access-keys-rotated - AWS Config
Checks whether the active access keys are rotated within the number of days specified in maxAccessKeyAge. The rule is non-compliant if the access keys have not ...

Specify “ACCESS_KEYS_ROTATED” for the SourceIdentifier property.
Specify “90” for the maxAccessKeyAge property to make access keys (IAM users) past 90 days a non-compliant resource.

AWS Config Remediation Action

    Type: AWS::Config::RemediationConfiguration
      Automatic: true
      ConfigRuleName: !Ref ConfigRule
      MaximumAutomaticAttempts: 5
              - !GetAtt RemediationConfigurationRole.Arn
            Value: RESOURCE_ID
              - !Ref MaxCredentialUsageAge
      RetryAttemptSeconds: 60
      TargetId: AWSConfigRemediation-RevokeUnusedIAMUserCredentials
      TargetType: SSM_DOCUMENT
      TargetVersion: "1"
Code language: YAML (yaml)

For basic information on remediation actions, please see the following pages.

There are two points.

The first point is the SSM runbook to be used.
For this requirement, we can use AWSConfigRemediation-RevokeUnusedIAMUserCredentials.

The AWSConfigRemediation-RevokeUnusedIAMUserCredentials runbook revokes unused AWS Identity and Access Management (IAM) passwords and active access keys. This runbook also deactivates expired access keys, and deletes expired login profiles.


Specify the book name in the TargetId property.
The second parameter is for this book.
This book takes the following three parameters

Type: String
Description: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager
Automation to perform the actions on your behalf.

Type: String
Description: (Required) The ID of the IAM resource you want to revoke unused credentials from.

Type: String
Default: 90
Description: (Required) The number of days within which the credential must have been used.


IAMResourceId is a dynamic value that varies for each discovered resource.
For dynamic values, use the ResourceValue property and specify “RESOURCE_ID” for the internal Value property.

Specify “90” for MaxCredentialUsageAge.
This will cause access keys that have not been used for more than 90 days to be disabled.

Specify the following IAM role for AutomationAssumeRole.

    Type: AWS::IAM::Role
    DeletionPolicy: Delete
        Version: 2012-10-17
          - Effect: Allow
            Action: sts:AssumeRole
        - PolicyName: RemediationConfigurationPolicy
            Version: 2012-10-17
              - Effect: Allow
                  - ssm:StartAutomationExecution
                  - ssm:GetAutomationExecution
                  - config:ListDiscoveredResources
                  - iam:DeleteAccessKey
                  - iam:DeleteLoginProfile
                  - iam:GetAccessKeyLastUsed
                  - iam:GetLoginProfile
                  - iam:GetUser
                  - iam:ListAccessKeys
                  - iam:UpdateAccessKey
                Resource: "*"
Code language: YAML (yaml)

The authorizations required to run this book are also summarized on the aforementioned page.
Total authorization for 10 actions.


Use CloudFormation to build this environment and check its actual behavior.

Create CloudFormation stacks and check the resources in the stacks

Create CloudFormation stacks.
For information on how to create stacks and check each stack, please see the following page.

After reviewing the resources in each stack, information on the main resources created in this case is as follows

  • AWS Config rule: soa-01-002-IAM-Access-Keys-Rotated

Check each resource from the AWS Management Console.

Check the AWS Config rule.

Detail of AWS Config 1.

The rule has been successfully created.
In addition, a remediation action has been created.

Review audit results.

Detail of AWS Config 2.

Two IAM users were determined to be non-compliant.

The Status column shows that the remediation action was performed.

Check the status of each user’s access key.

Detail of IAM 1.
Detail of IAM 2.

Each has been in place for over 90 days since creation, but the results are split.
One is disabled, the other remains enabled.

Check the SSM Automation logs executed for each user.

First, check the execution log for the user whose access key has been disabled.

Detail of SSM 1.

The Outputs shows that the deactivation process was indeed executed for the access key associated with this user.

On the other hand, check the execution log for users whose access keys remain valid.

Detail of SSM 2.

The Outputs shows “successful” but no access key is selected and no processing is being performed.

This is due to the SSM runbook AWSConfigRemediation-RevokeUnusedIAMUserCredentials specification.
As the name suggests, this book targets unused credentials (e.g. access keys).
This means that access keys that have been used recently, even if they exceeded the AWS Config rule due date, will not be subject to remediation actions by this runbook.

This can be determined from the AWSConfigRemediation-RevokeUnusedIAMUserCredentials code.
The following code is from this book.

schemaVersion: "0.3"
description: |
   ### Document Name - AWSConfigRemediation-RevokeUnusedIAMUserCredentials

   ## What does this document do?
   This document revokes unused IAM passwords and active access keys. This document will deactivate expired access keys by using the [UpdateAccessKey API]( and delete expired login profiles by using the [DeleteLoginProfile API]( Please note, this automation document requires AWS Config to be enabled.

   ## Input Parameters
   * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.
   * IAMResourceId: (Required) IAM resource unique identifier.
   * MaxCredentialUsageAge: (Required) Maximum number of days within which a credential must be used. The default value is 90 days.

   ## Output Parameters
   * RevokeUnusedIAMUserCredentialsAndVerify.Output - Success message or failure Exception.

assumeRole: "{{ AutomationAssumeRole }}"
    type: String
    description: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.
    allowedPattern: '^arn:aws(-cn|-us-gov)?:iam::\d{12}:role\/[\w+=,.@_\/-]+|^$'
    type: String
    description: (Required) IAM resource unique identifier.
    allowedPattern: ^[\w+=,.@_-]{1,128}$
    type: String
    description: (Required) Maximum number of days within which a credential must be used. The default value is 90 days.
    allowedPattern: ^(\b([0-9]|[1-8][0-9]|9[0-9]|[1-8][0-9]{2}|9[0-8][0-9]|99[0-9]|[1-8][0-9]{3}|9[0-8][0-9]{2}|99[0-8][0-9]|999[0-9]|10000)\b)$
    default: "90"
  - RevokeUnusedIAMUserCredentialsAndVerify.Output
  - name: RevokeUnusedIAMUserCredentialsAndVerify
    action: aws:executeScript
    timeoutSeconds: 600
    isEnd: true
    description: |
      ## RevokeUnusedIAMUserCredentialsAndVerify
      This step deactivates expired IAM User access keys, deletes expired login profiles and verifies credentials were revoked
      ## Outputs
      * Output: Success message or failure Exception.
      Runtime: python3.6
      Handler: unused_iam_credentials_handler
        IAMResourceId: "{{ IAMResourceId }}"
        MaxCredentialUsageAge: "{{ MaxCredentialUsageAge }}"
      Script: |-
        import boto3
        from datetime import datetime
        from datetime import timedelta

        iam_client = boto3.client("iam")
        config_client = boto3.client("config")

        responses = {}
        responses["DeactivateUnusedKeysResponse"] = []

        def list_access_keys(user_name):
          return iam_client.list_access_keys(UserName=user_name).get("AccessKeyMetadata")

        def deactivate_key(user_name, access_key):
          responses["DeactivateUnusedKeysResponse"].append({"AccessKeyId": access_key, "Response": iam_client.update_access_key(UserName=user_name, AccessKeyId=access_key, Status="Inactive")})

        def deactivate_unused_keys(access_keys, max_credential_usage_age, user_name):
          for key in access_keys:
            last_used = iam_client.get_access_key_last_used(AccessKeyId=key.get("AccessKeyId")).get("AccessKeyLastUsed")
            if last_used.get("LastUsedDate"):
              last_used_date = last_used.get("LastUsedDate").replace(tzinfo=None)
              last_used_days = ( - last_used_date).days
              if last_used_days >= max_credential_usage_age:
                deactivate_key(user_name, key.get("AccessKeyId"))
              create_date = key.get("CreateDate").replace(tzinfo=None)
              days_since_creation = ( - create_date).days
              if days_since_creation >= max_credential_usage_age:
                deactivate_key(user_name, key.get("AccessKeyId"))

        def get_login_profile(user_name):
            return iam_client.get_login_profile(UserName=user_name)["LoginProfile"]
          except iam_client.exceptions.NoSuchEntityException:
            return False

        def delete_unused_password(user_name, max_credential_usage_age):
          user = iam_client.get_user(UserName=user_name).get("User")
          password_last_used_days = 0
          login_profile = get_login_profile(user_name)
          if login_profile and user.get("PasswordLastUsed"):
            password_last_used = user.get("PasswordLastUsed").replace(tzinfo=None)
            password_last_used_days = ( - password_last_used).days
          elif login_profile and not user.get("PasswordLastUsed"):
            password_creation_date = login_profile.get("CreateDate").replace(tzinfo=None)
            password_last_used_days = ( - password_creation_date).days
          if password_last_used_days >= max_credential_usage_age:
            responses["DeleteUnusedPasswordResponse"] = iam_client.delete_login_profile(UserName=user_name)

        def verify_expired_credentials_revoked(responses, user_name):
          if responses.get("DeactivateUnusedKeysResponse"):
            for key in responses.get("DeactivateUnusedKeysResponse"):
              key_data = next(filter(lambda x: x.get("AccessKeyId") == key.get("AccessKeyId"), list_access_keys(user_name)))
              if key_data.get("Status") != "Inactive":
                error_message = "VERIFICATION FAILED. ACCESS KEY {} NOT DEACTIVATED".format(key_data.get("AccessKeyId"))
                raise Exception(error_message)
          if responses.get("DeleteUnusedPasswordResponse"):
              error_message = "VERIFICATION FAILED. IAM USER {} LOGIN PROFILE NOT DELETED".format(user_name)
              raise Exception(error_message)
            except iam_client.exceptions.NoSuchEntityException:
          return {
              "output": "Verification of unused IAM User credentials is successful.",
              "http_responses": responses

        def get_user_name(resource_id):
          list_discovered_resources_response = config_client.list_discovered_resources(
          resource_name = list_discovered_resources_response.get("resourceIdentifiers")[0].get("resourceName")
          return resource_name

        def unused_iam_credentials_handler(event, context):
          iam_resource_id = event.get("IAMResourceId")
          user_name = get_user_name(iam_resource_id)
          max_credential_usage_age = int(event.get("MaxCredentialUsageAge"))
          access_keys = list_access_keys(user_name)
          unused_keys = deactivate_unused_keys(access_keys, max_credential_usage_age, user_name)

          delete_unused_password(user_name, max_credential_usage_age)

          return verify_expired_credentials_revoked(responses, user_name)
      - Name: Output
        Selector: $.Payload
        Type: StringMap
Code language: PHP (php)

The deactivate_unused_keys function implements processing related to access key deactivation.
The specification is to disable access keys only when the number of days from the date of the last access key use to today exceeds the threshold value.

Check both access keys again.
The first IAM user’s access key has been disabled because it has been more than 90 days since it was created and has never been used.
The access key for the second IAM user was not deactivated because it has been more than 90 days since it was created but has been used today.


We have identified how to use AWS Config to disable old access keys using SSM runbook once they are detected.
We also checked the behavior of AWSConfigRemediation-RevokeUnusedIAMUserCredentials.