Back to blog
AWSBest PracticesCloud SecurityStorage

S3 ACL-Based Public Read Not Blocked: Closing the Gap Policy Controls Miss

Your S3 bucket blocks public policies but not public ACLs, leaving data world-readable. Learn how to detect, fix, and prevent ACL-based public exposure on AWS.

TL;DR

This check finds S3 buckets that block public access via bucket policies but leave the ACL-based controls (BlockPublicAcls and IgnorePublicAcls) turned off, so a publicly readable ACL can still expose your objects. Enable all four Block Public Access settings on the bucket and at the account level to close the gap.

S3 has two distinct ways to grant access to a bucket and its objects: bucket policies (and IAM) on one side, and the older ACL (access control list) mechanism on the other. The Block Public Access feature has a separate toggle for each. It is easy to lock down policy-based access while forgetting the ACL toggles, which leaves a quieter but equally real path to public exposure.

This check flags exactly that situation: a bucket where BlockPublicPolicy and RestrictPublicBuckets are set, but BlockPublicAcls or IgnorePublicAcls is missing or false.


What this check detects

Every S3 bucket can carry a PublicAccessBlock configuration made of four independent flags:

  • BlockPublicAcls — rejects new PUT requests that would set a public ACL on the bucket or an object.
  • IgnorePublicAcls — ignores any public ACLs that already exist, so they have no effect at evaluation time.
  • BlockPublicPolicy — rejects bucket policies that grant public access.
  • RestrictPublicBuckets — restricts cross-account and anonymous access to buckets that already have a public policy.

The check fires when the policy-side flags are present but one or both of the ACL-side flags are not. In practice that means your bucket policy is safe, but someone can still attach a public-read ACL to an object, or an existing public ACL is being honored, and your data is readable by anyone on the internet.

Note: ACLs predate IAM policies. AWS now recommends disabling ACLs entirely on new buckets via the Object Ownership setting BucketOwnerEnforced. Many buckets created before 2023, or migrated from older tooling, still have ACLs enabled, which is why this gap shows up so often.


Why it matters

The dangerous part of ACL-based exposure is how invisible it is. Bucket policies are centralized and easy to audit. ACLs, by contrast, can be set per object, by any principal with s3:PutObjectAcl, and they do not show up when you glance at the bucket policy. A bucket can look completely locked down from the policy view while individual objects are world-readable.

A few concrete scenarios:

  • Accidental public uploads. An application or a developer runs aws s3 cp file s3://bucket/ --acl public-read as a habit, or copies a snippet from an old tutorial. Without BlockPublicAcls, that object is now public.
  • Compromised credentials. An attacker with limited write access cannot edit the bucket policy, but if they have s3:PutObjectAcl they can flip objects to public and exfiltrate data through an anonymous URL, bypassing your network controls entirely.
  • Legacy data left exposed. Objects uploaded years ago with public ACLs stay readable. Setting IgnorePublicAcls neutralizes them without needing to rewrite every object's ACL.

Public S3 buckets remain one of the most common sources of data breaches. The well-known incidents involving voter records, telecom customer data, and configuration backups were, in many cases, the result of permissive ACLs rather than permissive policies.

Warning: Enabling IgnorePublicAcls can break legitimate public-read use cases, for example a bucket serving static website assets directly. If you genuinely need public objects, serve them through CloudFront with an Origin Access Control instead of relying on public ACLs.


How to fix it

The fix is to enable all four Block Public Access settings. The two ACL flags are the ones this check cares about, but setting all four is the safest default and matches what most teams want.

Option 1: AWS CLI

First, inspect the current configuration so you know what you are changing:

aws s3api get-public-access-block \
  --bucket my-bucket

Then apply the full set of restrictions:

Danger: If any application or end user relies on public-read ACLs to fetch objects, this command will immediately cut off that access. Confirm there are no legitimate public consumers before running it against a production bucket.

aws s3api put-public-access-block \
  --bucket my-bucket \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Verify the change took effect:

aws s3api get-public-access-block \
  --bucket my-bucket \
  --query 'PublicAccessBlockConfiguration'

Option 2: AWS Console

  1. Open the S3 console and select the bucket.
  2. Go to the Permissions tab.
  3. Under Block public access (bucket settings), choose Edit.
  4. Tick Block public access to buckets and objects granted through new access control lists (ACLs) and Block public access to buckets and objects granted through any access control lists (ACLs). For full coverage, tick all four boxes.
  5. Choose Save changes and confirm.

Option 3: Disable ACLs entirely (recommended)

If you do not depend on ACLs, the cleanest fix is to disable them by enforcing bucket owner ownership. This removes the entire class of ACL-based exposure rather than just blocking it.

aws s3api put-bucket-ownership-controls \
  --bucket my-bucket \
  --ownership-controls 'Rules=[{ObjectOwnership=BucketOwnerEnforced}]'

Option 4: Terraform

resource "aws_s3_bucket" "this" {
  bucket = "my-bucket"
}

resource "aws_s3_bucket_public_access_block" "this" {
  bucket = aws_s3_bucket.this.id

  block_public_acls       = true
  ignore_public_acls      = true
  block_public_policy      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_ownership_controls" "this" {
  bucket = aws_s3_bucket.this.id

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

Tip: Set Block Public Access at the account level too, so every bucket inherits it by default and individual buckets cannot opt out. Run aws s3control put-public-access-block --account-id 111122223333 --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true. Account-level settings take precedence over bucket-level ones.


How to prevent it from happening again

One-off fixes drift back over time. Bake the controls into the places where buckets get created and changed.

Enforce account-level Block Public Access

Setting the four flags at the account level is the single highest-leverage move. It applies to all current and future buckets and means a misconfigured bucket cannot expose data even if someone forgets the bucket-level settings.

Use an SCP to lock the setting

An AWS Organizations service control policy can prevent anyone from weakening the account-level block:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyDisablingBPA",
      "Effect": "Deny",
      "Action": [
        "s3:PutAccountPublicAccessBlock",
        "s3:PutBucketPublicAccessBlock"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/CloudSecAdmin"
        }
      }
    }
  ]
}

Gate it in CI/CD with policy-as-code

Catch the misconfiguration before it ever reaches an account. A Checkov scan on Terraform plans will fail the build if any of the four flags are missing:

checkov -d . --check CKV_AWS_53,CKV_AWS_54,CKV_AWS_55,CKV_AWS_56

Those four checks map to BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy, and RestrictPublicBuckets respectively. Wire it into your pull request pipeline so a missing ACL flag blocks the merge.

Tip: Lensix runs the s3_public_read_access check continuously across your accounts, so even buckets created outside your IaC pipeline (by a console click or a third-party tool) get flagged. Pair the CI gate for new infrastructure with continuous scanning for everything else.


Best practices

  • Disable ACLs on every new bucket. Use BucketOwnerEnforced Object Ownership. ACLs are legacy, and removing them eliminates this whole category of risk.
  • Set all four BPA flags, not a subset. Partial configurations are exactly what created this finding. Treat the four flags as a single unit.
  • Apply Block Public Access at the account level. Bucket-level settings are easy to forget; account-level settings are inherited and harder to bypass.
  • Serve public content through CloudFront. If you have a genuine need for public objects, front them with CloudFront and an Origin Access Control rather than public ACLs, so the bucket itself stays private.
  • Audit existing object ACLs. When you enable IgnorePublicAcls, public ACLs are neutralized but not removed. Periodically review which objects still carry public ACLs so you understand what would happen if the flag were ever turned off.
  • Restrict s3:PutObjectAcl in your IAM policies. Few workloads need to set object ACLs. Removing the permission reduces the chance of an accidental or malicious public grant.

The recurring theme is defense in depth. Block Public Access at the account level is your floor, disabling ACLs removes the legacy attack surface, and a CI gate plus continuous scanning catches whatever slips through. Get all three in place and the gap this check describes simply cannot reopen.