CloudFront + S3 Static Website: Why You're Getting 403 and How to Fix It with OAC
You've deployed a static website to S3, put CloudFront in front of it, and now every request returns a cryptic 403 Forbidden. This is one of the most common — and most misunderstood — misconfigurations in AWS, rooted in a fundamental distinction between two different S3 origin types.
TL;DR
| Approach | Origin Type | Bucket Public? | OAC Supported? | Recommended? |
|---|---|---|---|---|
| S3 Website Endpoint | Custom Origin (HTTP) | ✅ Must be public | ❌ No | ⚠️ Legacy only |
| S3 REST API Endpoint | S3 Origin (native) | ❌ Keep private | ✅ Yes | ✅ Recommended |
The Root Cause: Two Completely Different S3 Endpoints
S3 exposes two distinct endpoint types, and CloudFront treats them very differently. Confusing them is the #1 source of 403 errors.
1. S3 Website Endpoint (Static Website Hosting)
When you enable Static Website Hosting in the S3 console, AWS activates a separate HTTP endpoint in the format:
http://<bucket-name>.s3-website-<region>.amazonaws.com
This endpoint behaves like a web server — it handles index.html routing, custom error pages, and redirects. However, it is HTTP-only and has no concept of AWS IAM authentication. CloudFront treats it as a Custom Origin. Because IAM-based access control doesn't apply here, the bucket's Block Public Access must be disabled and a public bucket policy must be attached. If either is missing, S3 returns 403.
2. S3 REST API Endpoint (Native S3 Origin)
This is the standard S3 endpoint format:
https://<bucket-name>.s3.<region>.amazonaws.com
CloudFront recognizes this as a native S3 Origin and can authenticate to it using Origin Access Control (OAC) — a signed request mechanism. The bucket stays completely private; only CloudFront can read from it via an IAM-based bucket policy.
Analogy: Think of the S3 Website Endpoint like a public library reading room — anyone can walk in. The S3 REST Endpoint with OAC is like a private archive — only a credentialed librarian (CloudFront) has a key, and the public never touches the shelves directly.
Why the 403 Happens: Decision Tree
- Website Endpoint + Block Public Access ON: S3 rejects all anonymous requests. CloudFront has no IAM credentials to use here, so every request returns 403.
- Website Endpoint + No Public Bucket Policy: Even with Block Public Access disabled, without an explicit
s3:GetObjectallow for*, S3 denies access. - REST Endpoint + No OAC + Private Bucket: CloudFront forwards unsigned requests; S3 sees an anonymous caller with no permissions — 403.
- REST Endpoint + OAC configured correctly: CloudFront signs every request with SigV4; the bucket policy grants
s3:GetObjectto the specific CloudFront distribution — success.
The Recommended Architecture: OAC with Private S3 Bucket
- User sends an HTTPS request to the CloudFront distribution domain.
- CloudFront checks its cache. On a miss, it forwards the request to the S3 REST endpoint, signing it with SigV4 using OAC.
- S3 Bucket Policy evaluates the request: the principal is the CloudFront service principal (
cloudfront.amazonaws.com) and the source distribution ARN matches — access is granted. - S3 returns the object to CloudFront, which caches and serves it to the user.
- Direct S3 URL access by any other caller is denied — the bucket is fully private.
Step-by-Step Fix: Migrate to OAC
Step 1: Disable Static Website Hosting (or use a separate bucket)
If you only need CloudFront to serve files (no server-side redirects or custom error pages from S3), disable Static Website Hosting. Your CloudFront distribution will handle error pages natively.
Step 2: Ensure Block Public Access is ENABLED
In the S3 console, navigate to your bucket → Permissions → Block Public Access → enable all four settings. This is the secure baseline.
Step 3: Create an OAC in CloudFront
🔽 AWS CLI: Create Origin Access Control
aws cloudfront create-origin-access-control \
--origin-access-control-config '{
"Name": "my-s3-oac",
"Description": "OAC for private S3 bucket",
"SigningProtocol": "sigv4",
"SigningBehavior": "always",
"OriginAccessControlOriginType": "s3"
}'
Note the Id returned — you'll attach it to your CloudFront origin.
Step 4: Update Your CloudFront Distribution Origin
Set the origin domain to the S3 REST endpoint (not the website endpoint) and attach the OAC ID.
🔽 CloudFormation: CloudFront Distribution with OAC
AWSTemplateFormatVersion: '2010-09-09'
Resources:
MyOAC:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: my-s3-oac
SigningProtocol: sigv4
SigningBehavior: always
OriginAccessControlOriginType: s3
MyDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- Id: S3Origin
DomainName: !Sub "${MyBucket}.s3.${AWS::Region}.amazonaws.com"
S3OriginConfig:
OriginAccessIdentity: ""
OriginAccessControlId: !GetAtt MyOAC.Id
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
DefaultRootObject: index.html
Enabled: true
Step 5: Attach the Bucket Policy
Grant CloudFront's service principal access, scoped to your specific distribution. Replace DISTRIBUTION_ID and BUCKET_NAME accordingly.
🔽 S3 Bucket Policy: OAC Least-Privilege Grant
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOAC",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::BUCKET_NAME/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/DISTRIBUTION_ID"
}
}
}
]
}
Key security note: The AWS:SourceArn condition pins access to your specific distribution. Without it, any CloudFront distribution in any AWS account could potentially read your bucket if they guessed the name.
What If You Must Keep the Website Endpoint? (Legacy Path)
Some features — like S3-native redirect rules or routing rules — only work on the website endpoint. In that case, OAC is not an option. Your only path is a public bucket policy. Mitigate risk by:
- Enabling CloudFront geo-restriction to limit exposure.
- Using AWS WAF on the CloudFront distribution to filter malicious traffic.
- Accepting that the S3 bucket URL itself is publicly accessible — consider whether that's acceptable for your threat model.
Common Pitfalls Checklist
| Symptom | Likely Cause | Fix |
|---|---|---|
| 403 on all requests | Website endpoint used as S3 origin with OAC | Switch to REST endpoint or remove OAC |
| 403 on all requests | Block Public Access ON + no OAC | Add OAC + bucket policy |
| 403 on all requests | Bucket policy missing or wrong ARN | Verify AWS:SourceArn matches distribution ARN |
| 403 only on subpaths | Missing DefaultRootObject or no index.html in subfolder | Use CloudFront Functions for subfolder index routing |
| 200 from CloudFront URL, 403 from S3 URL | ✅ Working as intended with OAC | No action needed |
Glossary
| Term | Definition |
|---|---|
| OAC (Origin Access Control) | A CloudFront feature that signs requests to S3 using SigV4, replacing the older OAI mechanism. Allows private S3 buckets to serve content exclusively through CloudFront. |
| S3 Website Endpoint | An HTTP-only endpoint activated by enabling Static Website Hosting on an S3 bucket. Treated as a Custom Origin by CloudFront; requires public bucket access. |
| S3 REST Endpoint | The standard S3 HTTPS API endpoint. Supports IAM-based authentication and is compatible with OAC. |
| Block Public Access | An S3 account/bucket-level setting that overrides any public ACLs or bucket policies, preventing anonymous access regardless of other configurations. |
| SigV4 (Signature Version 4) | AWS's request signing protocol. OAC uses SigV4 to cryptographically sign CloudFront's requests to S3, proving the request originates from an authorized distribution. |
Next Steps
- 📖 AWS Docs: Restricting access to an Amazon S3 origin
- 📖 AWS Docs: Website endpoints vs. REST endpoints
- If you need subfolder
index.htmlresolution (a gap with the REST endpoint), implement a CloudFront Function to rewrite URIs — it's a 10-line JavaScript function and far more secure than a public bucket.
Comments
Post a Comment