Back to blog
AWSCloud SecurityMonitoring & LoggingOperations & ComplianceStorage

No Alarm for S3 Bucket Policy Changes (CIS 3.8): Detect and Fix

Learn why a missing CloudWatch alarm for S3 bucket policy changes (CIS 3.8) is risky and how to fix it with CLI, console, and Terraform steps.

TL;DR

This check flags AWS accounts that have no CloudWatch alarm watching for S3 bucket policy changes (CIS 3.8). Without it, an attacker or careless engineer can open a bucket to the public and you would never know. Fix it by creating a metric filter on your CloudTrail log group and wiring it to an SNS-backed CloudWatch alarm.

S3 bucket policies are one of the most direct paths to a data breach in AWS. A single edit can flip a private bucket holding customer PII into one that is readable by anyone on the internet. The change takes seconds, leaves a trace in CloudTrail, and is trivially easy to miss if nobody is looking. The CIS AWS Foundations Benchmark calls this out specifically in control 3.8, and Lensix surfaces it as account_alarm_s3policychanges.

This post walks through what the check looks for, why an unmonitored policy change is dangerous, and how to set up the alarm with the CLI, the console, and Terraform.


What this check detects

The check inspects your account for a CloudWatch alarm tied to a metric filter that matches S3 bucket policy API calls in CloudTrail. Specifically, it looks for monitoring on these events:

  • PutBucketPolicy — sets or replaces a bucket policy
  • DeleteBucketPolicy — removes a bucket policy entirely
  • PutBucketAcl and PutBucketCors — related access-control changes (often grouped in the same filter)

If there is no metric filter matching these events, or there is a filter but no alarm attached to it, the check fails. Passing requires the full chain: a multi-region CloudTrail trail delivering to CloudWatch Logs, a metric filter on that log group, and a CloudWatch alarm pointing at the resulting metric with an active SNS subscription.

Note: This is a detective control, not a preventive one. The alarm does not stop a bad policy change from happening. It tells you quickly when one does, so you can react before the exposure turns into an incident.


Why it matters

Public S3 buckets remain one of the most common causes of large-scale data leaks. The pattern repeats year after year: a bucket policy or ACL is loosened, sensitive objects become world-readable, and a researcher (or attacker) finds it before the team does.

Consider a realistic chain of events:

  1. An attacker compromises a developer's IAM credentials through a phishing email or a leaked access key in a public repo.
  2. They call PutBucketPolicy on a bucket holding application backups, adding a statement that grants s3:GetObject to Principal: "*".
  3. They quietly exfiltrate the data over hours or days.
  4. Because the change was a valid API call from valid credentials, nothing blocks it and nothing pages anyone.

The same risk exists without any attacker. An engineer debugging a CDN issue might widen a bucket policy "just to test" and forget to revert it. The technical event is identical, and so is the exposure.

With control 3.8 in place, that PutBucketPolicy call generates a CloudWatch data point, trips an alarm, and fires an SNS notification to your security channel within a minute or two. That is the difference between a contained near-miss and a breach disclosure.

Warning: S3 Block Public Access (BPA) at the account level prevents most public-grant policies from taking effect, and you should absolutely enable it. But BPA does not cover every cross-account or condition-based policy change, and it can be disabled by anyone with the right permissions. The alarm catches the change regardless of whether BPA blocked it.


How to fix it

The fix has three pieces: a CloudTrail trail sending logs to CloudWatch Logs, a metric filter on that log group, and an alarm with an SNS topic. If you already have a CIS-compliant trail wired to CloudWatch Logs (control 3.1), you only need the last two steps.

Step 1: Confirm CloudTrail is delivering to CloudWatch Logs

aws cloudtrail describe-trails \
  --query 'trailList[].{Name:Name,LogGroup:CloudWatchLogsLogGroupArn,MultiRegion:IsMultiRegionTrail}' \
  --output table

You need a trail where MultiRegion is true and LogGroup is populated. If LogGroup is empty, attach a log group before continuing.

Step 2: Create the metric filter

This filter matches S3 access-control changes and increments a custom metric each time one occurs. Replace my-cloudtrail-log-group with your actual log group name.

aws logs put-metric-filter \
  --log-group-name "my-cloudtrail-log-group" \
  --filter-name "S3BucketPolicyChanges" \
  --filter-pattern '{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }' \
  --metric-transformations \
      metricName=S3BucketPolicyChanges,metricNamespace=CISBenchmark,metricValue=1,defaultValue=0

Step 3: Create an SNS topic and subscribe to it

# Create the topic
TOPIC_ARN=$(aws sns create-topic --name cis-security-alarms --query 'TopicArn' --output text)

# Subscribe an email address (you must confirm the subscription via email)
aws sns subscribe \
  --topic-arn "$TOPIC_ARN" \
  --protocol email \
  --notification-endpoint [email protected]

Step 4: Create the alarm

aws cloudwatch put-metric-alarm \
  --alarm-name "CIS-3.8-S3BucketPolicyChanges" \
  --alarm-description "Alarm for S3 bucket policy changes (CIS 3.8)" \
  --metric-name "S3BucketPolicyChanges" \
  --namespace "CISBenchmark" \
  --statistic Sum \
  --period 300 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --evaluation-periods 1 \
  --treat-missing-data notBreaching \
  --alarm-actions "$TOPIC_ARN"

Once the email subscription is confirmed, the next S3 policy change in your account will increment the metric and trip the alarm.

Tip: Pipe SNS into something your team actually watches. An email that lands in a shared inbox nobody reads is barely better than no alarm at all. Route SNS to Slack or PagerDuty through a Lambda or an AWS Chatbot integration so the alert shows up where people are already paying attention.

Console route

If you prefer the console:

  1. Open CloudWatch → Log groups and select your CloudTrail log group.
  2. Choose Metric filters → Create metric filter and paste the filter pattern from Step 2.
  3. Name the metric S3BucketPolicyChanges in the CISBenchmark namespace.
  4. Go to Alarms → Create alarm, pick that metric, set the threshold to >= 1 over a 5-minute period.
  5. Attach an SNS topic for notifications and finish.

How to prevent it from coming back

Manually clicking through the console means the alarm only exists until someone deletes it or stands up a new account without it. Bake the control into infrastructure as code so every account gets it by default.

Terraform

resource "aws_cloudwatch_log_metric_filter" "s3_policy_changes" {
  name           = "S3BucketPolicyChanges"
  log_group_name = var.cloudtrail_log_group_name
  pattern        = "{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }"

  metric_transformation {
    name          = "S3BucketPolicyChanges"
    namespace     = "CISBenchmark"
    value         = "1"
    default_value = "0"
  }
}

resource "aws_cloudwatch_metric_alarm" "s3_policy_changes" {
  alarm_name          = "CIS-3.8-S3BucketPolicyChanges"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 1
  metric_name         = "S3BucketPolicyChanges"
  namespace           = "CISBenchmark"
  period              = 300
  statistic           = "Sum"
  threshold           = 1
  treat_missing_data  = "notBreaching"
  alarm_description   = "Alarm for S3 bucket policy changes (CIS 3.8)"
  alarm_actions       = [aws_sns_topic.security_alarms.arn]
}

resource "aws_sns_topic" "security_alarms" {
  name = "cis-security-alarms"
}

Put this module in your account baseline (Control Tower, Landing Zone, or your own org-level Terraform) so new accounts inherit it the moment they are provisioned.

Tip: Add a policy-as-code rule that fails CI if the alarm resource is missing. A simple OPA/Conftest or Checkov custom policy that asserts the presence of an aws_cloudwatch_metric_alarm matching the S3 policy metric stops anyone from merging a baseline change that drops the control.

Catch drift continuously

IaC guards the initial state, but alarms can still be deleted out of band during an incident or a "quick fix." Lensix re-runs account_alarm_s3policychanges on a schedule, so if the alarm disappears you find out at the next scan rather than during a breach post-mortem.


Best practices

  • Layer detection with prevention. Enable account-level S3 Block Public Access and use SCPs to forbid disabling it. The alarm is your backstop, not your only line of defense.
  • Group the CIS 3.x alarms. Control 3.8 is one of roughly a dozen CloudWatch metric filter controls in the benchmark (root login, IAM policy changes, security group changes, and so on). Deploy them together as a single module so they stay consistent.
  • Test the alarm. Make a benign, reversible policy change on a test bucket and confirm the notification actually arrives. An untested alarm is an assumption, not a control.
  • Send events to your SIEM too. Forward the matched CloudTrail events to a central log store so you have the full context (who, from where, what changed) when you investigate an alert.
  • Keep the trail healthy. All of this depends on CloudTrail actually logging. Monitor for StopLogging and validate log file integrity so an attacker cannot blind your detection before making the policy change.

Danger: Never respond to an S3 policy alarm by assuming it was a false positive. Treat every unexpected PutBucketPolicy or DeleteBucketPolicy on a sensitive bucket as a potential exposure until you have confirmed who made the change and why. Reverting first and asking questions later is the safer order of operations.

Setting up this alarm takes about ten minutes and closes one of the cheapest, highest-value detection gaps in an AWS account. Combine it with Block Public Access and an IaC baseline, and a stray bucket policy change goes from a silent risk to an alert you can act on.