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.
Environment
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
https://github.com/awstut-an-r/awstut-soa/tree/main/01/002
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
Resources:
ConfigRule:
Type: AWS::Config::ConfigRule
DependsOn:
- ConfigurationRecorder
Properties:
ConfigRuleName: !Sub "${Prefix}-IAM-Access-Keys-Rotated"
InputParameters:
maxAccessKeyAge: !Ref MaxCredentialUsageAge
Source:
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.
https://docs.aws.amazon.com/config/latest/developerguide/access-keys-rotated.html
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
Resources:
RemediationConfiguration:
Type: AWS::Config::RemediationConfiguration
Properties:
Automatic: true
ConfigRuleName: !Ref ConfigRule
MaximumAutomaticAttempts: 5
Parameters:
AutomationAssumeRole:
StaticValue:
Values:
- !GetAtt RemediationConfigurationRole.Arn
IAMResourceId:
ResourceValue:
Value: RESOURCE_ID
MaxCredentialUsageAge:
StaticValue:
Values:
- !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.
AWSConfigRemediation-RevokeUnusedIAMUserCredentials
Specify the book name in the TargetId property.
The second parameter is for this book.
This book takes the following three parameters
AutomationAssumeRole
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.IAMResourceId
Type: String
Description: (Required) The ID of the IAM resource you want to revoke unused credentials from.MaxCredentialUsageAge
AWSConfigRemediation-RevokeUnusedIAMUserCredentials
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.
Resources:
RemediationConfigurationRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service:
- ssm.amazonaws.com
Policies:
- PolicyName: RemediationConfigurationPolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 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.
Architecting
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.
The rule has been successfully created.
In addition, a remediation action has been created.
Review audit results.
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.
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.
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.
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](https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccessKey.html) and delete expired login profiles by using the [DeleteLoginProfile API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteLoginProfile.html). 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 }}"
parameters:
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+=,.@_\/-]+|^$'
IAMResourceId:
type: String
description: (Required) IAM resource unique identifier.
allowedPattern: ^[\w+=,.@_-]{1,128}$
MaxCredentialUsageAge:
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"
outputs:
- RevokeUnusedIAMUserCredentialsAndVerify.Output
mainSteps:
- 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.
inputs:
Runtime: python3.6
Handler: unused_iam_credentials_handler
InputPayload:
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 = (datetime.now() - last_used_date).days
if last_used_days >= max_credential_usage_age:
deactivate_key(user_name, key.get("AccessKeyId"))
else:
create_date = key.get("CreateDate").replace(tzinfo=None)
days_since_creation = (datetime.now() - create_date).days
if days_since_creation >= max_credential_usage_age:
deactivate_key(user_name, key.get("AccessKeyId"))
def get_login_profile(user_name):
try:
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 = (datetime.now() - 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 = (datetime.now() - 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"):
try:
iam_client.get_login_profile(UserName=user_name)
error_message = "VERIFICATION FAILED. IAM USER {} LOGIN PROFILE NOT DELETED".format(user_name)
raise Exception(error_message)
except iam_client.exceptions.NoSuchEntityException:
pass
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(
resourceType='AWS::IAM::User',
resourceIds=[resource_id]
)
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)
outputs:
- 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.
Summary
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.