Stopping S3 Data Exfiltration in Real Time: A Step-by-Step Incident Response
The Scenario 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-downloading 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. Step 1: Confirm the Compromised Role 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" Step 2: Revoke All Active Sessions 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. What This Does Under the Hood 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. Step 3: Validate the Revocation 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. Step 4: Verify Legitimate Workloads Still Function 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. Why Other Approaches Fail Security Group Isolation # This does NOT solve the problem 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: # Attacker runs this ON the instance 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. Bucket Policy Deny-All { "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. Gotchas and Tradeoffs 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. # Revoke first (handles exfiltrated credentials) aws iam put-role-policy ... # Then isolate (prevents new credential requests) 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. The Full Incident Response Sequence [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 Summary 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: Top comments (0)
Comments
No comments yet. Start the discussion.