An EC2 instance with an attached IAM role has s3:GetObject
on a bucket containing sensitive data. An attacker compromises the instance, extracts temporary credentials from the metadata service, and begins bulk-down objects. GuardDuty fires Exfiltration:S3/AnomalousBehavior
. The development team needs 4 hours to patch. You need to cut off access now.
This post walks through exactly how to do that — commands, policies, validation steps, and the reasoning behind each decision.
First, identify which IAM role is attached to the EC2 instance. You need the instance ID from the GuardDuty finding.
aws ec2 describe-instances \
--instance-ids i-0abc123def456 \
--query "Reservations[0].Instances[0].IamInstanceProfile.Arn"
Output:
"arn:aws:iam::123456789012:instance-profile/ProdDataAccessRole"
The instance profile name maps to the IAM role. Confirm the role name:
aws iam get-instance-profile \
--instance-profile-name ProdDataAccessRole \
--query "InstanceProfile.Roles[0].RoleName"
Output:
"ProdDataAccessRole"
This is the critical action. Calling revoke-sessions
on the role invalidates every set of temporary credentials issued before the current timestamp.
aws iam put-role-policy \
--role-name ProdDataAccessRole \
--policy-name AWSRevokeOlderSessions \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": ["*"],
"Resource": ["*"],
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "2026-08-23T10:30:00Z"
}
}
}
]
}'
Replace the timestamp with the current UTC time.
Alternatively, use the console shortcut: IAM → Roles → select the role → Revoke Sessions tab → Revoke active sessions. This does the same thing — attaches the inline deny policy automatically.
Every API call made with temporary credentials includes the aws:TokenIssueTime
claim. The inline policy denies all actions for any credential set issued before the specified timestamp. The deny is evaluated on every request, effective immediately — no propagation delay.
Credentials the attacker extracted 10 minutes ago? Denied. Credentials they might have cached? Denied. Credentials being used from a completely different network? Still denied.
Verify the inline policy is attached:
aws iam get-role-policy \
--role-name ProdDataAccessRole \
--policy-name AWSRevokeOlderSessions
Then confirm the attacker's access is actually cut. Check CloudTrail for AccessDenied
events after your revocation timestamp:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventSource,AttributeValue=s3.amazonaws.com \
--start-time "2026-08-23T10:30:00Z" \
--query "Events[?contains(CloudTrailEvent, 'AccessDenied')].[EventTime, CloudTrailEvent]" \
--max-results 10
If you see AccessDenied
responses for GetObject
calls on your bucket, the revocation is working.
The EC2 instance (if it has not been isolated) will automatically request new credentials from the metadata service. These new credentials have a TokenIssueTime
after your revocation timestamp, so they pass the condition check.
Confirm by SSHing into a healthy instance with the same role (or a test instance) and running:
aws s3api head-object --bucket patient-records --key test-object.json
If this returns metadata successfully, legitimate access is intact.
aws ec2 modify-instance-attribute \
--instance-id i-0abc123def456 \
--groups sg-0000000000000 # empty security group
This cuts network access to/from the instance. But the attacker likely already has the credentials elsewhere. Here is why:
When an attacker compromises an EC2 instance, the standard playbook is:
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ProdDataAccessRole
Response:
{
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"Token": "...",
"Expiration": "2026-08-23T16:00:00Z"
}
These three values are all that is needed. The attacker copies them to any machine and runs:
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
aws s3 sync s3://patient-records ./exfil/
The security group on the original instance is irrelevant at this point. The S3 API calls come from a different source IP entirely.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::patient-records/*"
}
]
}
This stops the attacker. It also stops:
The application servers reading patient data
Backup jobs
Analytics pipelines
Any other IAM principal in the account
In a healthcare system with active read traffic, this creates an immediate outage across all consumers. For a 4-hour fix window, that means 4 hours of service disruption affecting every system that depends on the bucket.
The revocation approach stops exactly one set of credentials. Everything else continues working.
1. The role still works after revocation.
Revocation does not disable the role. It only invalidates credentials issued before a specific time. New credentials obtained after the timestamp work normally. If the attacker still has access to the instance and can hit the metadata service again, they get fresh credentials that bypass the revocation.
Mitigation: Combine revocation with instance isolation (stop the instance or quarantine it). Revocation handles credentials already out in the wild. Isolation prevents new credential issuance.
aws iam put-role-policy ...
aws ec2 stop-instances --instance-ids i-0abc123def456
Order matters. Revoke first, then isolate. If you isolate first but do not revoke, exfiltrated credentials keep working.
2. Temporary credentials have a maximum lifetime.
STS credentials from instance profiles last up to 6 hours (default 1 hour, configurable). Even without revocation, they expire. But during an active exfiltration with hundreds of terabytes at risk, waiting for expiration is not an option.
3. The inline policy stays until you remove it.
After the development team deploys the fix, remove the revocation policy:
aws iam delete-role-policy \
--role-name ProdDataAccessRole \
--policy-name AWSRevokeOlderSessions
If you forget, any process that cached old credentials (unlikely but possible in long-running containers) will get denied unexpectedly.
4. CloudTrail latency.
CloudTrail events can take 5-15 minutes to appear. Do not wait for CloudTrail confirmation before proceeding. Apply the revocation immediately, validate later.
[T+0] GuardDuty alert fires
[T+2m] Identify compromised role from instance profile
[T+3m] Revoke active sessions (inline deny policy)
[T+4m] Stop or isolate the EC2 instance
[T+5m] Notify development team, provide role ARN and finding details
[T+10m] Validate via CloudTrail that AccessDenied responses are occurring
[T+15m] Confirm legitimate workloads are unaffected
[T+4h] Development team deploys fix
[T+4h+5m] Remove revocation inline policy
[T+4h+10m] Full post-incident review
The attack targets temporary credentials. The response must target temporary credentials. Everything else — network isolation, resource-level policies, classification tools — either misses the actual threat vector or creates collateral damage that exceeds the original incident.
One IAM inline policy with a DateLessThan
condition on aws:TokenIssueTime
is the smallest possible blast radius with the fastest possible effect.
References: