Back to blog
AWSBest PracticesCloud SecurityStorage

S3 ACL-Based Public Read Not Blocked: Closing the Other Half of Block Public Access

Your S3 bucket blocks public policies but not public ACLs, leaving data exposed. Learn how to enable full Block Public Access and prevent ACL-based leaks.

TL;DR

Your S3 bucket blocks public access through bucket policies but leaves ACL-based public access unblocked, so a single object or bucket ACL can still expose data to the world. Turn on all four Block Public Access settings, with BlockPublicAcls and IgnorePublicAcls set to true.

S3 has two separate mechanisms for granting public access: bucket policies and access control lists (ACLs). Block Public Access (BPA) is the safety net that overrides both, but only if you enable the right combination of settings. This check fires when you have closed the policy-based door but left the ACL-based door open. The result is a bucket that looks locked down at a glance yet can still serve objects publicly the moment someone applies a public-read ACL.


What this check detects

Every S3 bucket has a PublicAccessBlock configuration made up of four independent flags:

  • BlockPublicAcls — rejects new public ACLs on the bucket and its objects.
  • IgnorePublicAcls — ignores any public ACLs that already exist.
  • BlockPublicPolicy — rejects bucket policies that grant public access.
  • RestrictPublicBuckets — restricts access to buckets with public policies to authorized principals.

This check looks for buckets where the two policy-oriented flags are set but one or both ACL-oriented flags (BlockPublicAcls, IgnorePublicAcls) are missing or set to false. In that state, a public-read ACL on the bucket or on any individual object will take effect, regardless of how tight your bucket policy is.

Note: ACLs and policies are evaluated independently. A bucket policy that denies public access does not undo a public-read ACL on an object. The two systems coexist, which is exactly why partial Block Public Access coverage is dangerous.


Why it matters

Object ACLs are the classic source of accidental S3 exposure. They are easy to set, they apply per object, and they are invisible unless you go looking. A few realistic ways this bites teams:

  • Legacy upload code. Older SDK calls and copy-pasted snippets frequently include ACL: 'public-read' as a parameter. Every object written by that code becomes publicly readable, even though your bucket policy says nothing about public access.
  • Cross-account copies. When objects are copied from another account, the source ACL can carry over and grant AllUsers read access in your bucket.
  • Console mistakes. An engineer clicks "make public" on a single object to share a file quickly, then forgets about it. With ACL blocking off, that object stays exposed indefinitely.

The business impact is the familiar one: data leaks. Customer records, internal documents, backups, and credentials have all ended up in public S3 buckets through exactly this gap. Because the bucket policy looks secure, these objects often survive routine audits that only check policies.

Warning: Setting IgnorePublicAcls to true immediately stops existing public ACLs from being honored. If you are unknowingly relying on public-read ACLs to serve assets (for example, a static site or shared media), those objects will become inaccessible. Confirm how content is served before flipping this on in production.


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 any of them off for a bucket that should not be public.

Check the current state

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

You will get back something like this, with the ACL flags showing the gap:

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

Apply the full block via CLI

Danger: This command revokes public access immediately. If any production traffic depends on public ACLs in this bucket, that traffic will break. Verify there is no legitimate public dependency before running it.

aws s3api put-public-access-block \
  --bucket my-bucket \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=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), choose Edit.
  4. Check Block all public access, or at minimum enable the two ACL options: "Block public access to buckets and objects granted through new access control lists" and "Block public access to buckets and objects granted through any access control lists".
  5. Save and confirm.

Terraform

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
}

CloudFormation

MyBucket:
  Type: AWS::S3::Bucket
  Properties:
    BucketName: my-bucket
    PublicAccessBlockConfiguration:
      BlockPublicAcls: true
      IgnorePublicAcls: true
      BlockPublicPolicy: true
      RestrictPublicBuckets: true

Clean up existing public ACLs

Blocking new ACLs does not rewrite ones already on your objects. IgnorePublicAcls neutralizes them at evaluation time, but it is cleaner to remove them. To reset an individual object ACL to private:

aws s3api put-object-acl --bucket my-bucket --key path/to/object --acl private

Tip: For buckets where ACLs serve no purpose, set the bucket's Object Ownership to BucketOwnerEnforced. This disables ACLs entirely, so all access is governed by policies and IAM alone. It removes the whole class of ACL-based exposure rather than patching it case by case.

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

How to prevent it from happening again

One-off fixes drift. Bake the correct configuration into the places where buckets are created and changed.

Account-wide default

Set Block Public Access at the account level so it applies to every bucket, 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

Service Control Policy guardrail

In AWS Organizations, deny any attempt to weaken Block Public Access. This stops engineers and pipelines from disabling the settings, even with broad permissions:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyDisablingS3BPA",
      "Effect": "Deny",
      "Action": [
        "s3:PutAccountPublicAccessBlock",
        "s3:PutBucketPublicAccessBlock"
      ],
      "Resource": "*",
      "Condition": {
        "Bool": {
          "s3:PublicAccessBlockConfiguration/BlockPublicAcls": "false"
        }
      }
    }
  ]
}

Policy-as-code in CI/CD

Catch the misconfiguration before it merges. A Checkov scan on your Terraform will flag any aws_s3_bucket_public_access_block that does not set all four flags to true:

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

Wire that into a pull request check so a non-compliant bucket fails the build rather than reaching production.

Tip: Lensix continuously evaluates this check across all your buckets and accounts, so a setting that drifts after deployment, whether through a console edit or an out-of-band API call, surfaces without you having to rerun scans by hand.


Best practices

  • Enable all four BPA flags by default. Treat partial coverage as a bug. The four settings are designed to work together, and the ACL flags close gaps the policy flags cannot.
  • Disable ACLs where possible. Use BucketOwnerEnforced object ownership on new buckets. ACLs are a legacy access model, and removing them eliminates an entire exposure path.
  • Serve public content properly. If you genuinely need to serve files publicly, front them with CloudFront and an Origin Access Control rather than making bucket objects public. Keep the bucket itself private.
  • Audit object ownership and ACLs during migrations. Cross-account copies and data imports are prime times for stray public ACLs to slip in.
  • Monitor for drift. Account defaults and SCPs reduce risk, but continuous monitoring confirms reality matches intent across hundreds of buckets.

The takeaway is simple: blocking public bucket policies is only half the job. Until BlockPublicAcls and IgnorePublicAcls are both on, a single careless ACL can undo all of it. Enable the full set, prefer disabling ACLs entirely, and enforce the configuration through guardrails so it cannot quietly slip back.