Back to blog
AWSBest PracticesCloud SecurityStorage

S3 ACL-Based Public Read Not Blocked: Closing the Other Door to Public Buckets

Your S3 bucket blocks public policies but leaves ACLs open, risking data exposure. Learn why it matters and how to enable BlockPublicAcls and IgnorePublicAcls.

TL;DR

Your S3 bucket blocks public access through policies but leaves the ACL doors open, which means a publicly readable object ACL can still expose your data. Fix it by enabling all four Block Public Access settings, especially BlockPublicAcls and IgnorePublicAcls.

S3 Block Public Access has four independent toggles, and a lot of teams only flip two of them. They lock down bucket policies, see "public access blocked" in the console, and move on. The problem is that S3 has two completely separate mechanisms for granting public access: bucket and object policies, and bucket and object ACLs. If you only block one, the other can still leak data.

This check flags exactly that gap. The bucket blocks policy-based public access, but the ACL-based controls (BlockPublicAcls and IgnorePublicAcls) are missing or disabled, so a public-read ACL on the bucket or any object inside it will still take effect.


What this check detects

Every S3 bucket has a Public Access Block configuration made up of four boolean settings:

  • BlockPublicAcls — rejects PUT requests that include a public ACL, so you cannot add new public ACLs.
  • IgnorePublicAcls — ignores any public ACLs that already exist on the bucket or its objects, neutralizing them at evaluation time.
  • BlockPublicPolicy — rejects bucket policies that grant public access.
  • RestrictPublicBuckets — restricts access to buckets that have a public policy so only the bucket owner and AWS services can reach them.

This check fires when BlockPublicPolicy and RestrictPublicBuckets are set, but one or both of the ACL settings are not. In that state your policies are safe, but anyone with permission to set ACLs, or any pre-existing public ACL, can still make objects world-readable.

Note: ACLs are a legacy access control mechanism that predates IAM and bucket policies. AWS now recommends disabling ACLs entirely via Object Ownership set to Bucket owner enforced, but plenty of older buckets still have ACLs active, which is where this risk lives.


Why it matters

The danger with ACL-based exposure is that it can happen object by object, quietly, without anyone touching the bucket policy. A bucket policy is a single, reviewable document. ACLs are scattered across potentially millions of objects, and each one can carry its own grant to AllUsers (anonymous) or AuthenticatedUsers (any AWS account on earth).

Here are the realistic ways this bites you:

  • Upload tooling sets public ACLs. Plenty of SDKs, scripts, and CMS plugins default to x-amz-acl: public-read when uploading. If BlockPublicAcls is off, those objects go public the moment they land, regardless of how locked down your policy is.
  • Pre-existing public ACLs survive a policy lockdown. Teams often clean up the bucket policy after an audit and assume the bucket is private. Objects uploaded with public ACLs months earlier stay public unless IgnorePublicAcls is on.
  • An attacker with PutObjectAcl escalates exposure. A compromised role or over-permissive IAM policy that grants s3:PutObjectAcl lets an attacker mark sensitive objects public for later exfiltration, without ever modifying the bucket policy you are watching.

The business impact is the usual S3 horror story: backups, logs, PII, customer documents, or internal source code sitting behind a public URL. Many of the largest cloud data leaks on record trace back to public S3 access, and ACL-based exposure is a frequent culprit precisely because it is easy to overlook.

Warning: A bucket can report "Objects can be public" in the console even when the bucket policy is fully private. That status reflects ACL settings, not just the policy, so do not assume green-on-policy means the data is safe.


How to fix it

The fix is to enable all four Block Public Access settings on the bucket. Start by checking the current state.

1. Inspect the current configuration

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

A bucket flagged by this check will show something like the following, with the ACL fields set to false:

{
  "PublicAccessBlockConfiguration": {
    "BlockPublicAcls": false,
    "IgnorePublicAcls": false,
    "BlockPublicPolicy": true,
    "RestrictPublicBuckets": true
  }
}

2. Enable all four settings

Danger: Enabling IgnorePublicAcls immediately removes public access from any object currently served via a public ACL. If you have a bucket intentionally serving public assets through ACLs (for example, a legacy static site), this will break those URLs. Confirm the bucket is not meant to be public before applying.

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

3. Verify

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

All four values should now read true.

Console steps

  1. Open the S3 console and select the bucket.
  2. Go to the Permissions tab.
  3. Under Block public access (bucket settings), click Edit.
  4. Check Block all public access (or at minimum the two ACL-related boxes), then save and confirm.

Better fix: disable ACLs entirely

If you do not actually rely on ACLs, the cleanest long-term move is to stop using them. Setting Object Ownership to Bucket owner enforced disables ACLs across the whole bucket, so public-read ACLs become impossible by design.

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

Tip: Combine Bucket owner enforced with full Block Public Access. The first removes ACLs as a mechanism, the second guards policies. Together they close both paths to public access with no remaining gaps.


Fixing it in infrastructure as code

Manual fixes drift. Bake the setting into your IaC so every bucket is born locked down.

Terraform

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

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

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

resource "aws_s3_bucket_ownership_controls" "data" {
  bucket = aws_s3_bucket.data.id
  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

CloudFormation

Resources:
  DataBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-bucket
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        IgnorePublicAcls: true
        BlockPublicPolicy: true
        RestrictPublicBuckets: true

How to prevent it from happening again

Catching this once is fine. Stopping it from recurring across hundreds of buckets is the real goal.

Turn on account-level Block Public Access

The single highest-leverage control is the account-wide setting. It overrides per-bucket configuration and prevents any bucket in the account from being made public, including ones created later.

aws s3control put-public-access-block \
  --account-id 111122223333 \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Warning: Account-level Block Public Access applies to every bucket in the account with no exceptions. If you genuinely need a public bucket (static website hosting, public datasets), use a dedicated account or front it with CloudFront and an origin access control instead.

Gate it in CI/CD with policy as code

Block non-compliant buckets before they ship. With Checkov, the relevant rules are already built in:

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

Or write an OPA/Conftest rule that fails any plan where the ACL blocks are not set:

package main

deny[msg] {
  resource := input.resource.aws_s3_bucket_public_access_block[name]
  not resource.block_public_acls == true
  msg := sprintf("%s must set block_public_acls = true", [name])
}

deny[msg] {
  resource := input.resource.aws_s3_bucket_public_access_block[name]
  not resource.ignore_public_acls == true
  msg := sprintf("%s must set ignore_public_acls = true", [name])
}

Enforce with an SCP

For organizations, a Service Control Policy can deny anyone the ability to weaken Block Public Access settings, so even an admin cannot accidentally undo it.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyDisablingS3PublicAccessBlock",
      "Effect": "Deny",
      "Action": "s3:PutAccountPublicAccessBlock",
      "Resource": "*"
    }
  ]
}

Tip: Lensix continuously evaluates this check across every bucket and region, so newly created or modified buckets that slip past your IaC gates get flagged automatically rather than waiting for the next manual audit.


Best practices

  • Treat all four settings as one unit. There is rarely a good reason to enable two of the four. Set them together or disable ACLs entirely.
  • Disable ACLs by default. Use Object Ownership Bucket owner enforced on new buckets. ACLs are legacy and create exactly this class of hidden exposure.
  • Prefer account-level controls. Per-bucket settings drift over time. The account-wide block is set once and protects everything.
  • Serve public content through CloudFront. If you need public assets, put them behind a CloudFront distribution with origin access control and keep the bucket itself fully private.
  • Audit IAM for s3:PutObjectAcl and s3:PutBucketAcl. These permissions let principals create public ACLs. Restrict them to the roles that genuinely need them.
  • Scan IaC in CI, monitor live state continuously. Shift-left checks catch most issues, but runtime monitoring catches the ones created outside your pipeline.

The short version: blocking policy-based public access is half the job. ACLs are the other door, and leaving it propped open is how perfectly private-looking buckets end up in the news. Enable all four settings, disable ACLs where you can, and enforce it at the account level so the problem cannot come back.