How to Rotate IAM Access Keys in AWS

Complete guide to AWS IAM access key rotation including manual rotation steps, automated rotation with Lambda, detecting stale keys, and implementing organization-wide key rotation policies.

9 min readUpdated 2026-01-13

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 FactorImpact
Keys leaked in code repositoriesAttackers scan GitHub for exposed keys
Keys stolen from developer laptopsMalware targets AWS credentials
Keys shared via insecure channelsEmail and chat logs expose credentials
Former employee accessUnrevoiced keys enable unauthorized access
Third-party compromiseVendor 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,16

Find 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")
EOF

Step 2: Manual Key Rotation

Using AWS Console

  1. Open the IAM Console
  2. Navigate to Users and select the user
  3. Click Security credentials tab
  4. In Access keys section, click Create access key
  5. Select the use case and create the new key
  6. Download or copy the new credentials
  7. Update your application with the new key
  8. Test the application thoroughly
  9. Return to console and Deactivate the old key
  10. 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 AKIAIOSFODNN7OLDKEY

Step 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=90

Rotation 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_COMPLIANT

Terraform 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
done

Step 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'
            )
EOF

Migrate from Access Keys to IAM Roles

Where possible, replace access keys with IAM roles:

Use CaseRecommended Approach
EC2 instancesUse instance profile with IAM role
Lambda functionsExecution role (automatically configured)
ECS tasksTask IAM role
EKS podsIRSA (IAM Roles for Service Accounts)
Cross-account accessAssume role with external ID
Human CLI accessIAM Identity Center (SSO)
CI/CD pipelinesOIDC 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

PracticeRecommendation
Rotation FrequencyEvery 90 days maximum
Prefer IAM RolesUse roles instead of keys when possible
Automated RotationUse Secrets Manager for service accounts
MonitoringAWS Config rule + CloudWatch alarms
Unused KeysDisable after 90 days of no use
Root KeysNever create access keys for root account

Frequently Asked Questions

Find answers to common questions

AWS recommends rotating access keys every 90 days as a security best practice. Many compliance frameworks including PCI DSS and HIPAA require rotation periods between 90-180 days. For highly sensitive workloads, consider 30-60 day rotation cycles. Implement automated rotation to ensure consistent adherence to your rotation policy. Monitor key age using AWS Config rules and generate alerts for keys approaching rotation deadlines.

Need Professional Help?

Our team of experts can help you implement and configure these solutions for your organization.