AWS Builder Center
Deleting a Leaked AWS Key Isn't Enough. Here's What People Miss.

Deleting a Leaked AWS Key Isn't Enough. Here's What People Miss.

If an AWS key leaks, deleting it does not stop the access made with it. Here is a one-hour sandbox test that shows why, plus the two steps that actually shut those sessions down.

DevSecOps Engineer & AWS Community Builder | Cloud Security
Almost every guide for a leaked AWS key says the same thing. Delete the key, rotate it, close the ticket. I tried exactly that in a test account, and the access I was trying to shut down kept working for hours after the key was gone.
This isn't a bug or a trick. It's normal AWS behavior and it's right there in the docs. It just rarely makes it into the incident runbook. Below is why it happens and the two steps that actually stop it. You can run the whole thing in a throwaway account in about an hour.

The short version

A leaked access key isn't the only credential that matters. The key can create more. STS calls like GetSessionToken and AssumeRole hand back temporary credentials that stand on their own. They have their own access key ID that starts with ASIA, their own secret, and a session token. AWS checks them against the current permissions on the user or role. It does not check them against the key that created them.
So when you delete the original key, you kill that key and nothing else. Every request gets checked against the live permissions of the identity, and that is the opening you need. You can't cancel a temporary credential once it exists, but you can block everything that was issued before a point in time.
Here are the durations from the STS docs, so you can see how long this window stays open:
  • GetSessionToken for an IAM user: 12 hours by default, up to 36 hours.
  • AssumeRole: 1 hour by default, up to the role's max session length, which can be 12 hours.
A session created right before you delete the key can keep working for most of a day.

Reproduce it safely

Use admin credentials in a sandbox account you don't care about. Make a read-only user and a role it can assume.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
aws iam create-user --user-name leak-test
aws iam attach-user-policy --user-name leak-test \
--policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
cat > trust.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::${ACCOUNT_ID}:user/leak-test" },
"Action": "sts:AssumeRole"
}]
}
EOF

aws iam create-role --role-name leak-test-role \
--assume-role-policy-document file://trust.json
aws iam attach-role-policy --role-name leak-test-role \
--policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess

aws iam create-access-key --user-name leak-test
Save the AccessKeyId and SecretAccessKey from the last command. This is the credential we'll treat as leaked.

Create the temporary credentials

This is the part most responders never see, because it happens on the other side of the incident. In your sandbox you can watch it yourself. Put the key in a profile, then make a session token and an assumed-role session.
1
2
3
4
5
6
7
8
aws configure set aws_access_key_id AKIA... --profile exposed
aws configure set aws_secret_access_key ... --profile exposed
aws configure set region us-east-1 --profile exposed

aws sts get-session-token --duration-seconds 129600 --profile exposed
aws sts assume-role \
--role-arn arn:aws:iam::${ACCOUNT_ID}:role/leak-test-role \
--role-session-name observed --profile exposed
Each call returns an ASIA... credential set. Load them into two more profiles called session and role using aws configure set ... aws_session_token ..., then confirm they work with aws sts get-caller-identity and aws s3 ls.

Now do what the runbook says

Delete the leaked key and check that it's dead.
1
2
3
aws iam delete-access-key --user-name leak-test --access-key-id AKIA...
aws s3 ls --profile exposed
# An error occurred (InvalidClientTokenId) ... The security token included in the request is invalid.
Key gone. In a normal workflow, this is where the ticket closes.

The gap, shown

1
2
3
4
5
6
aws s3 ls --profile session
# still lists buckets

aws ec2 describe-instances --profile role \
--query 'Reservations[].Instances[].InstanceId'
# still returns instance IDs
Both temporary credentials still work. Run aws sts get-caller-identity --profile session and look at the ARN. It still says user/leak-test. The user exists, the user still has ReadOnlyAccess, so the session is still allowed. Deleting the key changed none of that.

How to close it properly

You can't revoke a temporary credential, but you can deny everything a user or role issued before now. The condition key that does it is aws:TokenIssueTime.

Step 1: the role sessions

The console does this one for you. In IAM, open the role, go to the Revoke sessions tab, and choose Revoke active sessions. AWS attaches an inline policy named AWSRevokeOlderSessions that looks like this:
1
2
3
4
5
6
7
8
9
10
11
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": { "aws:TokenIssueTime": "2026-10-03T12:00:00Z" }
}
}
}
It denies everything for sessions issued before that timestamp. It kicks in for past sessions and about 30 seconds into the future. Sessions assumed after that short window keep working, which is fine, because by then the key is already gone.

Step 2: the user's own sessions

GetSessionToken credentials carry the user's ARN, not a role's, so revoking the role sessions doesn't touch them. Attach the same kind of deny policy straight to the user.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat > deny-old.json <<EOF
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": { "aws:TokenIssueTime": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" }
}
}
}
EOF

aws iam put-user-policy --user-name leak-test \
--policy-name DenyOldSessions --policy-document file://deny-old.json
Run aws s3 ls --profile session again and it now fails. The session credential still exists and hasn't expired yet, but every request it signs gets denied.
There is a blunt option too. Deleting the IAM user makes all of its temporary credentials stop working. It's final and it works, but it also breaks anything legitimate still using that identity, so the time-based deny is usually the cleaner way to contain things.

How to tell if it happened to you

The evidence is in CloudTrail. Look for:
  • GetSessionToken, AssumeRole, or GetFederationToken calls made with the leaked access key ID, especially ones with a long durationSeconds.
  • API activity under ASIA... credentials after the moment you deleted the key.
  • Calls from source IPs or user agents you don't recognize in the same window.
A quick CloudTrail Lake or Athena query filtered on the key ID and those STS event names will show the create calls. From there you can trace each session that came out of them.

A leaked-key checklist worth keeping

  1. Deactivate or delete the leaked access key.
  2. Revoke active sessions on any role the user could assume.
  3. Attach an aws:TokenIssueTime deny policy to the user to stop its running sessions.
  4. Search CloudTrail for STS create calls and anything that used the results.
  5. Delete the user if the identity is disposable and you want to be sure.
  6. Stop the repeat. Use roles and short-lived credentials instead of long-lived keys, turn on secret scanning in your repos, and alert on odd GetSessionToken or GetFederationToken usage.
Deleting the key is step one of six, not the whole job. The credential you can see is rarely the only one that counts.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article