All Articles

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.

DevOpsBoysApr 25, 20265 min read
Share:Tweet

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:

bash
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 403

Also 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:

bash
aws cloudfront get-distribution \
  --id YOUR_DISTRIBUTION_ID \
  --query 'Distribution.DistributionConfig.Origins.Items[*].S3OriginConfig'

Fix — Create OAC and attach it:

  1. Create OAC:
bash
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
  1. 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
  2. Update S3 bucket policy:

json
{
  "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"
        }
      }
    }
  ]
}
bash
aws s3api put-bucket-policy \
  --bucket your-bucket-name \
  --policy file://policy.json

Fix 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:

bash
aws s3api get-public-access-block --bucket your-bucket-name

Fix: 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.

bash
# 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.com

Check 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:

bash
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.json

Or 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.

bash
# 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:

bash
# 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:

bash
# 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-1

Common 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.

bash
# 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:

python
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.

bash
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

bash
# 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 sourceLikely causeFix
CloudFront (X-Cache: Error)WAF, geo restriction, signed URLCheck WAF/restrictions
S3 originOAC missing, bucket policy wrongConfigure OAC + bucket policy
All pathsWrong origin domainUse REST endpoint, not website endpoint
Root path onlyNo default root objectSet index.html as default
Some IPs onlyWAF rate limit or IP blockWhitelist 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.

Newsletter

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

Comments