🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
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.

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

Quick Fix: Start With the Failing Request

Error: 403 Forbidden or AccessDenied through CloudFront.

First check: Compare /index.html with /. If only / fails, set the default root object to index.html without 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 cloudfront alone 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:

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

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

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

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

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: WAF logs show a blocking rule for the matching request. The X-Cache header alone cannot establish that WAF blocked it.

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

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. If the old error is cached, invalidate only the affected path
aws cloudfront create-invalidation \
  --distribution-id DIST_ID \
  --paths "/test.html"

Error sourceLikely causeFix
WAF or viewer restrictions confirmed in logs/configurationWAF, geo restriction, signed URLCheck the matching restriction
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
  • 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

Browse fixes
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