はじめに
AWSにおいて「Client -> CloudFront -> API Gateway(IAM認証有効)」という構成を運用する際、直接APIを叩けば成功するリクエストをCloudFrontが経由した瞬間に署名一致エラー(403 Forbidden) を返すケースがあります。
エラーメッセージ:
403 Forbidden: The request signature we calculated does not match the signature you provided.
この「署名の不一致」は、AWSが提供する強力なセキュリティ機構であるSigV4(署名バージョン 4) の仕様と、CloudFrontの挙動が衝突することで発生します。本記事では、この不一致が起きるメカニズムを解き明かし、現場で取るべき2つの解決策を提示します。
原因:リクエスト情報の「変質」による署名不一致
SigV4署名は、リクエストのあらゆる構成要素(Host、Path、QueryString、Header、Payload)を材料としてハッシュ化し、署名を作成します。例えるなら、「中身が変われば封印が壊れる封筒」 のような仕組みです。
CloudFrontを経由した際に403エラーが出る場合、クライアントが署名した際のリクエスト情報が、API Gatewayに届くまでの間に変化していることが主要な原因であると推測されます。
最も多い原因:Hostヘッダーの不一致
- クライアント側の計算: CloudFrontのドメイン(例: xxx.cloudfront.net)をHostとして署名
- CloudFrontによる書き換え: デフォルトではオリジンへ転送する際、HostをAPI Gatewayのドメイン(例: yyy.execute-api...)に書き換える
- API Gatewayによる検証: 実際に届いたHost(APIGWドメイン)で署名を再計算するため、不一致が発生
公式リファレンス:
AWS 署名バージョン 4 のリファレンス
その他の微細な変化
Hostヘッダー以外にも、以下の要素が署名を無効化させる要因となります
- クエリパラメータの欠落: CloudFrontが転送対象から外している場合
- URLの正規化: パス内のスラッシュの扱いなどがCloudFrontとAPI Gatewayの間で異なる場合
解決策1(推奨): カスタムドメインを使用して情報の整合性を保つ
最も確実で、設計として「あるべき姿」なのが、CloudFrontとAPI Gatewayの両方に共通のカスタムドメイン(例: api.example.com) を適用する方法です。これにより、Hostヘッダーを含むリクエスト情報が最初から最後まで一貫します。
設定のポイント
1. API Gatewayのカスタムドメイン設定
-
エンドポイントタイプ: 「リージョナル(Regional)」 を推奨
- 補足:Edge-optimized(エッジ最適化)でも技術的に構築は可能ですが、内部でCloudFrontを使用するため「二重のCloudFront構成」となり、レイテンシ増加やトラブルシューティングの困難化を招くため非推奨です
- ACM証明書: API Gatewayと同じリージョンで作成したものを指定
- APIマッピング: カスタムドメインを対象のAPIステージに紐付け
2. CloudFrontの設定
-
代替ドメイン名(CNAME):
api.example.comを追加 - オリジンリクエストポリシー: Hostヘッダーをオリジンに転送(Allowlist) する設定を適用
- ACM証明書: バージニア北部(us-east-1) で作成した証明書を指定
3. DNS(Route 53)の設定
-
api.example.comのAレコードを CloudFrontディストリビューション に向ける
解決策2(回避策): クライアント側で署名対象を強制指定する
インフラの構成変更が困難な場合、あるいは暫定的な対応が必要な場合に有効な手法です。リクエストの宛先はCloudFrontのまま、署名計算時のみ「最終的な到達先であるAPI Gateway」のホスト名を明示的に指定します。
Python(boto3)での実装例
import requests
from aws_requests_auth.aws_auth import AWSRequestsAuth
# 実際のリクエスト先 (CloudFront)
url = "[https://xxx.cloudfront.net/v1/resource](https://xxx.cloudfront.net/v1/resource)"
# 署名計算に使うホスト (API Gatewayのオリジンドメイン)
# 送信先と署名対象を分離することで、転送による書き換えを先回りして考慮する
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, # ★署名対象をAPIGWドメインに固定
aws_region='ap-northeast-1',
aws_service='execute-api'
)
# 送信先はCloudFrontだが、署名はAPIGWドメインで計算済み
response = requests.get(url, auth=auth)
💡 Tips: Postmanでのテスト方法
解決策2を採用する場合、Postman標準の「AWS Signature」認証機能は利用できません。Postmanの標準機能はリクエストURLのドメインを署名ホストとして自動使用するため、今回のような「署名ホストのみを差し替える」という特殊な操作に対応していないためです。
この制限を回避し、Postman上で署名ホストを上書きして疎通確認を行うためのPre-request Scriptを公開しています。デバッグの際にご活用ください。