本記事では、Amazon CloudFront を用いて、Amazon API Gateway を保護するためにどのような設定を行えばよいか紹介していきます。
CloudFront を API Gateway の前に置くべき理由
クライアントと API Gateway の間に CloudFront を挟むことには、以下のようなセキュリティ・パフォーマンス面でのメリットがあります。
- API の直 URL が秘匿される
- 各地域のエッジを経由するため、通信の高速化が期待できる
- エンドポイントが攻撃を受けた際、単一の API Gateway ではなく、CloudFront の複数のエッジロケーションへ攻撃が分散される。また、AWS Shield Advanced を利用する場合、API Gateway は適用対象ではないが、CloudFront は対象となっている
さらに AWS WAF も組み合わせ、不審なリクエストを弾くことも推奨されます。
昨年の re:Inforce で行われた講演のスライドでも、しれっと API Gateway の前に CloudFront が置かれており、これがベストプラクティスであることがわかります。
https://reinforce.awsevents.com/content/dam/reinforce/2024/slides/NIS305_Secure-your-APIs-the-Well-Architected-way-from-foundation-to-perimeter.pdfreinforce.awsevents.com
では、実際にこれらを組み合わせる際、どのような設定を入れるとよいのでしょうか?
本記事では、「API を一般に公開せず、特定のクライアントのみからリクエストを受け付ける」というケースに的を絞り、検証用のデモ環境を構築する方法を解説していきます。
注意点
- 今回の構成では、Referer ヘッダーを認証に用います。アプリケーション側で Referer ヘッダーを別用途で用いている場合、今回の方法は利用できず、他の認証方法(Lambda オーソライザー等)を用いる必要があるのでご注意ください。
- 検証としての構成なので、WAF の導入など重要な設定はいくつか省いています。同じものを作った場合、検証完了後は早めに環境を無効化もしくは削除することをおすすめします。
具体的な設定方法
今回は Webhook 想定で、検証用のモック統合 API を作成していきます。また、この API をオリジンとした CloudFront ディストリビューションを作成します。
今回の構成ではAPI Gateway の「API キー」機能と「リソースポリシー」機能を用いて、2重の認証を設定します。
API Gateway に錠を2重にかけておき、認証されたクライアントと CloudFront がそれぞれ鍵を持っておくようなイメージです。

① API Gateway の作成
API Gateway のコンソールから「API の作成」をクリックし、「REST API」を構築していきます。

適当な名前(今回は webhook-test)を入れ、他はデフォルトのまま、API を作成します。

今はまだ何も入っていない状態です。「メソッド」欄の「メソッドを作成」をクリックします。

メソッドタイプは「POST」、統合タイプは「Mock」を選び、「メソッドを作成」をクリックします。

「統合レスポンス」タブへ行き、「編集」ボタンを押します。

マッピングテンプレートの「テンプレート本文」に以下を入力し、「保存」を押します。
{
"status": "success",
"message": "Request processed successfully by Mock endpoint."
}

これで Mock API の設定ができました。「API をデプロイ」ボタンをクリックし、これをデプロイしていきます。

ステージ名はなんでもよいですが、今回は test とします。

デプロイできました。「URL を呼び出す」欄の URL で API を叩くことができます。

実際にやってみましょう。API コールには HTTPie を使っています。(インストールされていない場合はpip install httpie などでインストールするか、curl コマンドで代替してください。)

空のリクエストを行い、正常にレスポンスが返ってきました。このように、作成したばかりの API Gateway では、エンドポイントはそのままグローバルに公開されている状態となります。
② CloudFront ディストリビューションの作成
次に、この API Gateway を保護するための CloudFront ディストリビューションを作成していきましょう。
CloudFront のコンソールで「ディストリビューションを作成」をクリックします。
名前は適当に webhook-test-dist としておきます。

Origin type は API Gateway とし、Origin として先ほど作成した API Gateway を指定します。
指定の際は、「Browse APIs」ボタンからリージョンとバージョンを指定することで検索することができます。

今回は検証ですので、WAF は無効としておきます。

設定を確認し、「Create Distribution」をクリックしてディストリビューションを作成します。

ディストリビューションが作成されました。

URL https://(ディストリビューションドメイン名)/test を用いて、先ほどと同じように API を叩いてみます。

同じようなレスポンスが返ってきましたね。この CloudFront ディストリビューションを経由して API を叩けていることがわかります。
③ API Gateway リソースポリシーを用いた認証の設定
この段階では、API Gateway と CloudFront ディストリビューション双方のエンドポイントは認証無しに全てのリクエストを受け付ける状態となっています。ここから、認証のための設定を入れて行きます。
まずは、構成図中の黒い鍵と錠前にあたる、CloudFront の認証設定を行っていきます。この設定により、API Gateway は このディストリビューションを経由しないリクエストを全て拒否するようになります。

まず、パスワード生成器などを用いて、大文字・小文字・数字を用いた十分な長さの文字列を用意します。これは後で Referer ヘッダーの値として設定する認証用文字列です。
API Gateway のコンソールで先ほど作成した webhook-test を開き、左メニューの「リソースポリシー」をクリックします。

「ポリシーを作成」をクリックし、「ポリシーの詳細」に以下を入力します。(リージョン)、(アカウント ID)、(API ID) および (認証用文字列) にはそれぞれ対応する値を入力します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": [
"arn:aws:execute-api:(リージョン):(アカウント ID):(API ID)/*"
]
},
{
"Effect": "Deny",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": [
"arn:aws:execute-api:(リージョン):(アカウント ID):(API ID)/*"
],
"Condition" : {
"StringNotEquals": {
"aws:Referer": "(認証用文字列)"
}
}
}
]
}

左メニューの「リソース」に戻り、「API をデプロイ」をクリックします。先ほどと同じステージ名 test で API をデプロイします。

次に、CloudFront コンソールから先ほど作成したディストリビューションを開きます。
「オリジン」タブへ行き、オリジン (webhook-api の URL) を選択し「編集」をクリックします。

「カスタムヘッダーを追加」欄で以下を追加し、変更を保存します。
- ヘッダー名:
Referer - 値:認証用文字列

これで、1つ目の認証設定は完了です。(反映に数分程度かかる場合があります。)
試しに、先ほどと同じようにこの API を叩いてみましょう。まず、API Gateway の URL を直接用いてリクエストを送信してみると、拒否され 403 が返ってきます。

次にディストリビューションドメイン名を用いてリクエストを送信してみると、今度は正常にレスポンスが返ってきます。

④ API キーを用いた認証の設定
最後に、API キーの設定を行い、認証されたクライアントでない場合にリクエストを拒否するような設定を行います。構成図中の白抜きの鍵と錠前を作っていくイメージです。

まず、API Gateway のコンソール左メニューから「使用量プラン」を開き、「使用量プランを作成」をクリックします。
名前は webhook-test-usage-plan とし、「スロットリング」と「クォータ」をどちらもオフにして作成します。(検証なのでオフにしていますが、実際の環境では設定することを推奨します。)

次に、左メニューから「API キー」を開き、「API キーの作成」をクリックします。
API キーの名前を入力し、「保存」をクリックします。今回は webhook-test-api-key という名前にしておきます。

作成された API キーが表示されます。キーの値を控えておき、「使用量プランに追加」をクリックします。
使用量プランは webhook-test-usage-plan を選択し、「保存」をクリックします。

再び使用量プラン webhook-test-usage-plan の画面へ行き、「関連付けられたステージ」タブから「ステージを追加」をクリックします。

API は webhook-test を、ステージは test を選択し、「使用量プランに追加」をクリックします。

最後に、再び API webhook-test の画面へ行き、「メソッドリクエスト」タブから「編集」をクリックします。

「API キーは必須です」にチェックを入れ、保存します。

保存したら、再び右上の「API をデプロイ」からステージ test でデプロイを行います。
これで、2つ目の認証設定も完了です。(反映に数分程度かかる場合があります。)
試しにまた API を叩いてみましょう。今度は CloudFront 経由であっても、空のリクエストだと Forbidden となり拒否されます。API の直 URL でも当然拒否されます。

そこで、先ほど控えておいた API キーの値を用いて、ヘッダー X-API-Key:(キーの値) を追加してリクエストしてみましょう。

通りました!ちなみにこのヘッダーがあっても、API の直 URL へリクエストした場合はやはり拒否されます。
以上のような設定を行うことで、「適切な API キーを持ったクライアントが」「CloudFront 経由でリクエストした場合」のみ通すようなセキュアな API を構築することができます。ぜひ参考にしてみてください。