{"slug": "stopping-s3-data-exfiltration-in-real-time-a-step-by-step-incident-response", "title": "Stopping S3 Data Exfiltration in Real Time: A Step-by-Step Incident Response", "summary": "A developer detailed a step-by-step incident response for stopping S3 data exfiltration in real time, focusing on revoking IAM role sessions to invalidate compromised temporary credentials. The approach uses an inline deny policy with a timestamp condition, which takes effect immediately without propagation delay. The post emphasizes that revoking sessions is more effective than isolating the EC2 instance, as attackers may have already exfiltrated credentials.", "body_md": "An EC2 instance with an attached IAM role has `s3:GetObject`\n\non 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`\n\n. The development team needs 4 hours to patch. You need to cut off access now.\n\nThis post walks through exactly how to do that — commands, policies, validation steps, and the reasoning behind each decision.\n\nFirst, identify which IAM role is attached to the EC2 instance. You need the instance ID from the GuardDuty finding.\n\n```\naws ec2 describe-instances \\\n  --instance-ids i-0abc123def456 \\\n  --query \"Reservations[0].Instances[0].IamInstanceProfile.Arn\"\n```\n\nOutput:\n\n```\n\"arn:aws:iam::123456789012:instance-profile/ProdDataAccessRole\"\n```\n\nThe instance profile name maps to the IAM role. Confirm the role name:\n\n```\naws iam get-instance-profile \\\n  --instance-profile-name ProdDataAccessRole \\\n  --query \"InstanceProfile.Roles[0].RoleName\"\n```\n\nOutput:\n\n```\n\"ProdDataAccessRole\"\n```\n\nThis is the critical action. Calling `revoke-sessions`\n\non the role invalidates every set of temporary credentials issued before the current timestamp.\n\n```\naws iam put-role-policy \\\n  --role-name ProdDataAccessRole \\\n  --policy-name AWSRevokeOlderSessions \\\n  --policy-document '{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n      {\n        \"Effect\": \"Deny\",\n        \"Action\": [\"*\"],\n        \"Resource\": [\"*\"],\n        \"Condition\": {\n          \"DateLessThan\": {\n            \"aws:TokenIssueTime\": \"2026-08-23T10:30:00Z\"\n          }\n        }\n      }\n    ]\n  }'\n```\n\nReplace the timestamp with the current UTC time.\n\nAlternatively, 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.\n\nEvery API call made with temporary credentials includes the `aws:TokenIssueTime`\n\nclaim. 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.\n\nCredentials the attacker extracted 10 minutes ago? Denied. Credentials they might have cached? Denied. Credentials being used from a completely different network? Still denied.\n\nVerify the inline policy is attached:\n\n```\naws iam get-role-policy \\\n  --role-name ProdDataAccessRole \\\n  --policy-name AWSRevokeOlderSessions\n```\n\nThen confirm the attacker's access is actually cut. Check CloudTrail for `AccessDenied`\n\nevents after your revocation timestamp:\n\n```\naws cloudtrail lookup-events \\\n  --lookup-attributes AttributeKey=EventSource,AttributeValue=s3.amazonaws.com \\\n  --start-time \"2026-08-23T10:30:00Z\" \\\n  --query \"Events[?contains(CloudTrailEvent, 'AccessDenied')].[EventTime, CloudTrailEvent]\" \\\n  --max-results 10\n```\n\nIf you see `AccessDenied`\n\nresponses for `GetObject`\n\ncalls on your bucket, the revocation is working.\n\nThe EC2 instance (if it has not been isolated) will automatically request new credentials from the metadata service. These new credentials have a `TokenIssueTime`\n\nafter your revocation timestamp, so they pass the condition check.\n\nConfirm by SSHing into a healthy instance with the same role (or a test instance) and running:\n\n```\naws s3api head-object --bucket patient-records --key test-object.json\n```\n\nIf this returns metadata successfully, legitimate access is intact.\n\n```\n# This does NOT solve the problem\naws ec2 modify-instance-attribute \\\n  --instance-id i-0abc123def456 \\\n  --groups sg-0000000000000  # empty security group\n```\n\nThis cuts network access to/from the instance. But the attacker likely already has the credentials elsewhere. Here is why:\n\nWhen an attacker compromises an EC2 instance, the standard playbook is:\n\n```\n# Attacker runs this ON the instance\ncurl http://169.254.169.254/latest/meta-data/iam/security-credentials/ProdDataAccessRole\n```\n\nResponse:\n\n```\n{\n  \"AccessKeyId\": \"ASIA...\",\n  \"SecretAccessKey\": \"...\",\n  \"Token\": \"...\",\n  \"Expiration\": \"2026-08-23T16:00:00Z\"\n}\n```\n\nThese three values are all that is needed. The attacker copies them to any machine and runs:\n\n```\nexport AWS_ACCESS_KEY_ID=ASIA...\nexport AWS_SECRET_ACCESS_KEY=...\nexport AWS_SESSION_TOKEN=...\naws s3 sync s3://patient-records ./exfil/\n```\n\nThe security group on the original instance is irrelevant at this point. The S3 API calls come from a different source IP entirely.\n\n```\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Deny\",\n      \"Principal\": \"*\",\n      \"Action\": \"s3:GetObject\",\n      \"Resource\": \"arn:aws:s3:::patient-records/*\"\n    }\n  ]\n}\n```\n\nThis stops the attacker. It also stops:\n\nThe application servers reading patient data\n\nBackup jobs\n\nAnalytics pipelines\n\nAny other IAM principal in the account\n\nIn 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.\n\nThe revocation approach stops exactly one set of credentials. Everything else continues working.\n\n**1. The role still works after revocation.**\n\nRevocation 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.\n\nMitigation: Combine revocation with instance isolation (stop the instance or quarantine it). Revocation handles credentials already out in the wild. Isolation prevents new credential issuance.\n\n```\n# Revoke first (handles exfiltrated credentials)\naws iam put-role-policy ...\n\n# Then isolate (prevents new credential requests)\naws ec2 stop-instances --instance-ids i-0abc123def456\n```\n\nOrder matters. Revoke first, then isolate. If you isolate first but do not revoke, exfiltrated credentials keep working.\n\n**2. Temporary credentials have a maximum lifetime.**\n\nSTS 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.\n\n**3. The inline policy stays until you remove it.**\n\nAfter the development team deploys the fix, remove the revocation policy:\n\n```\naws iam delete-role-policy \\\n  --role-name ProdDataAccessRole \\\n  --policy-name AWSRevokeOlderSessions\n```\n\nIf you forget, any process that cached old credentials (unlikely but possible in long-running containers) will get denied unexpectedly.\n\n**4. CloudTrail latency.**\n\nCloudTrail events can take 5-15 minutes to appear. Do not wait for CloudTrail confirmation before proceeding. Apply the revocation immediately, validate later.\n\n```\n[T+0]  GuardDuty alert fires\n[T+2m] Identify compromised role from instance profile\n[T+3m] Revoke active sessions (inline deny policy)\n[T+4m] Stop or isolate the EC2 instance\n[T+5m] Notify development team, provide role ARN and finding details\n[T+10m] Validate via CloudTrail that AccessDenied responses are occurring\n[T+15m] Confirm legitimate workloads are unaffected\n[T+4h] Development team deploys fix\n[T+4h+5m] Remove revocation inline policy\n[T+4h+10m] Full post-incident review\n```\n\nThe 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.\n\nOne IAM inline policy with a `DateLessThan`\n\ncondition on `aws:TokenIssueTime`\n\nis the smallest possible blast radius with the fastest possible effect.\n\n**References:**", "url": "https://wpnews.pro/news/stopping-s3-data-exfiltration-in-real-time-a-step-by-step-incident-response", "canonical_source": "https://dev.to/nghidanh2005/-stopping-s3-data-exfiltration-in-real-time-a-step-by-step-incident-response-2jp", "published_at": "2026-08-23 07:45:16+00:00", "updated_at": "2026-08-23 08:13:38.110197+00:00", "lang": "en", "topics": ["ai-safety", "ai-policy"], "entities": ["AWS", "GuardDuty", "IAM", "EC2", "S3", "CloudTrail"], "alternates": {"html": "https://wpnews.pro/news/stopping-s3-data-exfiltration-in-real-time-a-step-by-step-incident-response", "markdown": "https://wpnews.pro/news/stopping-s3-data-exfiltration-in-real-time-a-step-by-step-incident-response.md", "text": "https://wpnews.pro/news/stopping-s3-data-exfiltration-in-real-time-a-step-by-step-incident-response.txt", "jsonld": "https://wpnews.pro/news/stopping-s3-data-exfiltration-in-real-time-a-step-by-step-incident-response.jsonld"}}