To catch expiring IAM access keys, list every key with its creation date, flag keys older than 90 days, and check last-used dates before rotating. The Bash audit script below does all three in one read-only pass.
Stale access keys are how quiet breaches happen. A key created for a migration in 2024 still works in 2026 if nobody rotates it, and the owner left the team two reorgs ago. AWS gives you the metadata to find these keys; few teams check it on a schedule.
This guide shows a read-only Bash audit that lists all IAM keys, flags old ones, and surfaces never-used keys for removal. It changes nothing in your account. Rotation itself is a separate, deliberate step.
Quick Answer: How Do You Find Expiring and Stale IAM Access Keys?
Run aws iam list-access-keys to get each key’s creation date and status, then aws iam get-access-key-last-used --access-key-id AKIA... to see when it was last active. The script in the audit script section loops over all users, flags keys older than 90 days, and highlights keys with no recorded use.
What the AWS Commands Return
Two documented IAM operations carry the whole audit. list-access-keys returns AccessKeyMetadata entries with UserName, AccessKeyId, Status (Active, Inactive, or Expired), and CreateDate. get-access-key-last-used returns LastUsedDate, ServiceName, and Region for one key. Note the documented edge cases: LastUsedDate is null when a key exists but has never been used since tracking began, and ServiceName reads “N/A” in the same situation. A null date means unused, not broken.
Prerequisites
- AWS CLI v2 installed and configured with a profile that can read IAM. Verify with
aws sts get-caller-identity. - IAM read permissions:
iam:ListUsers,iam:ListAccessKeys,iam:GetAccessKeyLastUsed. No write permissions are needed for the audit. - Bash on Linux or macOS. The script uses
date -d(GNU); macOS needs coreutils or the adjustment noted below.
The Read-Only Audit Script
This script only calls list and get operations. It creates, modifies, and deletes nothing. Syntax-checked with bash -n; AWS responses below follow the documented shapes from the CLI reference (checked 2026-09-20, CLI 2.36.x). Run it against your own account; do not run write operations to “test” this article.
#!/usr/bin/env bash
# iam-key-audit.sh: read-only report of IAM access keys by age and use.
# Usage: ./iam-key-audit.sh [max_age_days] [profile]
set -euo pipefail
MAX_AGE_DAYS="${1:-90}"
PROFILE="${2:-default}"
NOW_EPOCH=$(date +%s)
users=$(aws iam list-users --query 'Users[].UserName'
--output text --profile "$PROFILE")
printf '%-25s %-22s %-10s %-8s %sn' "USER" "ACCESS_KEY_ID" "AGE_DAYS" "STATUS" "LAST_USED"
for user in $users; do
keys=$(aws iam list-access-keys --user-name "$user"
--query 'AccessKeyMetadata[].[AccessKeyId,Status,CreateDate]'
--output text --profile "$PROFILE")
[ -z "$keys" ] && continue
echo "$keys" | while read -r key_id status created; do
created_epoch=$(date -d "$created" +%s)
age_days=$(( (NOW_EPOCH - created_epoch) / 86400 ))
last_used=$(aws iam get-access-key-last-used --access-key-id "$key_id"
--query 'AccessKeyLastUsed.LastUsedDate'
--output text --profile "$PROFILE")
[ "$last_used" = "None" ] && last_used="NEVER_USED"
flag=""
[ "$age_days" -gt "$MAX_AGE_DAYS" ] && flag="OLD"
[ "$last_used" = "NEVER_USED" ] && flag="$flag UNUSED"
printf '%-25s %-22s %-10s %-8s %s %sn'
"$user" "$key_id" "$age_days" "$status" "$last_used" "$flag"
done
done
Sample output shape (values illustrative, field layout real):
USER ACCESS_KEY_ID AGE_DAYS STATUS LAST_USED
deploy-bot AKIAIOSFODNN7EXAMPLE 812 Active 2024-02-11T09:00:00Z OLD
deploy-bot AKIAI44QH8DHBEXAMPLE 41 Active NEVER_USED UNUSED
Rotation Runbook: What to Do With the Report
- Triage UNUSED first. A key with no recorded use is either dead weight or attached to a job nobody documented. Confirm with the owning team, then delete it. One key per check, never in bulk.
- Rotate OLD keys one at a time. Create the replacement key, deploy it to the workload, confirm traffic on the new key (its
LastUsedDateupdates), then deactivate and finally delete the old key. AWS documents this create-deploy-deactivate-delete order in its guidance on managing access keys. - Never deactivate both keys of a workload at once. Each IAM user can hold two keys precisely so rotation is hitless. Use the overlap.
- Record the rotation. Append the date, key suffix, and owner to your team runbook so the next audit has context.
Automating the Check
Run the script monthly from a scheduler, not from a laptop. Either an EventBridge-scheduled Lambda with IAM read rights or a cron entry on a hardened admin host works. Alert on any row flagged OLD or UNUSED. Keep the history: append each run to a CSV so age trends are visible instead of recomputed from memory.
On macOS, replace date -d "$created" +%s with date -j -f "%Y-%m-%dT%H:%M:%SZ" "$created" +%s, or install coreutils for GNU date.
Common Mistakes
- Deleting a key before its replacement serves traffic. Always confirm
LastUsedDateon the new key first. - Treating “N/A” last-used as an API error. It is the documented value for never-used keys. Treat it as a removal candidate, not a bug.
- Auditing only humans. Service users and CI users (
deploy-bot,ci-runner) hold the oldest keys. The script covers all users for this reason. - Storing the report with full key IDs in chat logs. Key IDs are not secrets, but keep reports in your ticket system, not in public channels.
Troubleshooting
- AccessDenied on list-users: your profile lacks
iam:ListUsers. Ask your administrator for read-only IAM audit rights or run with an audit role. - Empty output, exit 0: the account truly has no IAM users with keys (possible with SSO-only setups). Confirm with
aws iam list-users --output table. - Pagination warnings on large accounts: the CLI paginates
list-access-keysautomatically; pass--no-paginateonly when debugging a single call.
Security Considerations
Access-key IDs in this report are identifiers, not credentials; the secret key is shown only at creation time and can never be listed again. Still, treat the report as sensitive, transmit it over authenticated channels, and never commit keys or secrets to git. If a secret was ever committed, rotate the key immediately rather than auditing around it.
Alternatives
- AWS IAM Access Analyzer and credential reports for console-based review without scripts.
- Short-lived credentials (instance roles, OIDC for CI) to remove long-lived keys entirely where the workload supports it.
- AWS Config rules such as checks for unused credentials when you need continuous compliance evidence rather than a monthly script.
FAQ
How often should IAM access keys be rotated?
A common baseline is 90 days for human users and shorter for high-risk workloads, but the right number comes from your policy and audit findings. This script defaults to flagging keys older than 90 days; adjust the threshold to match your written standard.
What does it mean when LastUsedDate is empty?
Per AWS documentation, a null date means the key has not been used since IAM began tracking, or there is no sign-in data. Treat such keys as removal candidates after confirming no workload depends on them.
Can this script modify or delete anything?
No. It calls only list-users, list-access-keys, and get-access-key-last-used. Rotation and deletion are manual follow-ups by design.
Does this work with SSO-only accounts?
If nobody uses long-term access keys, the report is legitimately empty. That is a good outcome. Keep the scheduled check so regressions (a newly created static key) get caught.
Related DevStackIO Resources
- Find and Delete Unused AWS EBS Volumes Automatically: the same audit-then-act pattern applied to storage costs.
- Automate AWS EC2 Backups with Bash and AWS CLI: tag-based discovery and
--queryfiltering techniques reused in this script. - How to Protect Git Repositories From AI Agent Abuse: scoping tokens and least privilege, the same principle behind short-lived AWS credentials.
Method note: AWS CLI shapes verified against the official CLI reference for iam list-access-keys and iam get-access-key-last-used (v2.36.x, checked 2026-09-20). The script was syntax-checked with bash -n; it was not executed against a live AWS account for this draft, so sample output is illustrative. Test in a sandbox account before scheduling.
Related Tools
Try these companion tools on DevStackIO Tools:
- days between dates calculator — track IAM access key expiration dates.