CloudFront + S3 Static Website 403 Errors: OAC vs Public Bucket Explained
You've wired CloudFront in front of an S3 static website and the browser returns a 403 — but the bucket policy looks right, the distribution is deployed, and the objects are definitely there. This is one of the most common CloudFront misconfiguration patterns, and the root cause almost always comes down to a fundamental architectural choice: are you using S3 as a REST API origin or as a static website endpoint? That distinction determines everything about how permissions work.
TL;DR: CloudFront + S3 403 Root Causes at a Glance
| Scenario | Origin Type | Auth Mechanism | Common 403 Cause |
|---|---|---|---|
| OAC (recommended) | S3 REST endpoint | SigV4 signed requests via OAC | Missing or incorrect bucket policy allowing OAC principal |
| Public bucket | S3 website endpoint | No auth — public read required | Block Public Access enabled, or bucket policy missing s3:GetObject for * |
| Legacy OAI | S3 REST endpoint | OAI principal in bucket policy | OAI not attached to distribution, or policy references wrong OAI ID |
How CloudFront-to-S3 Access Actually Works
S3 exposes two distinct endpoint types for the same bucket, and CloudFront treats them differently. The REST API endpoint (bucket.s3.amazonaws.com) enforces IAM-based access control — every request must be authorized. The static website endpoint (bucket.s3-website-region.amazonaws.com) bypasses IAM entirely and relies on the bucket being publicly readable. When you configure CloudFront's origin, the endpoint you choose locks you into one of these two access models. Mixing them — for example, pointing CloudFront at the REST endpoint while relying on a public bucket policy — produces 403s that look like permission errors but are actually an architectural mismatch.
- OAC path (left): CloudFront signs every origin request with SigV4. S3 validates the signature against the bucket policy, which must explicitly allow the CloudFront service principal. No public access required.
- Public website endpoint path (right): CloudFront forwards unsigned requests to the S3 website endpoint. S3 checks only whether the object is publicly readable. Block Public Access must be fully disabled and a public
s3:GetObjectbucket policy must exist. - Mismatch (center, red): Pointing CloudFront at the REST endpoint while expecting public-bucket behavior — or vice versa — produces a 403 that neither side logs as an explicit auth failure.
Step 1: Identify Which Origin Endpoint CloudFront Is Using
Before touching any policy, confirm what origin URL is actually configured. The CloudFront console auto-populates the origin domain when you select a bucket, but it may have defaulted to the REST endpoint even if you intended to use the website endpoint. These look similar but behave completely differently — and this is where most engineers waste an hour.
aws cloudfront get-distribution-config \
--id YOUR_DISTRIBUTION_ID \
--query 'DistributionConfig.Origins.Items[*].{Domain:DomainName,OAC:S3OriginConfig}' \
--output table
Check the DomainName field in the output:
your-bucket.s3.amazonaws.comoryour-bucket.s3.us-east-1.amazonaws.com→ REST endpoint. OAC or OAI is required.your-bucket.s3-website-us-east-1.amazonaws.com→ Static website endpoint. Public read is required; OAC cannot be used here.
Once you know which endpoint is in use, the diagnostic path splits cleanly.
Path A: REST Endpoint — Fix the OAC Configuration (Recommended)
OAC is the current AWS-recommended approach for restricting S3 access to CloudFront. It replaces the older Origin Access Identity (OAI) mechanism. With OAC, CloudFront signs origin requests using SigV4, and S3 validates those signatures against a bucket policy that grants access to the CloudFront service principal. If either side of this handshake is misconfigured, S3 returns 403.
Step 2: Verify an OAC Exists and Is Attached to the Distribution
An OAC that exists but isn't attached to the correct origin does nothing. Confirm both the OAC exists and is wired to the right origin in your distribution config.
# List all OACs in your account
aws cloudfront list-origin-access-controls \
--query 'OriginAccessControlList.Items[*].{Id:Id,Name:Name,SigningBehavior:SigningBehavior}' \
--output table
# Confirm the OAC is attached to your distribution's S3 origin
aws cloudfront get-distribution-config \
--id YOUR_DISTRIBUTION_ID \
--query 'DistributionConfig.Origins.Items[*].{Domain:DomainName,OACId:OriginAccessControlId}' \
--output table
If OriginAccessControlId is empty or null, the origin is making unsigned requests to S3 — which S3 will reject with 403 unless the bucket is public.
Step 3: Create and Attach an OAC If Missing
# Create the OAC
aws cloudfront create-origin-access-control \
--origin-access-control-config '{
"Name": "my-s3-oac",
"Description": "OAC for S3 static site",
"SigningProtocol": "sigv4",
"SigningBehavior": "always",
"OriginAccessControlOriginType": "s3"
}'
Note the Id returned. Then update your distribution to attach it — this requires fetching the current config, modifying the origin's OriginAccessControlId, and calling update-distribution with the current ETag. The update triggers a deployment; allow a few minutes for propagation.
Step 4: Verify the Bucket Policy Grants the CloudFront Service Principal
This is the step most engineers get wrong. The bucket policy must reference the CloudFront distribution ARN — not just the service principal alone. Without the aws:SourceArn condition, the policy grants access to all CloudFront distributions in all accounts, which is a security risk and not what AWS recommends.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOAC",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/YOUR_DISTRIBUTION_ID"
}
}
}
]
}
Apply this policy:
aws s3api put-bucket-policy \
--bucket your-bucket-name \
--policy file://bucket-policy.json
Step 5: Confirm Block Public Access Does Not Conflict
With OAC, Block Public Access should remain enabled — that's the point. But verify that no legacy public-read ACL or conflicting bucket policy statement is present that might be causing S3 to evaluate a Deny before reaching the Allow.
aws s3api get-bucket-policy-status \
--bucket your-bucket-name
aws s3api get-public-access-block \
--bucket your-bucket-name
For OAC, you want IsPublic: false and all Block Public Access settings enabled. If IsPublic: true appears alongside an OAC setup, a conflicting public ACL or policy statement is present — remove it.
Think of OAC like a VIP entrance with a signed guest list. CloudFront signs the request (the signature is the invite), and S3 checks the guest list (the bucket policy). If the bouncer's list doesn't name your specific event (the distribution ARN condition), anyone with a CloudFront badge gets in — not what you want.
Path B: Static Website Endpoint — Fix Public Access Configuration
If you're using the S3 website endpoint, OAC is not applicable — the website endpoint doesn't support SigV4 authentication. Public read is the only access model. This path is simpler but requires the bucket to be genuinely public, which means Block Public Access must be disabled and a bucket policy must explicitly allow s3:GetObject for all principals.
Step 6: Disable Block Public Access
aws s3api put-public-access-block \
--bucket your-bucket-name \
--public-access-block-configuration \
BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false
Step 7: Apply a Public Read Bucket Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
aws s3api put-bucket-policy \
--bucket your-bucket-name \
--policy file://public-bucket-policy.json
Note that making a bucket public exposes all objects to the internet directly via the S3 URL, not just through CloudFront. If restricting access to CloudFront only is a requirement, the REST endpoint with OAC is the correct architecture — not the website endpoint with a public bucket.
The Misdiagnosis That Costs an Hour
Here's the failure pattern that shows up repeatedly in production: an engineer sets up CloudFront, selects the bucket from the console dropdown, and the console auto-populates the REST endpoint (bucket.s3.amazonaws.com). They then enable static website hosting on the bucket and add a public bucket policy — expecting the public policy to satisfy CloudFront's origin requests. It doesn't. The REST endpoint requires signed requests or an explicit IAM-based bucket policy. The public policy only satisfies anonymous HTTP requests to the website endpoint.
The 403 that comes back has no CloudFront error detail — it's a raw S3 403, which CloudFront surfaces as-is. CloudFront access logs show 403 with an empty x-edge-result-type of Error, and S3 server access logs (if enabled) show AccessDenied. Engineers see the public bucket policy, assume it's correct, and start chasing CloudFront cache behaviors or geo-restriction settings — neither of which is the problem.
The fix is a single architectural decision: pick one endpoint type and configure the matching access model end-to-end.
- Start by identifying the origin endpoint type from the distribution config.
- If REST endpoint: verify OAC attachment, then verify the bucket policy references the correct distribution ARN in the
AWS:SourceArncondition. - If website endpoint: verify Block Public Access is fully disabled and a public
s3:GetObjectpolicy exists. - If the endpoint type is wrong for your intent, update the origin domain in the distribution and redeploy.
IAM Permissions Required to Execute These Steps
The engineer running these commands needs the following minimum permissions. Read/List actions on CloudFront distributions require "Resource": "*" as they do not support resource-level restrictions for those specific actions — verify current support in the AWS Service Authorization Reference.
🔽 Click to expand — IAM policy for CloudFront + S3 diagnosis
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudFrontRead",
"Effect": "Allow",
"Action": [
"cloudfront:GetDistribution",
"cloudfront:GetDistributionConfig",
"cloudfront:ListOriginAccessControls",
"cloudfront:CreateOriginAccessControl"
],
"Resource": "*"
},
{
"Sid": "S3BucketPolicy",
"Effect": "Allow",
"Action": [
"s3:GetBucketPolicy",
"s3:PutBucketPolicy",
"s3:GetBucketPolicyStatus",
"s3:GetPublicAccessBlock",
"s3:PutPublicAccessBlock"
],
"Resource": "arn:aws:s3:::your-bucket-name"
}
]
}
Wrap-Up: CloudFront + S3 403 Resolution and Next Steps
The CloudFront S3 403 error almost always traces back to an endpoint-access model mismatch. The decision is straightforward: use the REST endpoint with OAC if you want to keep the bucket private (recommended), or use the website endpoint with a fully public bucket if you need S3 website-specific features like custom error documents and index document routing. Don't mix the two.
For new deployments, AWS recommends OAC over both OAI and public buckets. OAI is still functional but is considered legacy. The OAC approach keeps your bucket private, supports SSE-KMS encrypted objects, and scopes access to a specific distribution via the AWS:SourceArn condition.
- AWS Docs: Restricting access to an Amazon S3 origin
- AWS Docs: Website endpoints vs REST API endpoints
Glossary
| Term | Definition |
|---|---|
| OAC (Origin Access Control) | Current AWS mechanism for authorizing CloudFront to access a private S3 bucket using SigV4-signed requests and a bucket policy with the CloudFront service principal. |
| OAI (Origin Access Identity) | Legacy CloudFront mechanism for S3 access control, superseded by OAC. Still functional but not recommended for new deployments. |
| S3 REST Endpoint | The standard S3 API endpoint (bucket.s3.amazonaws.com) that enforces IAM-based access control on every request. |
| S3 Website Endpoint | The static website hosting endpoint (bucket.s3-website-region.amazonaws.com) that serves public content without IAM authentication. |
| Block Public Access | Account- and bucket-level S3 settings that override bucket policies and ACLs to prevent public exposure of objects. |
Comments
Post a Comment