AWS access keys: from leak to takeover, the documented paths
IAM credential mechanics, what different permission sets unlock, the documented containment sequence — and why roles should replace long-lived keys.
AWS access keys: from leak to takeover, the documented paths
For anyone holding AWS access keys: how the credential pair works, what disclosed keys unlock permission-by-permission, AWS's own containment sequence — and the migration path off long-lived keys entirely.
AWS access keys occupy an extreme position among credentials: they are long-lived by default, they grant API access to infrastructure rather than an application, and their abuse potential scales directly with IAM policy design — which in practice ranges from surgical to AdministratorAccess. A leaked Stripe key drains a payments account; a leaked AWS key can silently create new permanent access, spin up billable fleets across regions, and read every S3 bucket policy forgot to protect. The difference isn't the leak. It's the blast radius the identity carries.
This guide stays close to AWS's own documentation throughout: credential formats and lifetimes, the capability map by permission, the containment sequence from AWS incident-response guidance, and the structural fix — replacing long-lived keys with roles and temporary credentials. Example key IDs below are synthetic placeholders matching documented formats.
The credential pair and its lifetime
Programmatic AWS access uses two strings (IAM access keys documentation):
| Component | Format | Notes |
|---|---|---|
| Access key ID | AKIA + 16 uppercase alphanumeric characters | Long-term identifier (synthetic example: AKIASYNTHETICEXAMPLE) |
| Secret access key | 40-character random string | Shown once at creation; equivalent to a password |
| Temporary session keys | IDs beginning ASIA… | Issued by STS; expire on schedule with session tokens |
The defining property is longevity: unlike STS sessions, IAM user access keys never expire until someone rotates or deletes them. Each IAM user holds at most two active keys simultaneously — a limit that exists precisely to enable overlap-based rotation, where a replacement deploys before its predecessor retires.
Temporary credentials behave differently: ASIA-prefixed IDs arrive with expiration baked in via session tokens, after which they authenticate nothing regardless of whether anyone remembers to revoke them. That asymmetry — eternal versus self-expiring — drives every recommendation later in this article.
Why these keys persist everywhere
Access keys accumulate because every integration tutorial begins with "create a user and generate keys," and nothing in the default flow expires:
- Local
.envfiles and shell exports (AWS_ACCESS_KEY_ID=…) on every laptop that touched deployment. - CI variables configured years ago by departed team members.
- Baked into machine images, container layers built with
COPY . ., ECS task definitions, and Lambda environment configurations. - Third-party tools' config files:
~/.aws/credentialsalone may hold half a dozen accounts' worth.
Each copy outlives its purpose indefinitely. The credential report routinely surfaces keys unused for years that remain fully valid — dormant credentials nobody remembers minting, still carrying whatever permissions were granted during setup. This persistence, combined with the takeover capabilities below, is why leaked AWS keys are treated as among the most severe finding classes in any scan.
What disclosed keys unlock
An access key authorizes whatever IAM allows its principal. Mapping documented permissions to outcomes clarifies why least privilege matters so much here (IAM best practices):
| Permission held | Documented capability |
|---|---|
s3:GetObject / ListBucket on data buckets | Bulk retrieval of stored objects — databases backups, uploads, archives |
iam:CreateUser + iam:CreateAccessKey | Persistence: brand-new credentials outside your inventory |
iam:AttachUserPolicy / PutUserPolicy | Privilege escalation onto existing principals |
sts:AssumeRole on broad trust policies | Lateral movement into other identities' permissions |
ec2:RunInstances | Arbitrary billable compute, commonly crypto-mining scale |
ses:SendEmail | Email sending under your domain reputation |
kms:Decrypt on broad key policies | Plaintext recovery for anything encrypted with those keys |
logs:GetLogEvents | Retrieval of application logs — which contain their own secrets |
Three patterns deserve emphasis. Persistence beats theft: the iam:Create* path means detection-and-revocation of the original key doesn't end an incident if attacker-created users survive cleanup — which is why response includes IAM-state review, not just key rotation. Spend is invisible: EC2/GPU usage bills quietly until invoice review; there's no rate-limit tripwire like an LLM budget cap unless budgets are deliberately configured. Encryption without scope is storage: kms:Decrypt breadth converts your encryption investment into mere delay for any principal holding it.
None of this constitutes an exploitation guide — it's the permission reference read from the defender's side, and it justifies two practices: audit what each key-bearing identity could actually do, and shrink that set relentlessly.
Users versus roles: the structural fix
AWS's guidance is unambiguous: prefer roles and temporary credentials over long-lived access keys, lock away root user credentials entirely, and reserve IAM users with keys for cases that genuinely cannot use roles (best practices).
Roles issue STS credentials automatically — short-lived, auto-rotated, scoped per session. Human federation (SSO) and workload federation eliminate static keys at both ends of the spectrum:
# GitHub Actions assuming an AWS role via OIDC — no stored keys anywhere
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/ci-deploy
aws-region: us-east-1
Under this pattern the question "what if the CI key leaks?" has a satisfying answer: there is no CI key. The remaining long-lived pairs in most organizations are legacy — created before federation was easy, surviving because rotation feels risky. The rotation playbook removes that objection: the two-key limit makes overlap migration routine.
Containment: AWS's documented sequence
When a key is known or suspected leaked, AWS incident-response guidance prescribes this ordering (Security Incident Response Guide):
- Cut the credential's ability to act immediately — faster than deletion workflows require. Attach an explicit deny-all inline policy, which takes effect ahead of allow-policies by policy evaluation rules:
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "Quarantine", "Effect": "Deny", "Action": "*", "Resource": "*" }
]
}
aws iam put-user-policy --user-name ci-deployer \
--policy-name Quarantine --policy-document file://quarantine.json
# Belt-and-suspenders: deactivate the key itself
aws iam update-access-key --user-name ci-deployer \
--access-key-id AKIASYNTHETICEXAMPLE --status Inactive
- Investigate before deleting — CloudTrail retains the evidence trail: filter events by
accessKeyIdto enumerate everything the key did, notingsourceIPAddress, user agents, and IAM mutations. Deletion destroys future lookups' anchor. - Review IAM state for persistence: unexpected users, new access keys, modified trust policies, added inline policies.
- Rotate legitimately-used consumers onto replacement credentials (overlap pattern), then retire the quarantined pair permanently.
- Scope the disclosure window from first-suspicious-event timestamps for billing review, support engagement, and notification obligations.
The full first-hour context — including the irreversibility questions that apply to data already read — lives in the incident-response runbook.
Finding exposure before anyone else does
Proactive discovery, cheapest first:
- Credential report monthly:
aws iam generate-credential-reportthenaws iam get-credential-reportyields CSV covering every user's key ages and last-use dates. Columns likeaccess_key_1_last_used_datemake dormancy obvious; anything stale beyond policy gets rotated or deleted. - Access Analyzer: flags external trust relationships and unintended resource exposure continuously.
- Artifact scanning: keys leak into bundles, logs, and public repos like everything else — AWS access-key IDs are among the partner patterns covered by platform push protection (coverage analysis), while everything beyond git requires artifact-level checks (continuous monitoring).
- Budget alarms: cost anomalies are frequently the first observable signal of key abuse; configure them before you need them.
A four-week migration off long-lived keys
Knowing roles beat keys changes nothing until someone schedules the migration. The obstacle is rarely technical — it's that key usage is undocumented, so every attempt stalls on "what breaks if we retire this?" The way through is incremental, evidence-driven, and fits neatly into a month of part-time work. The plan below assumes one offending IAM user with an active access key; repeat per user.
Week one — establish ground truth. Generate the credential report and pull CloudTrail activity attributable to the key, producing the consumption map that every later step depends on:
aws iam generate-credential-report
aws iam get-credential-report --query Content --output text \
| base64 --decode > credential-report.csv
# What has this key actually done recently?
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAFAKEEXAMPLE000000 \
--max-results 50
Classify every consumer the evidence reveals — deploy scripts, laptops, CI stores, vendor integrations — into the propagation-model table from the rotation playbook. Undocumented consumers discovered here are gifts: they'd otherwise be outage tickets later.
Week two — build the replacement identity. Create the role the work actually needs, permissioned from week one's evidence rather than from optimism, and trust it from the appropriate federation source. For CI on GitHub Actions, the trust policy names the OIDC provider with repository and branch conditions:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sigstore",
"token.actions.githubusercontent.com:sub": "repo:acme/app:ref:refs/heads/main"
}
}
}]
}
Scope the permission policy to the services the evidence showed being used — nothing speculative. Least privilege is easier to achieve during migration than any other time, because usage data is fresh and stakeholders are engaged.
Week three — move consumers, overlap running. Update each consumer to federated access (CI workflows gain the id-token permission and role assumption shown earlier; local scripts switch to SSO sessions), while the old key remains active as the safety net. Verify silence on the old key as each consumer flips — same discipline as any rotation.
Week four — retire and lock the door. Deactivate the key, observe through one full cycle of the slowest scheduled consumer, delete it, then prevent regression: attach an SCP or permission boundary denying iam:CreateAccessKey outside approved automation, so the ecosystem doesn't quietly regrow what you removed. Add the user's retirement to the inventory record.
Teams running this sequence discover the pattern behind the advice: long-lived keys persist because retiring them feels risky, and they feel risky precisely while usage stays unmapped. Week one dissolves the feeling; weeks two through four are mechanical. The end state — no static pair anywhere, ephemeral federation everywhere — eliminates this entire article's incident classes rather than managing them (the maturity view).
Reading CloudTrail like a responder
Containment freezes abuse; CloudTrail tells you what happened first. Responder-grade reading focuses on a handful of event attributes:
| Field | Question it answers |
|---|---|
userIdentity.accessKeyId | Which credential acted — filter to the exposed pair |
sourceIPAddress / ASN | Does traffic geography match your infrastructure? |
userAgent | Your deploy scripts have signatures; foreign tooling usually doesn't |
eventName | IAM mutations (CreateUser, CreateAccessKey, AttachUserPolicy) mark persistence attempts |
requestParameters | What exactly was read, created, or modified |
readOnly flag | Separates reconnaissance from action |
The lookup command from week one of the migration plan doubles as the incident query — filter by the exposed key ID across the exposure window, then read events chronologically looking for narrative phases: reconnaissance (ListBuckets, GetCallerIdentity), escalation (IAM mutations above), monetization (RunInstances, SES sends), persistence (new keys/users created). Each phase's timestamps feed the disclosure-window calculation that support tickets and notification analysis require (the assessment stage).
Two reading habits prevent common missteps. Console versus programmatic: management-console events arrive with distinct session semantics (via SSO or federated roles) — a pure access-key incident shows no console events unless credentials were also phished, so their absence is expected rather than reassuring. And absence of evidence has boundaries: data-plane reads for services without CloudTrail data-event coverage configured won't appear regardless of occurrence — check what your trail actually records before concluding restraint on the attacker's part. Trails configured after the fact start recording too late for the window that matters, which is why trail coverage belongs in standing hygiene rather than incident response.
IAM Access Analyzer extends discovery beyond keys to the trust relationships keys can reach: external principals on roles, buckets, KMS keys, and queues get flagged continuously rather than during audits. Pair it with the credential report on a monthly calendar and the pairing covers both halves of exposure — credentials that exist too long, and permissions that reach too far. Neither tool requires enablement ceremony beyond creation, and both feed the same standing inventory the rotation playbook consumes.
Reading IAM policies like an attacker completes the least-privilege skill set. Policy documents yield their secrets to a fixed reading order: Effect first (any Allow with broad scope deserves scrutiny regardless of how many tidy Deny clauses decorate the document), then Action wildcards (s3:* versus s3:GetObject — the difference between storage and disclosure), then Resource scope (* on data-plane actions is the classic finding), then Condition blocks, which frequently narrow real-world usability far more than the Action line suggests. The attacker-shaped questions fall out mechanically: could this principal read data from anywhere? create identities? assume other roles? Each yes maps to a row in the capability table above — and each tightening of an answer is blast radius removed permanently, no incident required.
A closing calibration on urgency: AWS exposure compresses response timelines more than most providers because persistence mechanisms deploy in seconds. The containment sequence above therefore tolerates no investigation-before-action ordering — deny first, ask questions with CloudTrail afterward. Teams rehearsing this once discover the second-order benefit: the same muscle memory serves every other credential class, since rotate-first-verify-after generalizes everywhere (the runbook's central decision).
The habit that outlives every procedure here is monthly evidence review: credential report scanned for dormancy, Access Analyzer findings triaged, budget alarms confirmed armed. Fifteen minutes, calendar-driven, and the standing answer to "how would we know?" becomes a documented system rather than a shrug.
FAQ
We found our key in a public repo but it looks unused. Still rotate? Yes, unconditionally. "Looks unused" depends on CloudTrail completeness and your attention; persistence mechanisms (created users, alternate keys) don't announce themselves in billing. Run the containment sequence anyway — it costs minutes against hours-scale downside.
How often should long-lived keys rotate if we keep any? Quarterly is a defensible ceiling for production-adjacent keys, event-driven rotation (staff changes, suspected exposure) layered atop. But treat each rotation cycle as pressure toward roles: the endgame is having nothing long-lived to rotate.
Can we restrict keys by IP or network like other providers allow? Not natively on the key itself — network conditions attach via IAM policy condition keys (for example, restricting aws:SourceIp), applied per-policy rather than per-credential. Workable, but another argument for roles: session policies and permission boundaries express the same intent more cleanly.
Do ASIA-prefixed temporary keys matter for scanning? They appear in artifacts when applications log full credential chains, and they're worth flagging despite expiring — the accompanying session token often outlives discovery windows. Detection should cover both prefixes; remediation differs only in patience required.
Long-lived AWS keys are liabilities you can retire. Meanwhile, know what your deployed artifacts serve: KeyDrift's free scan checks in minutes, no signup.