Introduction
When building secure AWS architectures, a common pattern is routing traffic through CloudFront to an API Gateway with IAM Authentication (SigV4) enabled.
However, many developers encounter a frustrating issue: requests that succeed when sent directly to API Gateway fail with a 403 Forbidden error when sent via CloudFront.
The Error Message:
403 Forbidden: The request signature we calculated does not match the signature you provided.
This "Signature Mismatch" is a classic pitfall caused by the way AWS Signature Version 4 (SigV4) interacts with CloudFront's default behavior. This article breaks down why this happens and provides two battle-tested solutions.
The Cause: Request Alteration in Transit
The SigV4 signing process generates a hash based on specific request components (Host, Path, Query Strings, Headers, and Payload). Think of it as a tamper-evident seal: if the request is modified even slightly after being signed, the seal becomes invalid.
When a 403 error occurs via CloudFront, it is highly probable that the request information used by the client for signing has been altered before reaching API Gateway.
The Primary Culprit: Host Header Rewrite
-
Client-side Signing: The client signs the request using the CloudFront domain (e.g.,
xxx.cloudfront.net) as theHost -
CloudFront Modification: By default, CloudFront overwrites the
Hostheader with the API Gateway origin domain (e.g.,yyy.execute-api...) before forwarding -
API Gateway Validation: API Gateway recalculates the signature using the
Hostit received (the APIGW domain). Since this doesn't match the originalHostused by the client, the validation fails
Official Reference:
AWS Signature Version 4 Reference
Other Potential Factors
While the Host header is the most common cause, the following can also trigger mismatches:
- Query Parameter Stripping: If CloudFront is configured to not forward specific query strings
- Path Normalization: Differences in how CloudFront and API Gateway handle trailing slashes or redundant path segments
Solution 1 (Recommended): Unify the Host with a Custom Domain
The most robust architectural solution is using a shared Custom Domain (e.g., api.example.com) for both CloudFront and API Gateway. This ensures the Host header remains consistent from the client all the way to the origin.
Implementation Checklist
1. API Gateway Configuration
-
Endpoint Type: Select "Regional"
- Note: Avoid "Edge-optimized" here. Since you are already using your own CloudFront, Edge-optimized creates an unnecessary "Double-CloudFront" setup, increasing latency and debugging complexity
- ACM Certificate: Create this in the same region as your API Gateway
- API Mapping: Map your custom domain to the target API stage
2. CloudFront Configuration
-
Alternate Domain Name (CNAME): Add
api.example.com - Origin Request Policy: Set the policy to forward the Host header (Allowlist)
- ACM Certificate: You must use a certificate created in the US East (N. Virginia - us-east-1) region for CloudFront
3. DNS (Route 53) Setup
- Create an A record for
api.example.comas an Alias to the CloudFront distribution
Solution 2 (Workaround): Client-side Signature Override
If you cannot change the infrastructure, you can resolve the issue in code. The client sends the request to CloudFront but calculates the signature using the API Gateway origin host.
Python (boto3) Implementation
import requests
from aws_requests_auth.aws_auth import AWSRequestsAuth
# The actual request destination (CloudFront)
url = "[https://xxx.cloudfront.net/v1/resource](https://xxx.cloudfront.net/v1/resource)"
# The host used for signature calculation (API Gateway Origin)
# This separates the request destination from the signing target
api_gateway_host = "yyy.execute-api.ap-northeast-1.amazonaws.com"
auth = AWSRequestsAuth(
aws_access_key='YOUR_ACCESS_KEY',
aws_secret_access_key='YOUR_SECRET_KEY',
aws_host=api_gateway_host, # ★ Fix the signing host to APIGW
aws_region='ap-northeast-1',
aws_service='execute-api'
)
# Request is sent to CloudFront, but signed for API Gateway
response = requests.get(url, auth=auth)
print(f"Status: {response.status_code}")
💡 Pro Tip: Testing with Postman
When implementing Solution 2, Postman’s built-in "AWS Signature" authorization cannot be used. This is because Postman automatically extracts the signing host from the request URL and does not allow for the "host mismatch" required in this workaround.
To overcome this limitation, I have published a Postman Pre-request Script that allows you to override the Host header specifically for signature calculation. You can use it to verify your setup before writing any code.
Top comments (0)