AWS CloudFront 403 Forbidden: Complete Troubleshooting Guide (Rate Limits, Timeouts & Fixes)
Fix AWS CloudFront 403 Forbidden errors fast. Step-by-step diagnosis covering S3 OAC misconfig, WAF blocks, geo-restrictions, signed URL expiry, and rate limits
- The most common cause of CloudFront 403 is a missing or misconfigured S3 bucket policy that does not grant the Origin Access Control (OAC) or legacy Origin Access Identity (OAI) principal read permissions on the bucket.
- WAF rate-based rules silently return 403 when a viewer exceeds the configured request threshold — check AWS WAF sampled requests before assuming an S3 or distribution config problem.
- Geo-restriction rules, signed URL/cookie expiration, and incorrect cache-behavior path patterns each produce identical 403 responses; use CloudFront access logs and the X-Cache and X-Amz-Cf-Id response headers to pinpoint the exact rejection layer before making any changes.
- Quick fix checklist: (1) confirm OAC policy is attached to the S3 bucket, (2) check WAF rule actions in CloudWatch, (3) validate signed URL expiry timestamp, (4) review geo-restriction allow/block lists, (5) inspect origin response timeout and increase if needed.
| Root Cause | Diagnostic Signal | Fix Method | Time to Resolve | Change Risk |
|---|---|---|---|---|
| S3 OAC/OAI misconfiguration | 403 from S3 origin; X-Cache: Miss from cloudfront | Update S3 bucket policy with correct OAC principal | 5–15 min | Low — policy-only change |
| WAF rate-based rule block | WAF sampled requests show BLOCK; high req/sec in CloudWatch | Raise rate limit threshold or add IP whitelist rule | 10–20 min | Medium — may expose origin |
| Geo-restriction block | CloudFront access log cs-uri-stem + sc-status 403; viewer country in x-forwarded-for | Add country to allowed list or switch to whitelist mode | 5 min | Low |
| Expired signed URL / Cookie | 403 immediately after URL was valid; Expires param in past | Re-generate signed URL with future epoch timestamp | 2 min | None |
| Origin read timeout (504 masking as 403) | X-Cache: Error from cloudfront; origin slow to respond | Increase origin response timeout (max 60 s); optimize origin | 15–30 min | Medium — latency trade-off |
| Missing S3 object ACL (non-OAC setup) | s3:GetObject AccessDenied in CloudTrail | Set object ACL to public-read or switch to OAC | 10 min | Low |
| SSL/TLS viewer protocol mismatch | HTTP 403 on HTTP requests when policy is Redirect to HTTPS | Set viewer protocol policy to Redirect HTTP to HTTPS | 5 min | Low |
Understanding the AWS CloudFront 403 Forbidden Error
CloudFront can generate a 403 response at three distinct layers: the CloudFront edge itself (geo-restriction, signed URL validation, viewer protocol policy), AWS WAF (rate-based or managed rule block), and the origin (S3 access denied, ALB/API Gateway auth failure). Each layer stamps different headers and log fields, so the first rule of debugging is always: identify which layer rejected the request.
The canonical error the developer sees in a browser or curl response is:
HTTP/1.1 403 Forbidden
X-Cache: Error from cloudfront
X-Amz-Cf-Id: abc123XYZrandomRequestId==
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
</Error>
When the block comes from WAF rather than S3, the body is often empty or contains a custom WAF response body you configured.
Step 1: Identify the Rejection Layer
Check the X-Cache response header first.
X-Cache: Hit from cloudfront— CloudFront served from cache; 403 was cached from a previous origin response.X-Cache: Miss from cloudfront— CloudFront forwarded to origin and origin returned 403.X-Cache: Error from cloudfront— CloudFront itself or WAF rejected the request before reaching origin.
Run a verbose curl to expose all headers:
curl -sI "https://d1234abcd.cloudfront.net/path/to/object" \
-H "Accept: */*" \
--write-out "\n%{http_code}\n"
Capture the X-Amz-Cf-Id value — you will need it when searching CloudFront access logs.
Enable and query CloudFront standard logs (if not already enabled):
- Go to CloudFront console → Distribution → Logging → Enable standard logging to an S3 bucket.
- Wait 5–10 minutes for log delivery, then query with Athena or download directly.
The key log fields for 403 triage are: sc-status, x-edge-result-type, x-edge-response-result-type, cs-uri-stem, x-forwarded-for, and cs(User-Agent).
Step 2: Fix S3 Origin Access Control (OAC) Issues
OAC is the modern replacement for OAI and the most common source of 403 errors when serving static sites or assets from S3.
Symptom: X-Cache: Miss from cloudfront with AccessDenied XML body.
Root cause: The S3 bucket policy does not include a statement granting the CloudFront service principal (OAC) s3:GetObject permission.
Fix: In the CloudFront console, navigate to your distribution → Origins → select the S3 origin → click Edit. Under Origin access, confirm Origin access control settings is selected and an OAC is assigned. Then click Copy policy and paste it into your 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::ACCOUNT-ID:distribution/DISTRIBUTION-ID"
}
}
}
]
}
Replace YOUR-BUCKET-NAME, ACCOUNT-ID, and DISTRIBUTION-ID with real values. Block Public Access settings on the bucket can remain enabled — OAC bypasses them via the service principal.
Step 3: Diagnose and Fix WAF Rate-Based Rule Blocks
AWS WAF rate-based rules return 403 (or a custom response code you set) when a single IP exceeds the configured request count within a 5-minute window. The default threshold for many managed rule groups is 2,000 requests per 5 minutes.
Symptom: 403 bursts correlating with traffic spikes; X-Cache: Error from cloudfront; no S3 or origin errors in CloudTrail.
Diagnose:
- Open AWS WAF Console → Web ACLs → select the ACL attached to your CloudFront distribution.
- Click Sampled requests for each rate-based rule — confirm the blocked IPs and request paths.
- Check CloudWatch metric
BlockedRequestsfor the specific rule name.
Fix options:
- Raise the rate limit on the rule: WAF → Rule → Edit → increase Rate limit to a value appropriate for your legitimate traffic.
- Add an IP set allow rule with higher priority than the rate rule for known good IPs (your CDN, monitoring services, etc.).
- Scope down the rate rule using scope-down statements so it only counts requests to sensitive endpoints (e.g.,
/api/auth) rather than all paths.
Step 4: Fix Geo-Restriction 403s
Symptom: 403 only for users in specific countries; CloudFront logs show ForbiddenByGeorestriction in x-edge-result-type.
Navigate to CloudFront console → Distribution → Geographic restrictions. Switch between whitelist (allow specific countries) and blacklist (block specific countries) modes, then add or remove the relevant country codes (ISO 3166-1 alpha-2 format, e.g., CN, RU, IR).
Step 5: Regenerate Expired Signed URLs
CloudFront signed URLs embed an Expires Unix timestamp. Once that timestamp passes, CloudFront returns 403 with the body <Message>Request has expired</Message>.
Verify with:
# Extract Expires from a signed URL query string
URL="https://d1234.cloudfront.net/file.mp4?Expires=1700000000&..."
EXPIRES=$(echo "$URL" | grep -oP 'Expires=\K[0-9]+')
echo "Expires at: $(date -d @$EXPIRES)"
echo "Now: $(date)"
If expired, regenerate the signed URL server-side using your CloudFront key pair. Ensure the DateLessThan condition is set far enough in the future for your use case.
Step 6: Resolve Origin Timeout Issues
When the origin (EC2, ALB, Lambda) does not respond within the configured timeout, CloudFront returns a 504. However, some origin frameworks return 403 under load when request queues overflow. CloudFront's default origin response timeout is 30 seconds and the origin connection timeout is 10 seconds.
Increase timeout via AWS CLI:
aws cloudfront get-distribution-config \
--id EDFDVBD6EXAMPLE \
--query 'DistributionConfig' > dist-config.json
# Edit dist-config.json: find your origin and update:
# "ResponseTimeout": 60,
# "ConnectionTimeout": 15
ETAG=$(aws cloudfront get-distribution-config \
--id EDFDVBD6EXAMPLE \
--query 'ETag' --output text)
aws cloudfront update-distribution \
--id EDFDVBD6EXAMPLE \
--if-match "$ETAG" \
--distribution-config file://dist-config.json
Origin response timeout maximum is 60 seconds. If your origin regularly needs more than 60 seconds, use Lambda@Edge or CloudFront Functions to return a 202/placeholder while async processing completes.
Step 7: Invalidate Cached 403 Responses
CloudFront caches 403 responses according to the cache policy's min-ttl. A misconfigured short-lived fix may still serve stale 403s from edge nodes.
Invalidate affected paths:
aws cloudfront create-invalidation \
--distribution-id EDFDVBD6EXAMPLE \
--paths '/images/*' '/index.html'
For persistent issues, set ErrorCachingMinTTL for 403 to 0 in Distribution → Error Pages while you debug, then restore a sensible TTL (e.g., 30 seconds) once resolved.
Frequently Asked Questions
#!/usr/bin/env bash
# CloudFront 403 Diagnostic Script
# Usage: CF_DIST_ID=EDFDVBD6EXAMPLE CF_DOMAIN=d1234.cloudfront.net bash cf-403-diag.sh
set -euo pipefail
CF_DIST_ID="${CF_DIST_ID:?Set CF_DIST_ID}"
CF_DOMAIN="${CF_DOMAIN:?Set CF_DOMAIN}"
TEST_PATH="${TEST_PATH:-/}"
REGION="${AWS_DEFAULT_REGION:-us-east-1}"
echo "=== 1. Probing endpoint ==="
HTTP_CODE=$(curl -sIo /dev/null -w "%{http_code}" "https://${CF_DOMAIN}${TEST_PATH}")
curl -sI "https://${CF_DOMAIN}${TEST_PATH}" | grep -E 'HTTP|x-cache|x-amz-cf-id|x-amz-cf-pop|server' || true
echo "HTTP Status: $HTTP_CODE"
echo ""
echo "=== 2. Distribution config overview ==="
aws cloudfront get-distribution-config \
--id "$CF_DIST_ID" \
--query 'DistributionConfig.{Status:Enabled,Origins:Origins.Quantity,WAFWebACLId:WebACLId,GeoRestriction:Restrictions.GeoRestriction.RestrictionType}' \
--output table
echo ""
echo "=== 3. Origin access control check ==="
aws cloudfront get-distribution-config \
--id "$CF_DIST_ID" \
--query 'DistributionConfig.Origins.Items[*].{Domain:DomainName,OAC:OriginAccessControlId,Protocol:CustomOriginConfig.OriginProtocolPolicy}' \
--output table
echo ""
echo "=== 4. Error page caching TTLs ==="
aws cloudfront get-distribution-config \
--id "$CF_DIST_ID" \
--query 'DistributionConfig.CustomErrorResponses.Items[*].{Code:ErrorCode,TTL:ErrorCachingMinTTL,ResponseCode:ResponseCode}' \
--output table 2>/dev/null || echo "No custom error pages configured."
echo ""
echo "=== 5. WAF WebACL (if attached) ==="
WAF_ARN=$(aws cloudfront get-distribution-config \
--id "$CF_DIST_ID" \
--query 'DistributionConfig.WebACLId' \
--output text)
if [ "$WAF_ARN" != "None" ] && [ -n "$WAF_ARN" ]; then
echo "WAF ACL ARN: $WAF_ARN"
WAF_ID=$(basename "$WAF_ARN")
aws wafv2 get-web-acl \
--id "$WAF_ID" \
--name "$(aws wafv2 list-web-acls --scope CLOUDFRONT --region us-east-1 --query "WebACLs[?Id=='${WAF_ID}'].Name" --output text)" \
--scope CLOUDFRONT \
--region us-east-1 \
--query 'WebACL.Rules[*].{Name:Name,Action:Action,Priority:Priority}' \
--output table 2>/dev/null || echo "WAF ACL detail fetch failed — check IAM permissions."
else
echo "No WAF WebACL attached to this distribution."
fi
echo ""
echo "=== 6. Recent 403 count from CloudWatch (last 1 hour) ==="
aws cloudwatch get-metric-statistics \
--namespace AWS/CloudFront \
--metric-name 4xxErrorRate \
--dimensions Name=DistributionId,Value="$CF_DIST_ID" Name=Region,Value=Global \
--start-time "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 300 \
--statistics Average \
--output table
echo ""
echo "=== 7. Create invalidation for common cached-error paths ==="
read -rp "Invalidate /* to clear cached 403 responses? [y/N] " CONFIRM
if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then
aws cloudfront create-invalidation \
--distribution-id "$CF_DIST_ID" \
--paths '/*' \
--query 'Invalidation.{Id:Id,Status:Status}' \
--output table
echo "Invalidation submitted. Monitor status in CloudFront console."
fi
echo ""
echo "=== Diagnosis complete ==="Error Medic Editorial
Error Medic Editorial is a team of senior DevOps and SRE engineers with collective experience managing large-scale cloud infrastructure on AWS, GCP, and Azure. We write actionable troubleshooting guides grounded in real incident postmortems, AWS documentation, and open-source community findings. All diagnostic scripts and configuration examples are tested against live AWS environments before publication.
Sources
- https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html
- https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html
- https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/HTTPStatusCodes.html
- https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-rate-based.html
- https://repost.aws/knowledge-center/cloudfront-troubleshoot-403
- https://stackoverflow.com/questions/19037664/how-do-i-make-my-amazon-s3-cloudfront-access-public
- https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesOriginResponseTimeout