AWS Access Key Compromised? The Step-by-Step Incident-Response Checklist
It usually starts with a symptom, not an alert: EC2 instances you never launched, an SES sending limit suddenly maxed out, or a bill that jumped an order of magnitude overnight. Someone has your access key. What you do in the next hour matters more than anything you do in the next week — and the order of operations matters, because several of the obvious moves do not actually stop an attacker who has already traded the key for other credentials.
This is the checklist, in the order to run it. Every command works from the AWS CLI with an administrator session that you control (CloudShell in the console is a good choice, since it does not depend on any local keys that may also be compromised).
Step 1: Deactivate the Key — Do Not Delete It Yet
Deactivation kills the credential immediately. Deletion does too, but it also makes the investigation harder: the key ID is your primary thread for the CloudTrail audit in step 4, and keeping the inactive key around removes any doubt later about which key it was.
aws iam update-access-key \
--user-name compromised-user \
--access-key-id AKIAXXXXXXXXXXXXXXXX \
--status InactiveIf you are not sure which key leaked, list keys per user with aws iam list-access-keys and check aws iam get-access-key-last-used — a key showing recent activity from a service or region you do not use is a strong signal. If the leaked key belongs to the root user, deactivate it from the console under Security Credentials, change the root password, and confirm MFA is on. Root keys have no policy limiting them; treat that as the worst case.
One more thing before moving on: if AWS detected the exposure first (they scan public GitHub commits, among other sources), they may have already opened a support case and attached a quarantine policy such as AWSCompromisedKeyQuarantineV3 to the user. Leave that policy in place until you have finished this checklist — it blocks the specific actions attackers use most, and removing it early re-opens the door.
Step 2: Kill the Credentials the Key Already Minted
This is the step most write-ups skip, and it is the one that lets attackers survive a key rotation. Long-lived access keys can be exchanged for temporary STS credentials, and those temporary credentials are not revoked when you deactivate the key that created them. They live until they expire — up to 12 hours, or longer for some role configurations.
- Sessions from assumed roles:for every role the key could assume, use the console's Revoke active sessions button (IAM → Roles → the role → Revoke sessions). This attaches an inline deny policy conditioned on
aws:TokenIssueTime, which invalidates every session issued before that moment. Legitimate workloads recover on their own the next time they assume the role. - Sessions from GetSessionToken or GetFederationToken:these inherit the IAM user's permissions at evaluation time, so attach an explicit deny-all policy to the compromised user. The existing temporary credentials instantly lose all permissions even though they have not expired.
If you skip this step, an attacker who assumed a role ten minutes before you rotated the key keeps working uninterrupted while you believe the incident is contained.
Step 3: Check What the Key Was Allowed to Do
Before reading CloudTrail, know what you are looking for. Pull every policy attached to the compromised user — managed, inline, and group-inherited — and read them as an attacker would: not "what does this workload use" but "what does this grant." A key with AdministratorAccess means the whole account is in scope. A key scoped to s3:GetObject on one bucket means the investigation is mostly about that bucket. Pay particular attention to iam:* actions, iam:PassRole, and sts:AssumeRole — those are the paths from a limited key to a bigger one.
Step 4: Audit CloudTrail — Every Region
Filter CloudTrail event history by the access key ID and reconstruct the timeline. Crucially, do this in every region, not just the ones you use — spinning up miners in a region nobody watches is standard practice precisely because most teams only look at their home region.
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAXXXXXXXXXXXXXXXX \
--region us-east-1 \
--max-results 50The events that matter most, roughly in order of how often they appear in real incidents:
GetCallerIdentity,ListUsers,ListRoles,GetAccountAuthorizationDetails— reconnaissance. Almost always the first few minutes.RunInstances— compute for cryptomining, often the largest GPU types, andCreateServiceLinkedRolefailures alongside it show what they tried and could not do.CreateUser,CreateAccessKey,CreateLoginProfile,AttachUserPolicy— persistence. The attacker is making a second door because they expect you to close the first one.UpdateAssumeRolePolicy— a subtler backdoor: editing a trust policy so an external account can assume one of your existing roles later.UpdateSendQuota/ SES activity — spam campaigns from your domain reputation.- S3 reads — note that
GetObjectis a data event and is not in event history unless you had data-event logging enabled beforehand. Absence of evidence here is not evidence of absence; if the key could read sensitive buckets, assume it did.
Step 5: Hunt and Remove Persistence
Everything the timeline showed the attacker creating needs to go, plus the things CloudTrail may not have surfaced. Sweep for:
- IAM users, access keys, and roles you did not create —
aws iam generate-credential-reportgives you every credential in the account with creation and last-used dates in one CSV. - Trust policies that now include an unknown AWS account ID as a principal — compare against the
UpdateAssumeRolePolicyevents from step 4. - EC2 instances in all regions, including stopped ones, plus spot requests and Auto Scaling groups that would relaunch terminated instances.
- Lambda functions with privileged execution roles, scheduled EventBridge rules, and ECS tasks — quieter persistence than EC2 and increasingly common.
Step 6: The Bill
Check Cost Explorer for the anomaly window and open an AWS Support case describing the compromise, even on basic support. AWS has historically been reasonable about fraud charges from a first-time compromise when the account owner responded promptly and can show it — which is another reason the CloudTrail timeline from step 4 is worth assembling carefully. Set a billing alarm before you close the incident; it is the cheapest detection you will ever deploy.
Step 7: Fix How It Leaked
The most common answers, in order: committed to a public Git repository (remember that rotating the key is the fix — scrubbing Git history is not, because the key is already harvested within minutes of the push), baked into a client-side app or mobile binary, pasted into CI logs, or sitting in a .env file that ended up somewhere public. Scan your repositories with a secrets scanner and add pre-commit hooks so the same path is closed permanently.
Then reduce how many long-lived keys exist at all: IAM roles for anything running on AWS, OIDC federation for CI pipelines like GitHub Actions, and IAM Identity Center for humans. A key that does not exist cannot leak. For the keys that must remain, GuardDuty detects anomalous credential use and is one checkbox to enable.
Step 8: Shrink the Blast Radius for Next Time
The difference between a bad afternoon and a disclosed breach is usually not detection speed — it is what the leaked credential was allowed to do. A key carrying AdministratorAccess hands over the account. A key scoped to exactly the actions and resources its workload needs hands over almost nothing. Least privilege is the control that was in place before the leak, working for you during it.
This is the part Shieldly helps with. Paste any IAM policy into the AI-Powered analyzer — free, no signup — and it flags wildcard actions, Resource: "*", privilege-escalation paths like unscoped iam:PassRole, and risky trust relationships, with a scoped-down replacement for each finding. The CLI does the same across policy files in a repo, the GitHub Action gates pull requests, and the CDK Guard construct catches over-broad roles at synth time — so the policies attached to your next set of credentials never grow the blast radius back. Submitted policies are never logged; caching uses a one-way SHA-256 hash of the input.
Know what your keys can do before someone else finds out
AI-Powered least-privilege analysis for IAM policies, trust policies, and CloudFormation — free, no credit card.
Amazon Web Services (AWS) is a trademark of Amazon.com, Inc. Shieldly is not affiliated with, endorsed by, or sponsored by Amazon Web Services.