Long-lived AWS access keys leak in boring ways. Someone commits a .env file. A key gets pasted into a chat thread, baked into a container image, or left in a notebook that ends up on GitHub. And public GitHub is watched constantly: automated scanners pick up exposed keys within minutes, and abuse can start almost as fast. The usual first sign is a bill full of GPU or compute instances in a region you’ve never used.

AWS watches too. When it detects an exposed key, it attaches a managed quarantine policy, AWSCompromisedKeyQuarantineV3, to the IAM user and opens a support case. That policy explicitly denies a long list of high-risk actions: creating access keys and users, launching instances, invoking AI models, and more. If you find it on a user, don’t detach it to “get things working again.” Follow the support case. It’s buying you time, not causing your outage.

This is the playbook I’d run in the first hour, organized around the incident response lifecycle: containment first, then analysis, then eradication and recovery, then lessons learned.

Minutes 0 to 10: contain

Find out whose key it is. An access key ID is enough to identify the account and the user:

aws sts get-access-key-info --access-key-id AKIAEXAMPLEKEYID
aws iam get-access-key-last-used --access-key-id AKIAEXAMPLEKEYID

The first returns the account ID. The second, run in that account, returns the IAM user name, plus when, where, and with which service the key was last used. A recent last-used time from a region you don’t operate in tells you a lot very quickly.

Deactivate the key. Don’t delete it yet.

aws iam update-access-key --user-name build-bot \
  --access-key-id AKIAEXAMPLEKEYID --status Inactive

Deactivation is immediate and reversible. Deleting the key destroys the identifier you’re about to search the logs for, and if something critical depended on the key, you’ll want to be able to reactivate it briefly while you fix that properly.

Kill the sessions it already minted. This is the step people miss. Deactivating a long-term key doesn’t revoke temporary credentials that were already issued from it, for example with sts:GetSessionToken, or by assuming a role. An attacker who got there first may be holding credentials that keep working for hours. The fix is a deny policy that applies to any temporary credential issued before now:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Deny",
    "Action": "*",
    "Resource": "*",
    "Condition": {
      "DateLessThan": { "aws:TokenIssueTime": "2026-10-29T18:00:00Z" }
    }
  }]
}

Attach it inline to the user. For any role the key was able to assume, the IAM console’s “Revoke active sessions” action on the role does the same thing for you.

Minutes 10 to 40: figure out what happened

Search CloudTrail for the key, in every region. lookup-events covers the last 90 days of management events, but only one region at a time, and attackers love the regions you don’t use:

for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  echo "== $r"
  aws cloudtrail lookup-events --region "$r" \
    --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAEXAMPLEKEYID \
    --query 'Events[].[EventTime,EventName,EventSource]' --output text
done

Then search again by user name. Calls made with temporary credentials derived from the key carry a different access key ID, one starting with ASIA, so a search for the original key alone can miss most of the activity.

Look for the indicators that matter. The first call is often sts:GetCallerIdentity: the attacker checking what they’ve got. After that, the ones to worry about are persistence and abuse:

  • New identities and credentials: CreateUser, CreateAccessKey, CreateLoginProfile, CreateRole, UpdateAssumeRolePolicy. These are how an attacker stays after you revoke the original key.
  • Privilege changes: AttachUserPolicy, PutUserPolicy, AttachRolePolicy, CreatePolicyVersion.
  • Compute in odd places: RunInstances, RequestSpotInstances, CreateFunction, especially in unfamiliar regions. This is usually crypto mining or resale.
  • Network changes: AuthorizeSecurityGroupIngress, ImportKeyPair.
  • Defense evasion: StopLogging, DeleteTrail, DeleteDetector. If you see these, assume the attacker knew what they were doing.

Be honest about data access. Management events don’t include S3 object reads, or most data reads at all, unless you enabled data events ahead of time. If you didn’t, you may not be able to prove what was or wasn’t read. In that case, the safe assumption is that anything the key’s permissions allowed it to read was read. That assumption drives the next phase.

Check GuardDuty findings and the billing console while you’re at it. Cost anomalies are often the loudest signal you’ll get.

Minutes 40 to 60: eradicate and recover

  • Remove persistence first. Delete any users, keys, roles, login profiles, and policy versions the attacker created. Revert trust policy changes. Do this before cleaning up resources, or you’ll be cleaning up the same resources again tomorrow.
  • Terminate what they launched, in every region, and check for anything with a DeletionPolicy or termination protection set to slow you down.
  • Rotate everything the key could read. If it could call secretsmanager:GetSecretValue or ssm:GetParameter, those secrets are now exposed too, and so is whatever they unlock. This is often the longest list and the most important one.
  • Replace the key, ideally with nothing. Create a new key only if you truly need one. Better: move the workload to an IAM role, IAM Identity Center for people, or OIDC federation for CI, so there’s no long-lived key left to leak.
  • Don’t rely on rewriting Git history. Once a key has been public, it’s in forks, caches, and scrapers’ databases. Removing it from the repo is hygiene. Revoking it is the actual fix.

After the hour: make it not happen again

The lessons-learned phase is where the real value is, and most of the fixes are cheap:

  • Turn on secret scanning and push protection for your repositories, so a key is blocked at git push instead of found after the fact. A pre-commit hook such as gitleaks catches it even earlier.
  • Get humans off access keys entirely with IAM Identity Center, and CI off them with OIDC federation.
  • Restrict regions with a service control policy so a leaked credential can’t launch anything where you don’t operate.
  • Set billing and cost anomaly alerts so the bill isn’t your detection system.
  • Send CloudTrail to an organization trail in a separate log archive account, with data events on your sensitive buckets, so the next investigation has the evidence this one didn’t.

A leaked key is one of the most common cloud incidents there is, and the response is very learnable. Contain in minutes, look for persistence before cleanup, assume exposure where you can’t prove otherwise, and spend the post-incident effort on making long-lived keys rare enough that the next leak has nothing to leak.