Private S3 ACL Still Leaves Objects Publicly Readable
A private S3 ACL does not mean a private bucket. A leftover bucket-policy statement and partially configured Block Public Access settings can leave objects publicly readable until both layers are checked together.
Operational summary
At a glance
- Symptom
- An Amazon S3 bucket that a team believes is private continues to allow anonymous read access, discovered through access-log anomalies or an external scan…
- Likely cause
- The root cause is typically a bucket policy statement added earlier for a legitimate purpose, such as a static website or a partner integration…
- Impact
- Unreviewed public access through a bucket policy can expose stored objects to anonymous read or write requests, independent of what the ACL or the console summary badge implies.Because…
- Verification signal
- Confirm the bucket reports as non-public through S3's own policy status check rather than relying on the console badge alone.aws s3api get-bucket-policy-status --bucket EXAMPLE-BUCKET-NAMEThe IsPublic field must return…
- Safe correction
- Apply Block Public Access explicitly across all four settings rather than relying on ACL changes alone, then remove or narrow the offending bucket policy statement.aws s3api get-public-access-block --bucket…
- Rollback or recovery
- Keep the pre-change output of get-public-access-block and the original bucket policy document saved before applying any correction, so the exact prior state can be restored.If restricting public access…
Symptom
An Amazon S3 bucket that a team believes is private continues to allow anonymous read access, discovered through access-log anomalies or an external scan rather than through the console itself.
The bucket’s ACL and the console’s ‘Block public access’ indicator both appear correct for a private bucket, yet objects remain retrievable by an unauthenticated request from outside the account.
False Assumption
The team assumes that a private ACL, or a ‘private’ status badge in the console, is sufficient evidence that the bucket denies public access.
In Amazon S3, ACLs, bucket policies, IAM policies and Block Public Access settings are independent, separately evaluated layers. By default S3 grants access if any applicable policy allows it and no explicit deny applies, so a private ACL does not remove a permissive statement sitting in the bucket policy.
Root Cause
The root cause is typically a bucket policy statement added earlier for a legitimate purpose, such as a static website or a partner integration, that still contains a broad Principal value, combined with Block Public Access settings that were never enabled for the policy dimension.
Block Public Access is exposed as four separate controls: BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy and RestrictPublicBuckets. The console’s single toggle sets all four together, but infrastructure-as-code templates require each value set explicitly. A template that only addresses the ACL-related pair leaves any public bucket policy statement fully enforced.
Impact
Unreviewed public access through a bucket policy can expose stored objects to anonymous read or write requests, independent of what the ACL or the console summary badge implies.
Because the exposure is invisible from the ACL view alone, it commonly survives change reviews that check only ACL settings, and can remain undetected until an external scan or a dedicated audit inspects the bucket policy and all four Block Public Access flags together.
Diagnosis
Confirm the actual exposure using read-only checks before changing anything, and use an isolated or non-production account first if this workflow is unfamiliar.
- Retrieve the bucket policy document and inspect every statement’s
Effect,PrincipalandResourcefields. - Retrieve the ACL grants to confirm they are, in fact, private.
- Retrieve the Block Public Access configuration and record all four boolean values.
- Call the policy status check to obtain S3’s own computed public or private verdict.
| Setting | Governs |
|---|---|
| BlockPublicAcls | Rejects new public ACL grants on PUT requests |
| IgnorePublicAcls | Ignores existing public ACL grants when evaluating access |
| BlockPublicPolicy | Rejects new bucket policy statements that grant public access |
| RestrictPublicBuckets | Restricts access for buckets with a public policy, including some cross-account cases |
If the policy status check reports the bucket as public while the ACL is private, the bucket policy is the active source of exposure, and the correction below targets that layer specifically.
Correction
Apply Block Public Access explicitly across all four settings rather than relying on ACL changes alone, then remove or narrow the offending bucket policy statement.
aws s3api get-public-access-block --bucket EXAMPLE-BUCKET-NAME
aws s3api put-public-access-block
--bucket EXAMPLE-BUCKET-NAME
--public-access-block-configuration
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
Before applying RestrictPublicBuckets, confirm with the owners of any legitimate cross-account or static-hosting integration that depends on the current policy, since this setting can remove access those integrations rely on. Where access is legitimate, replace the wildcard principal with the exact account ID or role ARN instead of leaving the policy broad and disabling Block Public Access to compensate.
Validation
Confirm the bucket reports as non-public through S3’s own policy status check rather than relying on the console badge alone.
aws s3api get-bucket-policy-status --bucket EXAMPLE-BUCKET-NAME
The IsPublic field must return false. Follow this with an unauthenticated request to a known object URL from outside the account network to confirm the response is 403 Forbidden rather than 200 OK. Re-run get-public-access-block afterwards to confirm all four settings persisted, since a later infrastructure-as-code apply from an out-of-date template can silently revert them.
Rollback
Keep the pre-change output of get-public-access-block and the original bucket policy document saved before applying any correction, so the exact prior state can be restored.
If restricting public access breaks a dependent integration, restore the previous Block Public Access values from the saved output and open a scoped change to correct the bucket policy principal instead of leaving the bucket open. Treat any rollback of Block Public Access as temporary and pair it with a follow-up ticket to narrow the policy.
Prevention
Set all four Block Public Access values explicitly in every infrastructure-as-code template that creates or updates an S3 bucket, rather than relying on provider defaults or a single ACL-related flag.
Add the bucket policy status check and all four Block Public Access flags to routine access reviews and drift detection, not only the ACL. Review bucket policies whenever an integration that required a broad principal is decommissioned, since orphaned wildcard statements are a common way this trap persists long after its original purpose has ended.
Apply the safer control
Before you change production
Confirm the affected scope, export the current configuration, and test the replacement control in a non-production environment first.
Fix commands and configuration
PrincipalBlockPublicAclsIgnorePublicAclsVerify, roll back or escalate
Verify
Confirm the bucket reports as non-public through S3's own policy status check rather than relying on the console badge alone.aws s3api get-bucket-policy-status --bucket EXAMPLE-BUCKET-NAMEThe IsPublic field must return false.
Rollback
Keep the pre-change output of get-public-access-block and the original bucket policy document saved before applying any correction, so the exact prior state can be restored.If restricting public access breaks a dependent integration, restore the previous Block Public Access values from…
Escalate
Escalate when the blast radius is uncertain, the control cannot be tested safely, or remediation requires an outage or security exception.