S3 'Access Denied' on a Public Object: Why Block Public Access Overrides Your ACL
TL;DR
Setting an S3 object ACL to public-read is not enough. AWS S3 has a bucket-level (and account-level) "Block Public Access" firewall that overrides all object-level ACLs and bucket policies. Both layers must be configured correctly before a public URL works.
| Layer | Setting Required | Where to Change |
|---|---|---|
| Account-level BPA | Disable relevant block settings | S3 Console > Block Public Access (account) |
| Bucket-level BPA | Disable BlockPublicAcls & IgnorePublicAcls | Bucket > Permissions > Block Public Access |
| Bucket Policy or ACL | Grant s3:GetObject to * | Bucket Policy JSON |
Fix (30-second version): Disable the blocking flags at the bucket level, then attach a bucket policy granting public GetObject. Do not rely on object ACLs alone.
Why This Happens: The Two-Layer Access Model
S3 access evaluation is a sequential gate system, not a single check. Think of it like a building with two locked doors: even if you have a key to the inner door (object ACL), the outer door (Block Public Access) can still stop you cold.
Here is the exact evaluation order S3 uses when an anonymous request hits an object URL:
Anonymous GET Request
|
v
[1] Account-level Block Public Access (BPA)
- If ANY blocking flag is ON → DENY immediately (no further evaluation)
|
v
[2] Bucket-level Block Public Access (BPA)
- BlockPublicAcls → Rejects PUT requests that grant public ACLs
- IgnorePublicAcls → Ignores ALL public ACLs already set on objects/bucket
- BlockPublicPolicy → Rejects bucket policies that grant public access
- RestrictPublicBuckets → Restricts access to only AWS services and authorized users
- If relevant flag is ON → DENY
|
v
[3] Bucket Policy evaluation
- Is there an explicit Allow for s3:GetObject to Principal "*"?
- No matching Allow → DENY
|
v
[4] Object ACL evaluation
- Is the ACL set to public-read?
- Only reaches here if ALL above gates passed
|
v
[5] ALLOW → Object servedThe critical insight: IgnorePublicAcls = true (the default on new buckets) means S3 completely ignores the public-read ACL you set on the object. Your ACL change had zero effect.
The Analogy: Building Security vs. Room Key
Setting an object ACL to public-read is like putting a "Welcome, walk in" sign on your office door. But Block Public Access is the building's front desk security guard with a standing order: "No visitors without a badge, regardless of what any door sign says." The guard never even lets visitors reach your door to read the sign. You must first update the guard's standing orders (BPA settings) before the door sign (ACL) has any meaning.
The Correct Fix: CLI-First Approach
The recommended modern approach is to use a bucket policy (not object ACLs) for public access. ACL-based public access is a legacy pattern AWS discourages.
Step 1: Disable Block Public Access at the Bucket Level
aws s3api put-public-access-block \
--bucket YOUR_BUCKET_NAME \
--public-access-block-configuration \
"BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false"Warning: Only disable the flags you actually need. For a bucket policy approach, you need BlockPublicPolicy=false and RestrictPublicBuckets=false at minimum.
Step 2: Attach a Bucket Policy Granting Public Read
Create a file public-read-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
}
]
}Apply it:
aws s3api put-bucket-policy \
--bucket YOUR_BUCKET_NAME \
--policy file://public-read-policy.jsonStep 3: Verify Access
# Check BPA status
aws s3api get-public-access-block --bucket YOUR_BUCKET_NAME
# Check bucket policy
aws s3api get-bucket-policy --bucket YOUR_BUCKET_NAME
# Test anonymous access
curl -I https://YOUR_BUCKET_NAME.s3.amazonaws.com/your-image.jpgExpected: HTTP/1.1 200 OK
Terraform Equivalent
resource "aws_s3_bucket_public_access_block" "example" {
bucket = aws_s3_bucket.example.id
block_public_acls = false
ignore_public_acls = false
block_public_policy = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "public_read" {
bucket = aws_s3_bucket.example.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "PublicReadGetObject"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.example.arn}/*"
}]
})
depends_on = [aws_s3_bucket_public_access_block.example]
}The depends_on is critical: the policy apply will fail if BPA is still blocking it.
IAM: Minimum Required Permissions
The IAM identity running these commands needs the following minimum permissions (least privilege):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutBucketPublicAccessBlock",
"s3:GetBucketPublicAccessBlock",
"s3:PutBucketPolicy",
"s3:GetBucketPolicy"
],
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME"
}
]
}Do not grant s3:* or attach AmazonS3FullAccess for this task.
Cost Impact
- S3 GET requests: Public objects served via S3 URL incur standard S3 GET request costs ($0.0004 per 1,000 requests in us-east-1) plus data transfer out costs ($0.09/GB to internet). For high-traffic public assets, put CloudFront in front of S3 — CloudFront-to-S3 transfer is free, and CloudFront edge caching dramatically reduces origin GET request volume.
- No cost for BPA configuration changes: Modifying Block Public Access settings and bucket policies is free.
Glossary
| Term | Definition |
|---|---|
| Block Public Access (BPA) | An S3 account- and bucket-level override that prevents public access regardless of ACL or policy settings; introduced in 2018 as a safety guardrail. |
| Object ACL | An access control list attached to an individual S3 object that can grant permissions (e.g., public-read) to specific grantees or the public; evaluated only after BPA gates pass. |
| Bucket Policy | A resource-based IAM policy attached to an S3 bucket that controls access at the bucket and object level using JSON policy syntax. |
| IgnorePublicAcls | A BPA flag that, when true, causes S3 to completely disregard any public-granting ACLs on the bucket or its objects. |
| Principal "*" | IAM policy notation for "any entity, authenticated or anonymous" — the mechanism that enables truly public (unauthenticated) access to S3 objects. |
Comments
Post a Comment