Back to blog
AWSBest PracticesCloud SecurityStorage

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

Learn why blocking S3 bucket policies isn't enough and how missing BlockPublicAcls and IgnorePublicAcls settings leave your data exposed, plus step-by-step fixes.

TL;DR

This check flags S3 buckets that block policy-based public access but leave ACL-based public access open, meaning a publicly readable ACL can still expose your objects. Fix it by enabling all four Public Access Block settings, especially BlockPublicAcls and IgnorePublicAcls.

S3 has two separate mechanisms for granting public access: bucket policies and access control lists (ACLs). It is easy to lock down one and forget the other. This check catches buckets where the policy side is covered but the ACL side is left exposed, which is a gap attackers and automated scanners actively look for.


What this check detects

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

  • BlockPublicAcls — rejects new requests that would set a public ACL on a bucket or object.
  • IgnorePublicAcls — ignores any existing public ACLs, so they have no effect.
  • BlockPublicPolicy — rejects bucket policies that grant public access.
  • RestrictPublicBuckets — restricts access to buckets that are already public via policy.

This check fires when the policy-side settings (BlockPublicPolicy and/or RestrictPublicBuckets) are enabled, but the ACL-side settings (BlockPublicAcls and IgnorePublicAcls) are missing or set to false. The result is a bucket that looks protected at a glance but still honors public ACLs.

Note: ACLs are a legacy access mechanism that predate IAM and bucket policies. AWS now recommends disabling ACLs entirely via Object Ownership set to "Bucket owner enforced", but many older buckets still have ACLs active, which is exactly where this gap shows up.


Why it matters

A public ACL grants the AllUsers or AuthenticatedUsers group read (or write) access to a bucket or to individual objects. Because ACLs can be set per object, a single misconfigured upload can expose one file even when the bucket policy is clean.

Here is how this typically goes wrong in practice:

  • An application or SDK call sets x-amz-acl: public-read on uploaded objects, often copied from an old tutorial or a default config.
  • The bucket policy is locked down, so the team assumes the bucket is private.
  • Without IgnorePublicAcls, those object-level ACLs are fully honored, and the files are readable by anyone with the URL.

The business impact is the usual S3 exposure story: leaked customer data, exposed backups, source code in object stores, and credentials sitting in config files. Automated scanners crawl S3 constantly, and a public object can be indexed and copied within minutes. Once data leaves your bucket, you cannot pull it back.

Warning: Enabling IgnorePublicAcls will immediately cut off access to any object currently served through a public ACL. If you have a legitimate workflow relying on public-read ACLs (for example, a static asset bucket), move that traffic to a bucket policy or CloudFront first to avoid breaking it.


How to fix it

The fix is to enable all four Public Access Block settings. There is rarely a good reason to enable only some of them.

Step 1: Check the current configuration

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

A bucket flagged by this check will show something like:

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

Step 2: Confirm nothing relies on public ACLs

Before flipping the switch, check whether any objects are currently public via ACL. The Access Analyzer for S3 dashboard in the console lists buckets with public or shared access. You can also spot-check object ACLs:

aws s3api get-object-acl --bucket my-bucket --key path/to/object.jpg

Danger: The command below changes live access controls on a production bucket. If any active application or CDN depends on public-read ACLs, it will lose access the moment this applies. Validate against a staging bucket and review your asset delivery path first.

Step 3: Enable all four settings

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

Step 4: Verify

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

All four values should now read true.

Going further: disable ACLs entirely

If your bucket does not need ACLs at all (most do not), set Object Ownership to bucket owner enforced. This removes ACLs from the equation completely and is the AWS-recommended posture for new buckets.

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

Tip: Apply Public Access Block at the account level too. The setting aws s3control put-public-access-block on your account ID enforces these defaults across every bucket, including ones created later, so a single misconfigured bucket cannot slip through.

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

How to prevent it from happening again

Manual fixes do not scale. Bake the protection into how buckets are created.

Terraform

Attach an explicit aws_s3_bucket_public_access_block resource to every bucket, with all four flags set:

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"
  }
}

Policy-as-code in CI

Catch missing flags before they ever reach AWS. A Checkov scan in your pipeline will fail the build on an incomplete Public Access Block:

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

Those four checks map directly to BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy, and RestrictPublicBuckets. Wire the scan into a required status check so a pull request cannot merge while any of them fail.

Guardrails with SCPs

For organization-wide enforcement, use a Service Control Policy that denies s3:PutBucketPublicAccessBlock calls which would weaken the settings, or deny s3:PutBucketAcl with public grants. AWS Control Tower and Config conformance packs also ship managed rules like s3-bucket-level-public-access-prohibited that flag drift automatically.

Tip: Pair detection with auto-remediation. An AWS Config rule plus an SSM Automation document can re-apply the correct Public Access Block within seconds of a bucket drifting, so a human mistake never stays exposed for long.


Best practices

  • Treat the four flags as a set. Enabling only the policy-side settings is the exact gap this check exists to find. Turn all four on unless you have a documented, reviewed exception.
  • Disable ACLs where you can. Bucket owner enforced ownership eliminates an entire class of misconfiguration and is the modern default.
  • Serve public assets through CloudFront. If you need public content, use a CloudFront distribution with an Origin Access Control and keep the bucket itself fully private.
  • Enforce at the account level. Account-wide Public Access Block is your backstop for buckets created outside your IaC pipeline.
  • Monitor continuously. Public access is not a one-time fix. Use Access Analyzer for S3 and continuous checks so new buckets and ACL changes are caught as they happen.

The short version: there is almost never a legitimate reason to leave ACL-based public access open while blocking policy-based access. Close the gap, push the configuration into Terraform, and gate it in CI so it stays closed.