DEV Community

Danh Hoang Hieu Nghi
Danh Hoang Hieu Nghi

Posted on

# 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"
Enter fullscreen mode Exit fullscreen mode

Output:

"arn:aws:iam::123456789012:instance-profile/ProdDataAccessRole"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Output:

"ProdDataAccessRole"
Enter fullscreen mode Exit fullscreen mode

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"
          }
        }
      }
    ]
  }'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "AccessKeyId": "ASIA...",
  "SecretAccessKey": "...",
  "Token": "...",
  "Expiration": "2026-08-23T16:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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/*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)