S3 data exfiltration detection: what the logs can prove
Prove or rule out S3 data theft: CloudTrail data events, S3 server access logs, public bucket policies, shared snapshots, and what you cannot see without them.
TL;DR. "Did they take the data?" can only be answered for S3 objects if CloudTrail S3 data events or S3 server access logs were enabled before the incident. With them: count GetObject per principal, bucket and hour, sum bytesTransferredOut / Bytes Sent, and list the object keys. Without them: you can show the attacker could read (permissions, ListBuckets, ListObjects is a data event too) and whether a bucket was made public or a snapshot shared (management events), but not which objects left. Also check the channels that need no download at all: public policies, ACLs, shared EBS/RDS snapshots and AMIs.
Data theft is the question lawyers, customers and regulators ask first, and the one where log coverage matters most. This post covers what each AWS log source can prove about S3 and snapshot exfiltration, and how to report honestly when it cannot.
Two sources for object reads
| CloudTrail S3 data events | S3 server access logs | |
|---|---|---|
| Enabled by default | No — must be selected on a trail or event data store (docs) | No — per bucket (docs) |
| Format | CloudTrail JSON, same files as management events | Space-delimited text, one line per request |
| Identity | Full userIdentity (ARN, access key, session) | Requester ARN or - for anonymous |
| Volume transferred | additionalEventData.bytesTransferredOut | Bytes Sent field |
| Delivery | About five minutes, like other CloudTrail events | Best effort, usually within a few hours; completeness not guaranteed |
| Cost | Charged per data event | Storage of the log objects |
Having both is ideal: CloudTrail gives reliable identity, access logs give an independent second record. Having either is enough to answer the question.
Reading CloudTrail data events
A data event for an object read looks like this (trimmed):
{
"eventSource": "s3.amazonaws.com",
"eventName": "GetObject",
"eventCategory": "Data",
"userIdentity": {"arn": "arn:aws:iam::111122223333:user/backup-admin", "accessKeyId": "AKIA..."},
"sourceIPAddress": "203.0.113.77",
"requestParameters": {"bucketName": "acme-customer-exports", "key": "exports/customers-0001.csv.gz"},
"additionalEventData": {"bytesTransferredOut": 12345678}
}
Questions and how to answer them:
- How many objects? Count distinct
requestParameters.keyper principal and bucket. - How much data? Sum
bytesTransferredOut. Partial reads (range requests) count their actual bytes. - Which objects? The key list is your disclosure inventory. Export it.
- From where?
sourceIPAddressanduserAgent: a boto3 client on a VPS pulling 300 objects in 20 minutes is not your backup job. - Before the reads:
ListObjects/ListObjectsV2(also data events), and management eventsListBuckets,GetBucketPolicy,GetBucketAcl,GetBucketLocationfrom the same principal.
The mapping is MITRE ATT&CK T1530 Data from Cloud Storage.
Reading S3 server access logs
Server access logs have one line per request; the fields that matter here are the bucket, time, remote IP, requester, operation, key, HTTP status and bytes sent (log format).
79a59df9... acme-customer-exports [14/Sep/2026:09:15:04 +0000] 203.0.113.77
arn:aws:iam::111122223333:user/backup-admin 3E57427F3EXAMPLE REST.GET.OBJECT
exports/2026/customers/customers-0001.csv.gz "GET /exports/... HTTP/1.1" 200 - 12345678 ...
(Wrapped here for readability; each record is a single line.)
Filter on REST.GET.OBJECT with status 200 or 206, group by requester and bucket. A requester of - means an anonymous request — expected for a public website bucket, a leak for anything else.
Exfiltration without downloads
Some of the most effective data theft on AWS never produces a GetObject in your account, because the attacker makes the data reachable and copies it from elsewhere. These are all management events, so they are visible even without data events:
| Technique | Events | ATT&CK |
|---|---|---|
| Bucket made public | PutBucketPolicy with "Principal": "*" and no restricting condition | T1530 |
| Public ACL | PutBucketAcl / PutObjectAcl granting AllUsers or AuthenticatedUsers | T1530 |
| Guard rail removed | DeleteBucketPublicAccessBlock, DeleteAccountPublicAccessBlock, weakened PutPublicAccessBlock | T1530 |
| EBS snapshot shared | ModifySnapshotAttribute adding a foreign account or all to createVolumePermission | T1537 |
| AMI shared | ModifyImageAttribute with launchPermission | T1537 |
| RDS snapshot shared | ModifyDBSnapshotAttribute / ModifyDBClusterSnapshotAttribute with restore | T1537 |
| Replication to an outside bucket | PutBucketReplication | T1537 |
A shared snapshot means the data can be copied into the attacker's account with no further trace in yours. Treat its content as disclosed.
When you have neither source
Be precise in the report. You can state:
- which principals had permission to read which buckets (IAM policies, bucket policies);
- whether the attacker listed buckets or read bucket policies (management events);
- whether any bucket was made public or any snapshot shared;
- whether network flows from your instances show large outbound transfers (VPC Flow Logs analysis) — useful for data staged on EC2, not for direct S3 downloads, which do not traverse your VPC.
You cannot state that objects were not read. "No evidence of access" is only meaningful when the evidence could have existed. The limitations post covers how to write this.
Notification clock
If personal data may be involved, legal deadlines start running from awareness, not from the end of the investigation — under the GDPR, 72 hours for notifying the supervisory authority (Article 33). Involve your data protection officer early, and hand them the object list as soon as you have it.
In the analyzer
AWS Forensics flags bulk downloads from either source (100+ objects from one bucket by one principal within an hour), anonymous downloads from public addresses, public bucket policies and ACLs, Block Public Access removed, snapshots or AMIs shared with other accounts, and secrets read in bulk. The coverage notes say explicitly when neither data events nor access logs were provided. Pivot on the bucket in the Entities tab to see every request against it, from both sources. For the full context, start from the incident response overview.