How to Fix S3 Public Access Block Overriding Your Bucket Policy Allow (403 Forbidden)
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH (public assets return 403; misconfigured relaxation exposes all bucket objects) | Time to Fix: 5 mins
TL;DR
- What broke:
BlockPublicPolicyorRestrictPublicBucketsis set totrueat the account or bucket level, which hard-vetoes any bucket policyEffect: AllowtoPrincipal: *— regardless of what the policy says. - How to fix it: Set the specific conflicting Public Access Block flag(s) to
falseon the bucket (not account) level, or restructure delivery through CloudFront + OAC so the bucket never needs to be public. - Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your bucket policy and Public Access Block config and get the corrected Terraform or CLI output instantly.
The Incident (What does the error mean?)
You applied this bucket policy expecting public read:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-assets-bucket/*"
}]
}
Requests return:
HTTP/1.1 403 Forbidden
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
The S3 access logs show the request hitting the bucket. The bucket policy is syntactically valid. IAM is not the problem. The Public Access Block is the problem. AWS evaluates Public Access Block before the bucket policy. It is not an IAM boundary — it is a hard infrastructure-layer gate that cannot be overridden by any policy document.
The four flags and which one is killing you:
| Flag | What it blocks |
|---|---|
BlockPublicAcls |
Rejects PUT requests that grant public ACL |
IgnorePublicAcls |
Ignores existing public ACLs at read time |
BlockPublicPolicy |
Rejects the PUT of any bucket policy granting Principal: * |
RestrictPublicBuckets |
Allows the policy to be stored but blocks all anonymous/cross-account access at request time |
If BlockPublicPolicy: true, AWS refused to even save your policy (you got a 403 on PutBucketPolicy). If RestrictPublicBuckets: true, the policy is saved but every anonymous GET returns 403. Either way, your Effect: Allow is dead.
The Attack Vector / Blast Radius
Scenario A — You disable too much: You set all four flags to false on the bucket to fix the 403. Now every object in the bucket is publicly listable and downloadable if someone guesses or enumerates key names. A single aws s3 ls s3://my-assets-bucket --no-sign-request from an attacker returns your full object inventory. If the bucket contains anything beyond intentionally public static assets (logs, exports, backups in a subdirectory), you have a data breach.
Scenario B — Account-level block overrides bucket-level: Your bucket-level Public Access Block is correctly configured, but the AWS account has RestrictPublicBuckets: true set via aws s3control put-public-access-block --account-id. Account-level settings always win. Changing the bucket setting does nothing. Engineers waste hours debugging the wrong resource.
Scenario C — Terraform drift: Your Terraform state shows restrict_public_buckets = false but an out-of-band console change set it back to true. The plan shows no diff because the remote state was not refreshed. You're flying blind.
How to Fix It (The Solution)
Diagnose First — Run These Two Commands
# Check bucket-level block
aws s3api get-public-access-block --bucket my-assets-bucket
# Check ACCOUNT-level block (this overrides everything)
aws s3control get-public-access-block --account-id $(aws sts get-caller-identity --query Account --output text)
Identify which flag is true that should not be.
Basic Fix — CLI (Surgical, Bucket-Level)
Only disable what you need. If you only need s3:GetObject public, you only need RestrictPublicBuckets: false and BlockPublicPolicy: false. Leave ACL-related flags alone.
aws s3api put-public-access-block \
--bucket my-assets-bucket \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=false,RestrictPublicBuckets=false"
Enterprise Best Practice — Terraform with CloudFront OAC (Never Make the Bucket Public)
The correct production pattern is: bucket stays fully private, CloudFront gets access via Origin Access Control. No Public Access Block flags need to change. No anonymous S3 access ever.
- resource "aws_s3_bucket_public_access_block" "assets" {
- bucket = aws_s3_bucket.assets.id
- block_public_acls = false
- ignore_public_acls = false
- block_public_policy = false
- restrict_public_buckets = false
- }
-
- resource "aws_s3_bucket_policy" "assets" {
- bucket = aws_s3_bucket.assets.id
- policy = jsonencode({
- Statement = [{
- Effect = "Allow"
- Principal = "*"
- Action = "s3:GetObject"
- Resource = "${aws_s3_bucket.assets.arn}/*"
- }]
- })
- }
+ resource "aws_s3_bucket_public_access_block" "assets" {
+ bucket = aws_s3_bucket.assets.id
+ block_public_acls = true
+ ignore_public_acls = true
+ block_public_policy = true
+ restrict_public_buckets = true
+ }
+
+ resource "aws_cloudfront_origin_access_control" "assets" {
+ name = "assets-oac"
+ origin_access_control_origin_type = "s3"
+ signing_behavior = "always"
+ signing_protocol = "sigv4"
+ }
+
+ resource "aws_s3_bucket_policy" "assets" {
+ bucket = aws_s3_bucket.assets.id
+ policy = jsonencode({
+ Statement = [{
+ Effect = "Allow"
+ Principal = {
+ Service = "cloudfront.amazonaws.com"
+ }
+ Action = "s3:GetObject"
+ Resource = "${aws_s3_bucket.assets.arn}/*"
+ Condition = {
+ StringEquals = {
+ "AWS:SourceArn" = aws_cloudfront_distribution.assets.arn
+ }
+ }
+ }]
+ })
+ }
This pattern: bucket is private, all four Public Access Block flags are true, CloudFront authenticates via SigV4. An attacker hitting the S3 endpoint directly gets 403. Only CloudFront can read objects.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Checkov — Block any Terraform plan that sets all four flags to false:
# .checkov.yaml
checks:
- CKV_AWS_53 # Ensure S3 bucket has block public ACLS enabled
- CKV_AWS_54 # Ensure S3 bucket has block public policy enabled
- CKV_AWS_55 # Ensure S3 bucket has ignore public ACLs enabled
- CKV_AWS_56 # Ensure S3 bucket has restrict public buckets enabled
Run in CI: checkov -d . --framework terraform --check CKV_AWS_53,CKV_AWS_54,CKV_AWS_55,CKV_AWS_56
2. AWS Config Rule — Continuous compliance:
Enable the managed rule s3-bucket-public-access-prohibited in AWS Config. Set remediation action to auto-invoke an SSM Automation document that re-enables all four flags if drift is detected.
3. SCP (Service Control Policy) — Account-level enforcement:
{
"Effect": "Deny",
"Action": "s3:PutBucketPublicAccessBlock",
"Resource": "*",
"Condition": {
"StringEquals": {
"s3:DataAccessPointAccount": "${aws:PrincipalAccount}"
},
"Bool": {
"s3:BlockPublicPolicy": "false"
}
}
}
4. Terraform Sentinel Policy (HashiCorp Cloud Platform):
# sentinel.hcl — deny public S3 buckets
all_s3_public_access_blocks = filter tfplan.resource_changes as _, rc {
rc.type is "aws_s3_bucket_public_access_block"
}
main = rule {
all all_s3_public_access_blocks as _, block {
block.change.after.restrict_public_buckets is true and
block.change.after.block_public_policy is true
}
}
This catches the misconfiguration at terraform plan time, before it ever reaches apply.