Back to blog
AWSBest PracticesCloud SecurityStorage

S3 ACL-Based Public Read Not Blocked: Closing the Hidden Bucket Gap

Your S3 bucket blocks public policies but not public ACLs, leaving a quiet exposure gap. Learn why it matters and how to enable all four Block Public Access settings.

TL;DR

Your S3 bucket blocks public access through policies but still allows ACLs to grant public read. A misconfigured or attacker-modified ACL can expose your objects. Fix it by enabling all four S3 Block Public Access settings, including BlockPublicAcls and IgnorePublicAcls.

S3 Block Public Access (BPA) is the safety net that prevents accidental public exposure of your data. But it is not one switch, it is four. Many buckets enable the two policy-based settings and leave the two ACL-based settings off, which creates a quiet gap. This check flags buckets where policy-based public access is blocked but ACL-based access is not, leaving the door open for a publicly readable ACL to take effect.

If you have ever assumed "Block Public Access is on, so we are covered," this is worth a few minutes of your attention.


What this check detects

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

  • BlockPublicAcls — rejects PUT requests that include a public ACL
  • IgnorePublicAcls — ignores any public ACLs already attached to the bucket or its objects
  • BlockPublicPolicy — rejects bucket policies that grant public access
  • RestrictPublicBuckets — restricts access to buckets that are already public via policy

This check specifically looks for buckets where the policy-based pair (BlockPublicPolicy and/or RestrictPublicBuckets) is enabled, but one or both of the ACL-based settings (BlockPublicAcls or IgnorePublicAcls) is set to false or missing.

In that state, AWS will reject a public bucket policy, but it will happily honor an object or bucket ACL that grants READ to AllUsers (everyone on the internet) or AuthenticatedUsers (any AWS account).

Note: ACLs are the legacy access control mechanism in S3. AWS now recommends disabling them entirely via Object Ownership set to BucketOwnerEnforced, but plenty of older buckets still have ACLs enabled, and that is exactly where this gap bites.


Why it matters

The danger here is that the partial configuration gives a false sense of security. Teams see the bucket policy locked down and stop looking. Meanwhile, two realistic paths to exposure remain open.

1. A misconfigured object upload

Applications that upload objects to S3 sometimes set ACLs on the way in. A bug, a copied code sample, or a careless default can attach public-read to uploaded objects:

aws s3 cp report.pdf s3://my-bucket/report.pdf --acl public-read

With BlockPublicAcls off, that command succeeds and the object becomes world-readable. The bucket policy never enters the picture.

2. An attacker with limited write access

Suppose an attacker compromises a set of IAM credentials that can write objects and modify ACLs but cannot edit bucket policies. They cannot make the bucket public through a policy, because BlockPublicPolicy stops them. But they can flip object ACLs to public and quietly exfiltrate data through plain HTTPS URLs, no AWS credentials required by whoever fetches them.

Danger: Publicly readable ACLs are a common root cause of S3 data leaks. Once an object is exposed via a public ACL, it can be crawled and indexed by search engines and scanning tools within hours. Assume that anything exposed has already been copied.

The business impact is the same as any S3 leak: exposure of customer data, credentials baked into config files, internal documents, or backups. The difference is that this gap survives the most common audit ("is the bucket policy public?") because the answer is no.


How to fix it

The fix is to enable all four Block Public Access settings on the bucket. There is rarely a good reason to leave the ACL settings off.

Option A: Enable Block Public Access via CLI

First, check the current state so you know what you are changing:

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

Then apply all four settings:

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

Warning: If anything legitimately relies on public ACLs (for example, a static asset bucket fronted directly by S3), enabling IgnorePublicAcls will immediately cut off that access. Verify intended public access is served through CloudFront with Origin Access Control, not raw public ACLs, before flipping these on in production.

Option B: Disable ACLs entirely (recommended)

The cleaner long-term fix is to remove ACLs from the equation by setting Object Ownership to BucketOwnerEnforced. This makes ACLs inert, so a public ACL cannot grant access even if one is attached:

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

You should still enable Block Public Access alongside this for defense in depth, but with ACLs disabled the specific ACL-based exposure path is closed for good.

Option C: 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 (this enables all four sub-settings), or at minimum enable the two ACL settings.
  5. Save and confirm by typing confirm when prompted.
  6. Under Object Ownership, click Edit and select ACLs disabled (recommended).

Find existing public ACLs before you lock down

Enabling IgnorePublicAcls hides public ACLs but does not remove them. To find and clean up objects that already have public ACLs, inspect them:

aws s3api get-object-acl --bucket my-bucket --key report.pdf

Look for a grant to http://acs.amazonaws.com/groups/global/AllUsers. Reset an object's ACL to private with:

aws s3api put-object-acl --bucket my-bucket --key report.pdf --acl private

Fix it in infrastructure as code

If you manage S3 with Terraform, define the Block Public Access settings explicitly so they cannot drift. The aws_s3_bucket_public_access_block resource controls all four flags:

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

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

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
}

For CloudFormation, the equivalent lives on the bucket resource:

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

Tip: Set Block Public Access at the account level too, using aws s3control put-public-access-block. Account-level settings apply to every bucket and act as a backstop for buckets created outside your IaC pipeline.


How to prevent it from happening again

One-time fixes drift. The reliable approach is to make the secure configuration the default and to block insecure ones before they reach production.

1. Enforce account-level Block Public Access

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

With this set, no individual bucket can be made public via ACL, even if someone forgets the per-bucket settings.

2. Gate IaC in CI/CD

Run a policy-as-code scanner against your Terraform or CloudFormation plans in the pipeline. With Checkov, the relevant rule (CKV2_AWS_6) fails any bucket missing a Block Public Access block:

checkov -d ./infra --check CKV2_AWS_6

Fail the build on any violation so the misconfiguration never merges.

3. Use an SCP as a hard guardrail

A Service Control Policy can deny any attempt to weaken Block Public Access across the entire organization:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyDisablingBPA",
      "Effect": "Deny",
      "Action": [
        "s3:PutAccountPublicAccessBlock",
        "s3:PutBucketPublicAccessBlock"
      ],
      "Resource": "*",
      "Condition": {
        "Null": {
          "s3:PublicAccessBlockConfiguration.IgnorePublicAcls": "false"
        }
      }
    }
  ]
}

4. Monitor continuously

Use a tool like Lensix to scan buckets on a schedule and alert when any of the four BPA flags is missing. Continuous detection closes the window between a change and the moment you notice it, which for S3 leaks can be the difference between a near miss and an incident.


Best practices

  • Treat ACLs as legacy. Set Object Ownership to BucketOwnerEnforced on new buckets and migrate old ones. ACLs add complexity and exposure with little upside in modern designs.
  • Enable all four BPA settings, always. Splitting policy and ACL controls only makes sense in rare edge cases. Default to all four on.
  • Serve public content through CloudFront. If you need to publish assets, use CloudFront with Origin Access Control and keep the bucket fully private. Never rely on public ACLs for delivery.
  • Apply controls at the account level. Bucket-level settings are easy to miss on new buckets. Account-level BPA covers everyone.
  • Audit existing ACLs before locking down. Ignoring public ACLs hides them but leaves them in place. Clean them up so a future change to ownership settings does not re-expose data.
  • Bake it into IaC and CI/CD. The most durable fix is the one a pipeline enforces on every change.

This check is a reminder that "Block Public Access is enabled" is not a yes or no question. It is four questions, and a single missing flag can undo the protection you thought you had. Turn on all four, disable ACLs where you can, and let automation keep them that way.