Error Medic

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

Last updated:
Last verified:
2,088 words
Key Takeaways
  • 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.
Fix Approaches Compared
Root CauseDiagnostic SignalFix MethodTime to ResolveChange Risk
S3 OAC/OAI misconfiguration403 from S3 origin; X-Cache: Miss from cloudfrontUpdate S3 bucket policy with correct OAC principal5–15 minLow — policy-only change
WAF rate-based rule blockWAF sampled requests show BLOCK; high req/sec in CloudWatchRaise rate limit threshold or add IP whitelist rule10–20 minMedium — may expose origin
Geo-restriction blockCloudFront access log cs-uri-stem + sc-status 403; viewer country in x-forwarded-forAdd country to allowed list or switch to whitelist mode5 minLow
Expired signed URL / Cookie403 immediately after URL was valid; Expires param in pastRe-generate signed URL with future epoch timestamp2 minNone
Origin read timeout (504 masking as 403)X-Cache: Error from cloudfront; origin slow to respondIncrease origin response timeout (max 60 s); optimize origin15–30 minMedium — latency trade-off
Missing S3 object ACL (non-OAC setup)s3:GetObject AccessDenied in CloudTrailSet object ACL to public-read or switch to OAC10 minLow
SSL/TLS viewer protocol mismatchHTTP 403 on HTTP requests when policy is Redirect to HTTPSSet viewer protocol policy to Redirect HTTP to HTTPS5 minLow

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

  1. Go to CloudFront console → Distribution → Logging → Enable standard logging to an S3 bucket.
  2. 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:

  1. Open AWS WAF Console → Web ACLs → select the ACL attached to your CloudFront distribution.
  2. Click Sampled requests for each rate-based rule — confirm the blocked IPs and request paths.
  3. Check CloudWatch metric BlockedRequests for 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

bash
#!/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 ==="
E

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

Related Articles in AWS CloudFront

Explore More Cloud Infrastructure Guides