Access keys are one of the most common ways AWS credentials leak — hardcoded in a Git repo, baked into an AMI, left in a CI/CD log, or forgotten on a former employee's laptop. If you never rotate AWS IAM access keys, a single leaked key can stay valid indefinitely, giving an attacker a long, quiet window to explore your account. Rotating keys on a schedule shrinks that window to nearly nothing, even if you never notice the leak yourself.
This guide walks through a safe, zero-downtime process to rotate AWS IAM access keys for a user or service, using the AWS CLI and console. By the end, you'll have a new active key, a deactivated old one, and a repeatable process you can turn into a standing iam credential rotation policy instead of a one-off fire drill.
Step 1: Audit Existing Keys Before You Rotate AWS IAM Access Keys
Before touching anything, get a full inventory of who has keys and how old they are. AWS IAM lets you list access key age directly:
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,Status,CreateDate]' \
--output table
done
For a broader view across the account, generate an IAM credential report:
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 --decode > credential-report.csv
Open the CSV and look at access_key_1_last_rotated and access_key_2_last_rotated. Any key older than 90 days should be treated as a priority. This audit step is also where most teams discover keys attached to service accounts nobody remembers creating — a good reason to run it quarterly even outside a rotation event.
Step 2: Create a New Access Key Alongside the Old One
AWS allows up to two active access keys per IAM user specifically so you can rotate without downtime. Create the second key before disabling the first:
aws iam create-access-key --user-name deploy-service
The response includes the new AccessKeyId and SecretAccessKey — this is the only time the secret is shown, so store it immediately in your secrets manager (AWS Secrets Manager, HashiCorp Vault, or equivalent). Never paste it into Slack, a ticket, or a plaintext file that outlives the terminal session.
Step 3: Update Applications and Services With the New Key
Replace the old key everywhere it's referenced. Common locations include:
- Environment variables in CI/CD pipelines (GitHub Actions secrets, GitLab CI variables, Jenkins credentials store)
~/.aws/credentialsprofiles on developer machines and build servers- Secrets Manager or Parameter Store entries consumed by Lambda, ECS, or EC2
- Third-party SaaS integrations that pull from AWS on your behalf
# ~/.aws/credentials
[deploy-service]
aws_access_key_id = AKIA...NEWKEY
aws_secret_access_key = ...
If you're managing infrastructure as code, update the underlying secret reference rather than hardcoding the key in Terraform or CloudFormation — the goal of aws access key rotation is to make this update mechanical, not a manual find-and-replace across every repo.
Step 4: Verify the New Key Is Active and Working
Before disabling the old key, confirm the new one actually works in every consuming system:
AWS_ACCESS_KEY_ID=AKIA...NEWKEY \
AWS_SECRET_ACCESS_KEY=... \
aws sts get-caller-identity
A successful response returns the account ID, user ARN, and user ID — proof the credentials are valid and correctly scoped. Run this check from each environment (local, staging, CI) that uses the key, not just from your laptop. It's common for a key to work locally but fail in a pipeline because a secret reference wasn't actually updated.
Step 5: Deactivate the Old Access Key
Once every consumer confirms success with the new key, deactivate — don't yet delete — the old one:
aws iam update-access-key \
--access-key-id AKIA...OLDKEY \
--status Inactive \
--user-name deploy-service
Deactivating instead of deleting gives you an instant rollback path. If something breaks in a system you missed during Step 3, you can flip the status back to Active in seconds:
aws iam update-access-key --access-key-id AKIA...OLDKEY --status Active --user-name deploy-service
Let the deactivated key sit for 24-48 hours while monitoring CloudTrail for any AccessDenied errors tied to it — that's your signal something still depends on the old credential.
Step 6: Delete the Old Key and Confirm Rotation Complete
If nothing breaks during the monitoring window, delete the old key permanently:
aws iam delete-access-key --access-key-id AKIA...OLDKEY --user-name deploy-service
Re-run the credential report from Step 1 to confirm only the new key remains and its age has reset to zero. Document the rotation — who did it, when, and why — in whatever change log or ticketing system your team uses for audit purposes.
Step 7: Turn This Into a Recurring IAM Credential Rotation Policy
A single rotation is a good start; a policy is what actually protects you long-term. Codify the cadence rather than relying on memory:
- Set a maximum key age (90 days is a common baseline) and enforce it with an IAM policy condition or a scheduled Lambda that flags stale keys
- Use AWS Config rule
access-keys-rotatedto automatically detect keys past their allowed age - Prefer IAM roles and temporary credentials (via STS
AssumeRole) over long-lived access keys wherever a workload supports it — this is one of the highest-leverage aws security best practices available, since a role's credentials expire automatically and never need manual rotation - For human users, consider eliminating standing access keys entirely in favor of federated SSO, reserving keys for service accounts that genuinely require them
aws configservice put-config-rule --config-rule '{
"ConfigRuleName": "access-keys-rotated",
"Source": {"Owner": "AWS", "SourceIdentifier": "ACCESS_KEYS_ROTATED"},
"InputParameters": "{\"maxAccessKeyAge\":\"90\"}"
}'
Troubleshooting and Verification
"An error occurred (LimitExceeded)" when creating a new key — IAM caps users at two access keys. Delete or deactivate an existing inactive key before creating another.
Applications still failing after the swap — check for cached credentials. Some SDKs and CLI tools cache STS or credential-file reads; restart the process or clear the local credential cache after updating environment variables.
CloudTrail shows continued use of a deactivated key — this usually means a hardcoded reference was missed, often in a Docker image baked with the old key or a scheduled job pulling from an outdated Parameter Store version. Search build artifacts and container images, not just live config.
Unsure which services actually use a given key — cross-reference aws cloudtrail lookup-events --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIA... over the prior 30 days to see every API call made with that key and which source IPs or user agents were involved.
Rotation keeps getting skipped — if this is a recurring problem, the process is too manual. Move enforcement into infrastructure (Config rules, SCPs, or automated tooling) rather than a calendar reminder.
How Safeguard Helps
Manually tracking key age across dozens of IAM users and service accounts doesn't scale, and it's exactly the kind of control that quietly lapses under deadline pressure. Safeguard continuously monitors your AWS environment for stale, overprivileged, and unrotated access keys, and correlates them against real usage from CloudTrail so you know which keys are safe to retire and which are still load-bearing before you touch them.
Instead of running the audit and verification steps above by hand every quarter, Safeguard surfaces keys approaching your rotation threshold, flags any that show signs of exposure (public repos, leaked secrets scans, anomalous usage patterns), and gives your team a single view to enforce an iam credential rotation policy consistently across accounts — not just the ones someone remembered to check. For software supply chain security teams, that means fewer long-lived credentials sitting around as an attack surface, and audit-ready evidence that rotation actually happened on schedule, not just that it was supposed to.