This check flags AWS accounts where no CloudTrail trail captures S3 object-level data events, leaving GetObject, PutObject, and DeleteObject calls invisible. Fix it by adding a data event selector to an existing trail so you can answer "who read or deleted this object" during an investigation.
By default, CloudTrail records management events: API calls that create, modify, or delete your AWS resources. What it does not record by default is data plane activity, the high-volume operations that touch the contents of your resources. For S3 that means object-level operations like GetObject, PutObject, and DeleteObject go completely unlogged unless you explicitly turn them on.
This check looks across your trails and confirms whether at least one of them is configured to log S3 data events. If none are, you have a blind spot exactly where attackers tend to operate: reading your data, exfiltrating it, or wiping it.
What this check detects
The check inspects every CloudTrail trail in the account and evaluates its event selectors (or advanced event selectors) for S3 data event coverage. It fails when no trail has a selector that includes the AWS::S3::Object resource type.
Note: CloudTrail splits activity into two buckets. Management events (logged free, on by default) cover the control plane: things like CreateBucket or PutBucketPolicy. Data events (off by default, billed per event) cover the data plane: the actual reads and writes against objects inside the bucket. Knowing a bucket policy changed is useful. Knowing which objects were downloaded after that change is what closes the case.
A passing trail has an event selector that looks something like this:
{
"EventSelectors": [
{
"ReadWriteType": "All",
"IncludeManagementEvents": true,
"DataResources": [
{
"Type": "AWS::S3::Object",
"Values": ["arn:aws:s3"]
}
]
}
]
}
Why it matters
S3 is where the data lives: backups, customer exports, application uploads, logs, build artifacts. When something goes wrong, the question is almost never "did someone change a bucket setting." It is "what did they take, and what did they destroy." Without data event logging, you cannot answer either.
The exfiltration scenario
An attacker compromises an IAM role through a leaked access key or an SSRF flaw in an application. The role has s3:GetObject on a bucket full of customer records. The attacker enumerates and downloads every object over a few hours. With only management events logged, your trail shows nothing. There is no record of which objects were read, how many, or when. During breach disclosure you are forced to assume the entire bucket was compromised because you have no evidence to scope it narrower.
The ransomware and destruction scenario
An attacker with s3:DeleteObject or s3:PutObject deletes objects or overwrites them with encrypted versions. If you logged data events, you have the exact timeline and object keys, which makes recovery from versioning or backups precise. If you did not, you are guessing.
Warning: Data events are not free. CloudTrail charges per data event recorded (roughly $0.10 per 100,000 events at time of writing), plus S3 storage for the log files. On a high-traffic bucket this adds up fast. Scope your selectors to the buckets that actually hold sensitive data rather than blanket-logging every object operation in the account.
Compliance pressure
Frameworks like PCI DSS, HIPAA, and SOC 2 expect audit trails for access to sensitive data. Management-event-only logging usually fails those requirements when the data in question lives in S3. Auditors want to see who accessed what.
How to fix it
You add a data event selector to an existing trail. You do not need a new trail. The steps below assume you already have a multi-region management trail in place, which most accounts do.
Option 1: AWS CLI
First, find your trail name:
aws cloudtrail describe-trails \
--query 'trailList[].{Name:Name,S3:S3BucketName,MultiRegion:IsMultiRegionTrail}' \
--output table
To log data events for a specific sensitive bucket (recommended over logging everything):
aws cloudtrail put-event-selectors \
--trail-name my-management-trail \
--advanced-event-selectors '[
{
"Name": "Log S3 object events for sensitive buckets",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Data"] },
{ "Field": "resources.type", "Equals": ["AWS::S3::Object"] },
{ "Field": "resources.ARN", "StartsWith": ["arn:aws:s3:::customer-data-prod/"] }
]
}
]'
Danger: put-event-selectors replaces the entire selector configuration on the trail. If your trail already has management event selectors or other data selectors, you must include them in the same command or you will silently drop existing logging. Run aws cloudtrail get-event-selectors --trail-name my-management-trail first and merge your changes into the existing config.
If you do want broad coverage across all buckets, drop the ARN field selector:
aws cloudtrail put-event-selectors \
--trail-name my-management-trail \
--advanced-event-selectors '[
{
"Name": "Log all S3 object data events",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Data"] },
{ "Field": "resources.type", "Equals": ["AWS::S3::Object"] }
]
},
{
"Name": "Keep management events",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Management"] }
]
}
]'
Option 2: Console
- Open CloudTrail and go to Trails.
- Select your existing trail and choose Edit under the data events section.
- Click Add data event type and select S3.
- Choose Read, Write, or both, then either select all buckets or specify individual buckets and prefixes.
- Save. New events start flowing within minutes.
Option 3: Terraform
resource "aws_cloudtrail" "main" {
name = "org-management-trail"
s3_bucket_name = aws_s3_bucket.cloudtrail.id
is_multi_region_trail = true
enable_log_file_validation = true
include_global_service_events = true
advanced_event_selector {
name = "Log S3 object data events for sensitive buckets"
field_selector {
field = "eventCategory"
equals = ["Data"]
}
field_selector {
field = "resources.type"
equals = ["AWS::S3::Object"]
}
field_selector {
field = "resources.ARN"
starts_with = ["arn:aws:s3:::customer-data-prod/"]
}
}
advanced_event_selector {
name = "Log management events"
field_selector {
field = "eventCategory"
equals = ["Management"]
}
}
}
Tip: In AWS Organizations, set this up on the org-level trail in the management account. One configuration then captures data events across every member account, and individual account owners cannot accidentally disable it.
How to prevent it from happening again
Catching this once is good. Making sure a trail never ships without data event coverage is better.
Enforce it in IaC review
If your trails are managed in Terraform, add a policy check with OPA/Conftest or a tool like tfsec / checkov that fails the pipeline when an aws_cloudtrail resource has no AWS::S3::Object data selector. A simple Conftest rule:
package main
deny[msg] {
resource := input.resource.aws_cloudtrail[name]
not has_s3_data_event(resource)
msg := sprintf("CloudTrail '%s' has no S3 data event selector", [name])
}
has_s3_data_event(resource) {
selector := resource.advanced_event_selector[_]
fs := selector.field_selector[_]
fs.field == "resources.type"
fs.equals[_] == "AWS::S3::Object"
}
Detect drift at runtime
IaC checks only cover resources created through IaC. Someone with console access can still edit a trail by hand. An AWS Config rule or a scheduled Lensix scan catches that drift and alerts you instead of waiting for the next audit.
Tip: Pair the data event trail with a CloudWatch metric filter and alarm on unusual GetObject volume from a single principal. Logging the events is step one. Getting paged when someone downloads 50,000 objects in ten minutes is what actually catches the breach in progress.
Best practices
- Scope before you blanket. Identify the buckets holding regulated or sensitive data and log those first. Account-wide data logging is expensive and noisy, and the signal you care about gets buried.
- Log both reads and writes for sensitive buckets. Reads tell you about exfiltration; writes and deletes tell you about tampering. You need both for a complete picture.
- Enable log file validation. Set
enable_log_file_validation = trueso you can prove the logs were not tampered with after the fact. This matters during legal and compliance proceedings. - Ship logs to a locked-down account. Send CloudTrail output to an S3 bucket in a separate logging account with restrictive bucket policies and object lock. An attacker who compromises the workload account should not be able to delete the evidence.
- Send data events to CloudWatch Logs or a SIEM. Raw S3 log files are fine for cold storage, but real-time detection needs the events in a queryable, alertable place.
- Review selectors when you add buckets. A new sensitive bucket created six months from now will not be covered by a prefix-scoped selector. Bake the review into your bucket provisioning process.
Data event logging is one of those controls that costs a little every day and pays for itself entirely on the one day you have an incident. The accounts that can scope a breach to "these 200 objects between 2:14 and 2:31 AM" are the ones that turned this on before they needed it.

