最近、iPhoneのTouch IDが故障してしまった畑野です。
パスキーが利用できなくなり、パスワードアプリのロックを解除できなくて割と困りました。
認証は大事ですね。
さて、今回は最近アップデートされた「Amazon CloudFrontがオリジン向け相互TLSサポート」で、オリジンをAPI Gateway(HTTP API)としたときの保護を検証してみます。
これにより、CloudFrontを経由した通信を証明書レベルで許可し、オリジン(API Gateway)への直接アクセスを安全に遮断できる構成が可能になります。
アップデート内容
検証内容
本記事では以下について検証してみます。
- CloudFront → API Gateway間をmTLS化
- API GatewayはHTTP API
- CloudFrontが提示するクライアント証明書を用いて通信を許可
- 証明書を提示できないAPI Gatewayへの直接通信は拒否
- アップデート内容
- 検証内容
- 今までの問題
- 構成図
- ルートCA用証明書の作成
- クライアント証明書の作成
- トラストストアの配置
- ACMへクライアント証明書をインポート
- API Gateway(HTTP API)とLambda関数の作成
- API GatewayのmTLS設定
- CloudFrontのオリジンmTLS設定
- 動作確認
- まとめ
今までの問題
CloudFrontのオリジンにAPI Gatewayを配置する場合、オリジンへの通信をCloudFrontからのみに制限するにはいくつか方法があります。
例えば、REST APIであればCloudFrontのオリジンにカスタムヘッダーを追加して、API Gateway側でHTTPリクエストヘッダーの検証を行うことで実現できました。
しかし現時点でHTTP APIはREST APIのようなリクエスト検証(マッピングテンプレートやリクエストバリデーターによるヘッダー・ボディのチェック)がなく、
ヘッダー検証を行うにはLambda Authorizer等に委ねる必要があります。 *1
そのため、Lambda Authorizerで独自に検証を実装する必要があり、オーバーヘッドやスロットリングに懸念がありました。*2
そこで今回は、mTLS化によってLambda Authorizerを使わずに、より低オーバーヘッドでオリジン制限できるかを検証します。
構成図
検証のためシンプルな構成にしています。
[ Client ]
|
v
CloudFront
(オリジン向け mTLS / Client Cert)
|
v
API Gateway (HTTP API)
- カスタムドメイン mTLS 必須
- デフォルトエンドポイント無効
|
v
Lambda
ルートCA用証明書の作成
まずはルートCAの証明書を作成し、クライアント証明書を作成します。
秘密鍵の作成
openssl genrsa -out root-ca.key 2048
証明書の作成
検証のため期限は90日と短めにしています。
openssl req -x509 -new \ -key root-ca.key \ -sha256 \ -days 90 \ -out root-ca.crt \ -subj "/C=JP/O=ExampleOrg/CN=example-root-ca" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ -addext "subjectKeyIdentifier=hash"
クライアント証明書の作成
ルートCAの証明書ができたので、次はクライアント証明書を作成します。
秘密鍵の作成
openssl genrsa -out cloudfront-client.key 2048
CSRの作成
openssl req -new \ -key cloudfront-client.key \ -out cloudfront-client.csr \ -subj "/C=JP/O=ExampleOrg/CN=cloudfront-origin-mtls"
クライアント証明書の作成
オリジンmTLSで利用できるクライアント証明書はExtended Key Usage(EKU)に TLS Web Client Authentication が必要です。 *3
EKUはCSRで指定することも出来ますが、証明書の用途をCA側で決定し、発行者保証としたいためこのような手順としています。
openssl x509 -req \ -in cloudfront-client.csr \ -CA root-ca.crt \ -CAkey root-ca.key \ -CAcreateserial \ -out cloudfront-client.crt \ -days 90 \ -sha256 \ -extfile <(printf "%s\n" \ "[v3_client]" \ "keyUsage = digitalSignature" \ "extendedKeyUsage = clientAuth" \ "basicConstraints = CA:FALSE") \ -extensions v3_client
最終的にこれらのファイルが作成されます。
├── cloudfront-client.csr ├── cloudfront-client.crt ├── cloudfront-client.key ├── root-ca.crt ├── root-ca.key
トラストストアの配置
信頼するCA証明書をまとめたトラストストアをS3バケットに配置します。
クライアント証明書を発行したCAを信頼することになります。
実運用では古いトラストストアへ復旧するためにバケットのバージョニングを有効化しておいた方がいいですね。
ルートCA証明書をコピーして、トラストストアを作る
トラストストアとして分かりやすい証明書ファイル名にします。
cp root-ca.crt truststore.crt
トラストストアをS3バケットに配置する
今回は検証のため、バケットポリシーは空の状態ですが、適切なプリンシパルから以外はDenyするポリシーを書いた方が安全です。
aws s3 cp truststore.crt s3://<BUCKET_NAME>/truststore.crt
ACMへクライアント証明書をインポート
リージョンは us-east-1 必須です。
aws acm import-certificate \ --region us-east-1 \ --certificate fileb://cloudfront-client.crt \ --private-key fileb://cloudfront-client.key \ --certificate-chain fileb://root-ca.crt
API Gateway(HTTP API)とLambda関数の作成
手順は割愛しますが、各リソースを作成します。
API GatewayはAPIタイプを HTTP API としてください。
ルートのメソッドはHEADとGETを作成し、ステージのデプロイを忘れないようにしてください。
私はHEADの追加を忘れて、 curl -I の結果が 404 になり悩みました。
参考に弊社ブログを記載します。
CloudFront で API Gateway を保護しよう - サーバーワークスエンジニアブログ
API GatewayのmTLS設定
カスタムドメインを作成します。下記は設定例です。

CloudFrontのオリジンmTLS設定
主な設定は赤枠箇所にご注意ください。
mTLSを有効化し、クライアント証明書を指定してください。
またビヘイビアのオリジンリクエストポリシーは AllViewerExceptHostHeader を指定します。

オリジンへの通信にHostヘッダーを含めないようにすることで、
オリジンドメインがHostヘッダーになり、
API Gatewayのカスタムドメイン宛に通信した際に、APIマッピングされているステージ宛に通信できるようになります。
※ViewerのHostヘッダー(xxx.cloudfront.net)が転送されると、 API Gateway側でカスタムドメインのAPIマッピングに一致せず502や403/404になります。
動作確認
設定が完了したのでまずはCloudFront経由でアクセスしてみます。
CloudFront経由
Lambdaの応答が返ってきますので成功しています。
※CloudFrontのオリジンにmTLSを指定しているため、curlでクライアント証明書の指定は不要です。
クライアント証明書はCloudFront → API Gateway間のmTLSでのみ使用されます。
curl https://xxx.cloudfront.net/ "Hello from Lambda!"
API Gatewayへの直アクセス
カスタムドメイン宛に直接通信すると失敗します。
curl -vの結果を見る限り、TLSハンドシェイクでサーバー証明書の検証には成功しており、
エラーの原因は、クライアント証明書検証に失敗するためと考えられます。*4
curl -v https://mtls.xxx.com/ --- 中略 --- curl: (56) Recv failure: Connection reset by peer
クライアント証明書を指定してアクセスすると、Lambdaの応答が返ってきます。
よって、CloudFrontが提示するクライアント証明書を信頼することで、
証明書を提示できない直接アクセスを遮断できました。*5
curl https://mtls.xxx.com/ \ --cert cloudfront-client.crt \ --key cloudfront-client.key "Hello from Lambda!"
もちろんデフォルトエンドポイントは無効化しているのでアクセスできません。ステージ名はProdとしています。 *6
curl -I https://xxx.execute-api.ap-northeast-1.amazonaws.com/Prod/ HTTP/2 404
まとめ
今回検証したCloudFrontのオリジン向けmTLS を利用すると、
CloudFront → API Gateway間を証明書で相互認証し、
CloudFrontからのオリジンアクセスを安全に許可できました。
HTTP APIでは従来、Lambda Authorizer等でヘッダー検証を行う必要がありましたが、
mTLSを使うことでアプリケーション側の実装を増やさず、
より低オーバーヘッドにオリジン保護を実現できました。
今回は自己証明書を利用しましたが、実運用ではトラストストアやクライアント証明書の期限更新の考慮が必要です。
ACM Private CAでCRLやOCSPは利用できますが、自動チェックは行わないため、失効チェックの仕組みを考える必要があります。 *7
*8 *9
最後に、本構成は追加のアプリケーション実装なしでオリジン制限できるため、
HTTP API を採用する場合の現実的な選択肢になるのではないでしょうか。
*1:他にはJWT Authorizerで認証する方法があります。
*2:WAFでCloudFront エッジサーバーの場所と IP アドレス範囲で制限したり、HTTPヘッダーを検査する方法もありますがWAFのコストがややもったいないです。
*4:エンドポイントはカスタムドメイン用のACM証明書を指定するため
*5:クライアント証明書があれば、CloudFrontを経由しなくてもアクセス可です
*6:HTTP APIの場合は404が返ってきます。
*8:CRLは、AWS プライベート CA を使用して Application Load Balancer に mTLS を設定する方法を教えてください。が流用できそうです