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
- Open the S3 console and select the bucket.
- Go to the Permissions tab.
- Under Block public access (bucket settings), click Edit.
- Check Block all public access (this enables all four sub-settings), or at minimum enable the two ACL settings.
- Save and confirm by typing
confirmwhen prompted. - 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
BucketOwnerEnforcedon 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.

