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.
Quick Fix: Start With the Failing Request
Error:
403 ForbiddenorAccessDeniedthrough CloudFront.First check: Compare
/index.htmlwith/. If only/fails, set the default root object toindex.htmlwithout a leading slash.Private S3 origin: Use the S3 REST endpoint, attach OAC, and allow the CloudFront service principal in the bucket policy with your distribution ARN. Keep Block Public Access enabled. Existing OAI setups need their corresponding policy instead.
Verify: Wait for the distribution to finish deploying, then request the same object again. Correlate CloudFront, WAF, and origin logs if it still fails.
X-Cache: Error from cloudfrontalone does not identify the blocking layer.
These checks narrow down the cause; the sections below cover permissions, origin selection, and viewer restrictions.
First: Identify Where the 403 Is Coming From
Record the response headers and request ID, then correlate the request with CloudFront, WAF, and origin logs:
curl -I https://your-cloudfront-domain.com/index.html
# Look for:
# X-Cache: Error from cloudfront — an error was served; origin errors can also appear this way
# x-amz-cf-id — useful when correlating the failing requestThe response body may provide another clue, but confirm it against logs:
- Viewer restriction → WAF, geo restriction, or signed URL required
- 403 from S3 → Bucket policy missing or wrong, OAC not configured, public access blocked
Fix 1: Origin Access Control (OAC) Not Configured
AWS recommends OAC for new private S3 origins. OAI remains a legacy option; missing OAC alone does not prove the cause if the distribution already uses a correctly authorized OAI. For OAC, both the origin configuration and bucket policy must agree.
Check it:
aws cloudfront get-distribution \
--id YOUR_DISTRIBUTION_ID \
--query 'Distribution.DistributionConfig.Origins.Items[*].{Domain:DomainName,OAC:OriginAccessControlId,OAI:S3OriginConfig.OriginAccessIdentity}'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: An authenticated S3 request works, but the same object through CloudFront returns 403. Your IAM identity and CloudFront have different permissions.
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: WAF logs show a blocking rule for the matching request. The X-Cache header alone cannot establish that WAF blocked it.
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: Identify the terminating rule in WAF logs and make a narrowly scoped correction. Test rule changes against representative traffic; avoid disabling the whole web ACL or bypassing protection for all visitors.
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. If the old error is cached, invalidate only the affected path
aws cloudfront create-invalidation \
--distribution-id DIST_ID \
--paths "/test.html"| Error source | Likely cause | Fix |
|---|---|---|
| WAF or viewer restrictions confirmed in logs/configuration | WAF, geo restriction, signed URL | Check the matching restriction |
| 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 |
Related Error Guides
- S3 403 Forbidden: investigate object and bucket permissions when S3 itself denies access.
- S3 CORS errors: diagnose browser cross-origin failures separately from authorization errors.
- Cross-account S3 403 fix: check permissions on both sides of a cross-account request.
Sources and Review
Reviewed on September 12, 2026. A 403 can originate from several layers, so use request-level evidence before changing access policies.
Did this fix work?
Tell us what needs improving. No account required.
Today I Fixed
Short real fixes from production — posted daily
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 Target Group Unhealthy — Every Cause and Fix
Your ALB shows targets as unhealthy and traffic isn't reaching your app. Here's every reason target health checks fail 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.