IAM access key rotation is a critical security practice that limits the window of opportunity for attackers who may have obtained your credentials. Long-lived access keys are one of the most common causes of AWS security breaches, making regular rotation essential for maintaining a secure cloud environment.
This article is part of our comprehensive Cloud Security Tips for 2026 guide covering essential practices for protecting your cloud environment.
Why Access Key Rotation Matters
| Risk Factor | Impact |
|---|---|
| Keys leaked in code repositories | Attackers scan GitHub for exposed keys |
| Keys stolen from developer laptops | Malware targets AWS credentials |
| Keys shared via insecure channels | Email and chat logs expose credentials |
| Former employee access | Unrevoiced keys enable unauthorized access |
| Third-party compromise | Vendor breaches expose shared credentials |
Step 1: Identify Existing Access Keys
Generate Credential Report
# Generate the credential report
aws iam generate-credential-report
# Wait for completion (usually a few seconds)
sleep 5
# Download and decode the report
aws iam get-credential-report \
--query 'Content' \
--output text | base64 -d > credential-report.csv
# View key information
cat credential-report.csv | cut -d',' -f1,9,10,11,14,15,16Find Keys Older Than 90 Days
# List all access keys with creation dates
aws iam list-users --query 'Users[].UserName' --output text | \
while read user; do
echo "User: $user"
aws iam list-access-keys --user-name "$user" \
--query 'AccessKeyMetadata[*].[AccessKeyId,CreateDate,Status]' \
--output table
done
# Find keys older than 90 days using Python
python3 << 'EOF'
import boto3
from datetime import datetime, timezone
iam = boto3.client('iam')
threshold_days = 90
for user in iam.list_users()['Users']:
username = user['UserName']
for key in iam.list_access_keys(UserName=username)['AccessKeyMetadata']:
age = (datetime.now(timezone.utc) - key['CreateDate']).days
if age > threshold_days:
print(f"STALE: {username} - {key['AccessKeyId']} - {age} days old")
EOFStep 2: Manual Key Rotation
Using AWS Console
- Open the IAM Console
- Navigate to Users and select the user
- Click Security credentials tab
- In Access keys section, click Create access key
- Select the use case and create the new key
- Download or copy the new credentials
- Update your application with the new key
- Test the application thoroughly
- Return to console and Deactivate the old key
- After confirming everything works, Delete the old key
Using AWS CLI
# Step 1: Create new access key
aws iam create-access-key --user-name my-service-account
# Output:
# {
# "AccessKey": {
# "UserName": "my-service-account",
# "AccessKeyId": "AKIAIOSFODNN7NEWKEY",
# "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYNEWKEY",
# "Status": "Active",
# "CreateDate": "2026-01-13T12:00:00Z"
# }
# }
# Step 2: Update application configuration with new key
# (Deploy the new credentials to your application)
# Step 3: Test that application works with new key
# (Verify functionality before proceeding)
# Step 4: Deactivate old key
aws iam update-access-key \
--user-name my-service-account \
--access-key-id AKIAIOSFODNN7OLDKEY \
--status Inactive
# Step 5: Monitor for any issues using the inactive key
# Wait 24-48 hours to ensure no systems depend on old key
# Step 6: Delete old key
aws iam delete-access-key \
--user-name my-service-account \
--access-key-id AKIAIOSFODNN7OLDKEYStep 3: Automated Key Rotation with Secrets Manager
For service accounts, use AWS Secrets Manager with automatic rotation:
Create Secret with Rotation
# Create secret for IAM user credentials
aws secretsmanager create-secret \
--name prod/myapp/service-account \
--description "Service account credentials for MyApp" \
--secret-string '{
"username": "myapp-service",
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}'
# Enable automatic rotation
aws secretsmanager rotate-secret \
--secret-id prod/myapp/service-account \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRotation \
--rotation-rules AutomaticallyAfterDays=90Rotation Lambda Function
import boto3
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
iam = boto3.client('iam')
secrets = boto3.client('secretsmanager')
def lambda_handler(event, context):
secret_id = event['SecretId']
token = event['ClientRequestToken']
step = event['Step']
if step == "createSecret":
create_secret(secret_id, token)
elif step == "setSecret":
set_secret(secret_id, token)
elif step == "testSecret":
test_secret(secret_id, token)
elif step == "finishSecret":
finish_secret(secret_id, token)
def create_secret(secret_id, token):
# Get current secret
current = secrets.get_secret_value(
SecretId=secret_id,
VersionStage="AWSCURRENT"
)
current_secret = json.loads(current['SecretString'])
username = current_secret['username']
# Create new access key
new_key = iam.create_access_key(UserName=username)['AccessKey']
# Store new credentials as AWSPENDING
new_secret = {
'username': username,
'accessKeyId': new_key['AccessKeyId'],
'secretAccessKey': new_key['SecretAccessKey']
}
secrets.put_secret_value(
SecretId=secret_id,
ClientRequestToken=token,
SecretString=json.dumps(new_secret),
VersionStages=['AWSPENDING']
)
def test_secret(secret_id, token):
# Get pending secret
pending = secrets.get_secret_value(
SecretId=secret_id,
VersionStage="AWSPENDING",
VersionId=token
)
pending_secret = json.loads(pending['SecretString'])
# Test the new credentials
test_client = boto3.client(
'sts',
aws_access_key_id=pending_secret['accessKeyId'],
aws_secret_access_key=pending_secret['secretAccessKey']
)
test_client.get_caller_identity()
logger.info("New credentials tested successfully")
def finish_secret(secret_id, token):
# Get current and pending secrets
current = secrets.get_secret_value(
SecretId=secret_id,
VersionStage="AWSCURRENT"
)
current_secret = json.loads(current['SecretString'])
# Move AWSPENDING to AWSCURRENT
secrets.update_secret_version_stage(
SecretId=secret_id,
VersionStage="AWSCURRENT",
MoveToVersionId=token,
RemoveFromVersionId=current['VersionId']
)
# Delete old access key
iam.delete_access_key(
UserName=current_secret['username'],
AccessKeyId=current_secret['accessKeyId']
)
logger.info("Old access key deleted")Step 4: Monitor Key Age with AWS Config
# Create AWS Config rule for access key age
aws configservice put-config-rule \
--config-rule '{
"ConfigRuleName": "access-keys-rotated",
"Description": "Checks whether active IAM access keys are rotated within 90 days",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "ACCESS_KEYS_ROTATED"
},
"InputParameters": "{\"maxAccessKeyAge\":\"90\"}",
"MaximumExecutionFrequency": "TwentyFour_Hours"
}'
# Check compliance status
aws configservice get-compliance-details-by-config-rule \
--config-rule-name access-keys-rotated \
--compliance-types NON_COMPLIANTTerraform Configuration
resource "aws_config_config_rule" "access_keys_rotated" {
name = "access-keys-rotated"
source {
owner = "AWS"
source_identifier = "ACCESS_KEYS_ROTATED"
}
input_parameters = jsonencode({
maxAccessKeyAge = "90"
})
maximum_execution_frequency = "TwentyFour_Hours"
}
# CloudWatch alarm for non-compliant keys
resource "aws_cloudwatch_metric_alarm" "stale_keys" {
alarm_name = "stale-access-keys"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "NonCompliantResourceCount"
namespace = "AWS/Config"
period = 86400
statistic = "Maximum"
threshold = 0
alarm_description = "Alert when access keys are not rotated"
alarm_actions = [aws_sns_topic.security_alerts.arn]
dimensions = {
ConfigRuleName = aws_config_config_rule.access_keys_rotated.name
}
}Step 5: Alert on Old Keys
#!/bin/bash
# Script to check and alert on stale access keys
MAX_AGE_DAYS=90
ALERT_EMAIL="[email protected]"
echo "Checking for access keys older than $MAX_AGE_DAYS days..."
# Generate fresh credential report
aws iam generate-credential-report > /dev/null
sleep 5
# Parse and check key ages
aws iam get-credential-report --query 'Content' --output text | base64 -d | \
tail -n +2 | while IFS=',' read -r user arn creation pw_enabled pw_last_used \
pw_last_changed pw_next mfa key1_active key1_last_rotated key1_last_used \
key1_last_region key2_active key2_last_rotated key2_last_used key2_last_region \
cert1 cert2; do
# Check key 1
if [ "$key1_active" = "true" ] && [ "$key1_last_rotated" != "N/A" ]; then
key1_age=$(( ($(date +%s) - $(date -d "$key1_last_rotated" +%s)) / 86400 ))
if [ "$key1_age" -gt "$MAX_AGE_DAYS" ]; then
echo "ALERT: $user has key older than $MAX_AGE_DAYS days (Key 1: $key1_age days)"
fi
fi
# Check key 2
if [ "$key2_active" = "true" ] && [ "$key2_last_rotated" != "N/A" ]; then
key2_age=$(( ($(date +%s) - $(date -d "$key2_last_rotated" +%s)) / 86400 ))
if [ "$key2_age" -gt "$MAX_AGE_DAYS" ]; then
echo "ALERT: $user has key older than $MAX_AGE_DAYS days (Key 2: $key2_age days)"
fi
fi
doneStep 6: Disable Unused Keys
# Find and disable keys not used in 90 days
python3 << 'EOF'
import boto3
from datetime import datetime, timezone, timedelta
iam = boto3.client('iam')
threshold = datetime.now(timezone.utc) - timedelta(days=90)
for user in iam.list_users()['Users']:
username = user['UserName']
for key in iam.list_access_keys(UserName=username)['AccessKeyMetadata']:
if key['Status'] != 'Active':
continue
key_id = key['AccessKeyId']
# Get last used info
last_used = iam.get_access_key_last_used(AccessKeyId=key_id)
last_used_date = last_used.get('AccessKeyLastUsed', {}).get('LastUsedDate')
if last_used_date is None:
# Never used - check creation date
if key['CreateDate'] < threshold:
print(f"Disabling never-used key: {username} - {key_id}")
iam.update_access_key(
UserName=username,
AccessKeyId=key_id,
Status='Inactive'
)
elif last_used_date < threshold:
print(f"Disabling unused key: {username} - {key_id}")
iam.update_access_key(
UserName=username,
AccessKeyId=key_id,
Status='Inactive'
)
EOFMigrate from Access Keys to IAM Roles
Where possible, replace access keys with IAM roles:
| Use Case | Recommended Approach |
|---|---|
| EC2 instances | Use instance profile with IAM role |
| Lambda functions | Execution role (automatically configured) |
| ECS tasks | Task IAM role |
| EKS pods | IRSA (IAM Roles for Service Accounts) |
| Cross-account access | Assume role with external ID |
| Human CLI access | IAM Identity Center (SSO) |
| CI/CD pipelines | OIDC identity provider |
# Example: GitHub Actions with OIDC (no access keys needed)
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["1c58a3a8518e8759bf075b76b750d4f2df264fcd"]
}
resource "aws_iam_role" "github_actions" {
name = "github-actions-deploy"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:myorg/myrepo:*"
}
}
}]
})
}Best Practices Summary
| Practice | Recommendation |
|---|---|
| Rotation Frequency | Every 90 days maximum |
| Prefer IAM Roles | Use roles instead of keys when possible |
| Automated Rotation | Use Secrets Manager for service accounts |
| Monitoring | AWS Config rule + CloudWatch alarms |
| Unused Keys | Disable after 90 days of no use |
| Root Keys | Never create access keys for root account |