How to Fix: AWS CloudFront 403 Forbidden

AWS403High severity

The Scenario

You’re deploying a static site to an S3 bucket fronted by CloudFront. The CI/CD pipeline finishes cleanly, but when you hit the CloudFront domain (e.g., d123.cloudfront.net), you get a white page with “403 Forbidden.” The S3 bucket URL works fine. It’s 4:55 PM on a Friday.

Symptoms

Root Cause

CloudFront is unable to fetch the object from the S3 origin. The most common cause is a missing or misconfigured Origin Access Identity (OAI) — the virtual user identity that CloudFront uses to authenticate to S3. Without the OAI, the S3 bucket policy either denies all anonymous access (blocking CloudFront) or allows all public access (bypassing CloudFront’s security). Another frequent cause is a WAF rule (e.g., geo-restriction or rate-based rule) incorrectly blocking the request.

Resolution (Step-by-Step)

  1. Verify the OAI is configured on the CloudFront distribution

    aws cloudfront get-distribution-config --id <DISTRIBUTION_ID> --query 'DistributionConfig.Origins.Items[*].S3OriginConfig.OriginAccessIdentity'

    Expected output: a non-null value (e.g., origin-access-identity/cloudfront/ABCDEFGHIJKLM). If null, the OAI is missing.

  2. Create an OAI and attach it to the distribution

    # Create OAI
    aws cloudfront create-cloud-front-origin-access-identity \
      --cloud-front-origin-access-identity-config CallerReference=$(date +%s),Comment="OAI for my-distribution"
    # Note the Id from the output, then update distribution:
    aws cloudfront update-distribution --id <DISTRIBUTION_ID> \
      --distribution-config file://updated-config.json

    In updated-config.json, set Origins.Items[*].S3OriginConfig.OriginAccessIdentity to origin-access-identity/cloudfront/<OAI_ID>.

  3. Update the S3 bucket policy to allow only the OAI

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity <OAI_ID>"
          },
          "Action": "s3:GetObject",
          "Resource": "arn:aws:s3:::<BUCKET_NAME>/*"
        }
      ]
    }

    Apply with:

    aws s3api put-bucket-policy --bucket <BUCKET_NAME> --policy file://policy.json
  4. Check WAF rules if using AWS WAF

    aws wafv2 get-web-acl --name <WEB_ACL_NAME> --scope CLOUDFRONT --id <WEB_ACL_ID>

    Look for Block actions on geo-match or rate-based rules. Temporarily disable the rule and test.

  5. Invalidate the CloudFront cache

    aws cloudfront create-invalidation --distribution-id <DISTRIBUTION_ID> --paths "/*"

Why This Sometimes Doesn’t Work

If you’re using a custom origin (e.g., an ALB or EC2) instead of S3, the OAI is irrelevant. In that case, the 403 is likely from the origin itself (e.g., ALB security group blocking CloudFront IPs, or the application returning 403). Also, if you have signed URLs or signed cookies enabled on the distribution, the OAI fix won’t help — you need to verify the signer configuration (trusted key groups) and that the request includes valid signatures.

Verification

curl -s -o /dev/null -w "%{http_code}" https://d123.cloudfront.net/index.html

Should return 200. Also check CloudFront distribution metrics for 4xxErrorRate — it should drop to near zero.

Common Follow-up Questions

Q: Why does the S3 bucket URL work but CloudFront doesn’t?
A: The S3 bucket is likely set to public-read (allows anonymous access), but CloudFront’s OAI is not in the bucket policy, so CloudFront gets denied. The OAI must be explicitly allowed.

Q: I updated the OAI but still get 403. What’s wrong?
A: You likely forgot to invalidate the CloudFront cache. The edge locations are still serving the cached 403 response. Run an invalidation with /*.

Q: Can I use the same OAI for multiple distributions?
A: Yes, a single OAI can be used by multiple distributions. Just reference the same OAI ID in each distribution’s origin config and bucket policy.

Prevent This in the Future

This error almost always happens when infrastructure is deployed manually or without validation. Use infrastructure-as-code (e.g., Terraform, CloudFormation) to enforce OAI and bucket policy as a single atomic unit, and add a post-deploy smoke test that checks the HTTP status code. Proactive monitoring with a tool like Better Stack can catch this within seconds of deployment by testing your CloudFront URL from multiple regions and alerting on non-200 responses. Set up uptime monitoring and alerts with Better Stack