AWS CloudFront 403 Forbidden — Every Cause and Fix (2026)
CloudFront returns 403 Forbidden but your S3 bucket or origin looks fine. Here's every cause — OAC misconfiguration, bucket policy missing, wrong origin domain, geo-restriction — and the exact fix.
CloudFront returns 403 Forbidden to your users but the S3 bucket or backend looks healthy. This is one of the most confusing AWS errors because the 403 can come from multiple places — CloudFront itself, S3, or your origin.
Here's every cause and the exact fix.
First: Identify Where the 403 Is Coming From
CloudFront adds a header to responses that tells you the source:
curl -I https://your-cloudfront-domain.com/index.html
# Look for:
# X-Cache: Error from cloudfront ← CloudFront itself is blocking
# X-Cache: Miss from cloudfront ← Passed to origin, origin returned 403Also check the CloudFront error page — AWS shows a specific error code:
- 403 from CloudFront → WAF, geo restriction, signed URL required, or missing OAC
- 403 from S3 → Bucket policy missing or wrong, OAC not configured, public access blocked
Fix 1: Origin Access Control (OAC) Not Configured
The most common cause in 2026. AWS deprecated Origin Access Identity (OAI) and replaced it with OAC. If you set up a CloudFront + S3 distribution without OAC, S3 will block requests.
Check it:
aws cloudfront get-distribution \
--id YOUR_DISTRIBUTION_ID \
--query 'Distribution.DistributionConfig.Origins.Items[*].S3OriginConfig'Fix — Create OAC and attach it:
- Create OAC:
aws cloudfront create-origin-access-control \
--origin-access-control-config '{
"Name": "my-oac",
"Description": "OAC for S3",
"SigningProtocol": "sigv4",
"SigningBehavior": "always",
"OriginAccessControlOriginType": "s3"
}'
# Note the Id from the output-
Update your distribution to use OAC (easier via Console):
- CloudFront → Distributions → Your distribution → Origins → Edit
- Set "Origin access" to "Origin access control settings"
- Select your OAC
- Copy the S3 bucket policy it shows you
-
Update S3 bucket policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"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_DIST_ID"
}
}
}
]
}aws s3api put-bucket-policy \
--bucket your-bucket-name \
--policy file://policy.jsonFix 2: S3 Block Public Access Is On — But No OAC
Symptom: Direct S3 URL works, CloudFront URL gives 403.
If your bucket has Block Public Access enabled (it should!) but you're trying to serve objects publicly through CloudFront without OAC, S3 will reject every CloudFront request.
Check:
aws s3api get-public-access-block --bucket your-bucket-nameFix: Use OAC (Fix 1 above). Don't disable Block Public Access — that's a security risk. OAC is the right solution.
Fix 3: Wrong Origin Domain
Symptom: 403 for all requests, distribution looks correctly configured.
Common mistake: Using the S3 bucket website endpoint instead of the S3 REST API endpoint.
# WRONG — website endpoint (can't use OAC with this)
your-bucket.s3-website-us-east-1.amazonaws.com
# CORRECT — REST API endpoint (use with OAC)
your-bucket.s3.us-east-1.amazonaws.comCheck your origin domain in the CloudFront distribution settings. If it ends in s3-website, change it to the REST endpoint format.
Exception: If you need S3 static website features (custom error pages, index documents), you must use the website endpoint — but then you can't use OAC, so the bucket itself must allow public access for that specific use case.
Fix 4: Missing Default Root Object
Symptom: https://your-domain.com/ returns 403, but https://your-domain.com/index.html works.
Cause: CloudFront doesn't know what to serve for /.
Fix:
aws cloudfront update-distribution \
--id YOUR_DIST_ID \
--if-match $(aws cloudfront get-distribution-config --id YOUR_DIST_ID --query 'ETag' --output text) \
--distribution-config file://dist-config.jsonOr via Console: CloudFront → Distribution → General → Edit → Default root object → set index.html
Fix 5: Geo Restriction Blocking the Request
Symptom: Works from some locations, 403 from others.
# Check if geo restriction is enabled
aws cloudfront get-distribution \
--id YOUR_DIST_ID \
--query 'Distribution.DistributionConfig.Restrictions'If your distribution has a whitelist/blacklist, requests from blocked countries get 403.
Fix: Update the geo restriction in CloudFront → Distribution → Security → Restrictions.
Fix 6: AWS WAF Blocking the Request
Symptom: 403 with X-Cache: Error from cloudfront header. The request never reaches S3.
Check:
# Check if WAF is associated
aws cloudfront get-distribution \
--id YOUR_DIST_ID \
--query 'Distribution.DistributionConfig.WebACLId'If a WebACL is associated, check WAF logs:
# Enable WAF logging first if not already enabled
# Then query logs in CloudWatch or S3
aws wafv2 list-web-acls --scope CLOUDFRONT --region us-east-1Common WAF rules that cause false 403s:
- Rate limiting rules triggered by your own testing
- IP reputation lists blocking your IP
- Managed rule groups with aggressive settings
Fix: Review WAF rules, add an IP whitelist for your office/CI IPs, or temporarily disable the rule to confirm it's the cause.
Fix 7: CloudFront Signed URLs/Cookies Required
Symptom: 403 on all requests, CloudFront error code shows "Missing Key-Pair-Id".
Cause: The distribution is configured to require signed URLs/cookies but you're making unsigned requests.
# Check behavior for signed URL requirement
aws cloudfront get-distribution \
--id YOUR_DIST_ID \
--query 'Distribution.DistributionConfig.DefaultCacheBehavior.TrustedKeyGroups'If TrustedKeyGroups is set, you must generate a signed URL:
import boto3
from botocore.signers import CloudFrontSigner
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import datetime
def rsa_signer(message):
with open('private_key.pem', 'rb') as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None,
backend=default_backend()
)
return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1())
cf_signer = CloudFrontSigner('YOUR_KEY_PAIR_ID', rsa_signer)
signed_url = cf_signer.generate_presigned_url(
'https://your-domain.com/protected-file.pdf',
date_less_than=datetime.datetime.now() + datetime.timedelta(hours=1)
)Fix: Either generate signed URLs in your app, or remove the trusted key group requirement if you don't need access control.
Fix 8: Cache Behavior Path Pattern Mismatch
Symptom: Some paths work, specific paths return 403.
Cause: A cache behavior with a specific path pattern has different origin or viewer protocol settings.
aws cloudfront get-distribution \
--id YOUR_DIST_ID \
--query 'Distribution.DistributionConfig.CacheBehaviors'Check that all path pattern behaviors point to the correct origin and have the right viewer protocol (redirect HTTP to HTTPS vs HTTP only).
Debugging Checklist
# 1. Check where 403 originates
curl -sv https://your-cf-domain.com/test.html 2>&1 | grep -E "(< HTTP|X-Cache|x-amz)"
# 2. Test direct S3 access (using AWS credentials)
aws s3 cp s3://your-bucket/test.html /tmp/test.html
# 3. Verify OAC configuration
aws cloudfront get-distribution --id DIST_ID \
--query 'Distribution.DistributionConfig.Origins.Items[*].OriginAccessControlId'
# 4. Verify S3 bucket policy allows CloudFront OAC
aws s3api get-bucket-policy --bucket your-bucket | jq .
# 5. Check CloudFront distribution status
aws cloudfront get-distribution --id DIST_ID \
--query 'Distribution.Status'
# Must be "Deployed" not "InProgress"
# 6. Invalidate cache after any fix
aws cloudfront create-invalidation \
--distribution-id DIST_ID \
--paths "/*"| Error source | Likely cause | Fix |
|---|---|---|
| CloudFront (X-Cache: Error) | WAF, geo restriction, signed URL | Check WAF/restrictions |
| S3 origin | OAC missing, bucket policy wrong | Configure OAC + bucket policy |
| All paths | Wrong origin domain | Use REST endpoint, not website endpoint |
| Root path only | No default root object | Set index.html as default |
| Some IPs only | WAF rate limit or IP block | Whitelist or adjust WAF rules |
CloudFront 403 errors almost always come down to OAC not being configured correctly or the S3 bucket policy not allowing CloudFront's service principal.
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.
Related Articles
AWS ALB 504 Gateway Timeout — Every Cause and Fix (2026)
Your ALB returns 504 Gateway Timeout but the app seems fine. Here's every reason this happens — backend timeouts, keepalive mismatches, health check failures — and exactly how to fix each one.
AWS ALB Showing Unhealthy Targets — How to Fix It
Fix AWS Application Load Balancer unhealthy targets. Covers health check misconfigurations, security group issues, target group problems, and EKS-specific ALB controller debugging.
AWS RDS Connection Timeout from EKS Pods — How to Fix It
EKS pods can't connect to RDS? Fix RDS connection timeouts from Kubernetes — covers security groups, VPC peering, subnet routing, and IAM auth issues.