2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Web APIをCloudflare(CDN) × API Gateway(WAF)で保護する

Last updated at Posted at 2024-06-02

はじめに

Web API(REST APIのバックエンドのサーバ)を構築するときに、できるだけセキュアで時間課金ではなく従量課金にする方法を考えていた。そしてAWSのリソースを利用してサーバを構築し、オリジンサーバへのアクセスを保護するためにWAFを導入したかった。

ふと、Cloudflare(CDN)とAPI Gatewayの組み合わせで、

  • mTLS
  • WAFによるHTTPヘッダー検証

の組み合わせで保護するのが結構いい感じなのではと思いやってみた。

※AWSでCDNと言えばCloudFrontだが、CloudFrontについて検討した事はおまけで取り上げたいと思う。

今回やろうとしていることの完成図

今回やろうとしていることの最終的な完成図は以下。
image.png

上記のような構成をとることで、多層防御(Defense in Depth)を実現できる。また、ネットワークでアクセスがどこまで到達するか?を考えてみると、以下のようにオリジンサーバであるLambdaにまで到達しないのが特徴。

  • mTLS
    • トランスポート層でTCPコネクションを確立した後にTLSハンドシェイクをする時に行われる。つまり、API GatewayがSSL/TLSの終端になっているので、オリジンサーバであるLambdaまでリクエストは到達しない。これはALBなどでも同じ。
  • WAFでのHTTPヘッダー検証
    • mTLSが終わり、実際に暗号通信をしてAPI Gatewayまでリクエストが届くが、そのリクエストの内容はSSL/TLSの終端であるAPI Gatewayで復号される。その後、Lambdaにプロキシされるが、その前にWAFでのチェックが入る。つまり、HTTPヘッダーの検証ではじけば、オリジンサーバであるLambdaまでリクエストは到達しない。これもまたALBでも同じ。

※Web APIの保護という点では、OAuth2.0のアクセストークン(JWT)を利用することが多いと思われるが、それについては本記事では触れない。OAuth2.0のアクセストークンでの保護はアプリケーション層での保護に該当する。

では実際にやっていきたいと思う。

その前に API Gateway+Lambda関数の準備

今回はAPI Gatewayでのカスタムドメイン設定を行い、mTLS(相互TLS認証)の設定・WAFの設定をするのがメインなので、Lambda関数は適当に200・hello worldを返す関数とする。以下のテンプレートを少しいじって作ると楽。
image.png

Deployした関数のコードは以下。
image.png

続いて、API Gatewayと統合する。マネージメントコンソールの「トリガーの追加」からだとAPIキーが必要になってしまったり不便なのでAPI Gatewayのメニューから作成する。
REST API を作成からAPIを作成し、リソースを作成からプロキシのリソースを作成する。
image.png

続いてメソッドを作成から/{proxy+}に対してANYを設定し、統合タイプにLambdaを選択して先ほど作成したLambda関数を選択する。ここまで出来たらいったんテストからLambdaとうまく統合できているか?を確認しておく。
image.png

問題なければAPIをデプロイから適当なステージ名をつけてデプロイし、今度はcurl等で今設定したAPIを呼び出してみる。
image.png

$ curl https://<APIのID>.execute-api.ap-northeast-1.amazonaws.com/default/api/test
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    11  100    11    0     0     54      0 --:--:-- --:--:-- --:--:--    55
hello world

ここまでで事前準備は完了。以降では、カスタムドメインの設定をして、mTLS(相互TLS認証)の設定・WAFの設定をすることで、API Gatewayのエンドポイントを保護してみたいと思う。

API GatewayでのmTLSの設定

まず、mTLSの設定にはConfiguring mutual TLS authentication for a REST APIに書かれている通り、API Gatewayのカスタムドメインの設定が必要になる。そしてカスタムドメインの設定の際にはサーバ証明書が、またmTLSの設定時にはクライアント証明書の指定も必要になる。

色々やることがあるが、以下の順にやっていく。

  1. SSLサーバ証明書をACM(AWS Certificate Manager)に登録する
  2. ドメイン所有のパブリック証明書の発行する
  3. 信頼ストアとしてmTLSで利用するクライアント証明書(公開鍵)を配置する
  4. API Gatewayでカスタムドメインの作成をする(mTLSは設定しない)
  5. 4の手順で設定したカスタムドメインの名前解決が行われるようにDNSでCNAMEレコードを設定する
  6. mTLSの設定を有効にする

1. SSLサーバ証明書をACM(AWS Certificate Manager)に登録する

まずはACMにカスタムドメインに対するSSLサーバ証明書が必要になる。今回はCloudflareをCDNとして利用するので、Cloudflareで発行したオリジン証明書(サーバ証明書)を利用する。

ACMの証明書をインポートからCloudflareで発行したサーバ証明書を入力する(サーバ証明書の発行についてはCloudflareのSSL/TLSについてを参照)。インポートできると以下のように表示される。なお、Cloudflareのオリジン証明書は自前で発行したものに等しいので自動更新されないことに注意。
image.png

2. ドメイン所有のパブリック証明書の発行

パブリック証明書とは、カスタムドメインの設定の以下で指定するACMの証明書のことで、ドメインの所有者であることを証明するために必要。
image.png

ドメインの所有確認は、DNSのCNAMEを設定して行う方法が簡単なのでそれを行う。まず証明書をリクエストから証明書の発行を依頼する。依頼すると保留中になってDNSで設定すべきCNAMEレコードの内容が表示されるのでその通りに設定する。
image.png

お名前ドットコムでドメインは取得したが、ドメインの管理はCloudflareのネームサーバで行うように設定していたので、CloudflareのDNSメニューから以下のようにCNAMEを設定する。
image.png

うまくいくと以下のようにステータスが〇〇になる(DNSの設定を更新してから10分くらいはかかった気がする)。
image.png

ownershipVerificationCertificate does not cover one or more subjects in the imported/private certificate.みたいなエラーが出るときは、1で登録したサーバ証明書のドメインと乖離があるので、そこを確認する。

3. 信頼ストアとしてmTLSで利用するクライアント証明書(公開鍵)を配置する

Updating your truststoreに書かれている手順。
この手順は、mTLSではクライアント証明書を使ってリクエストを送ってきたクライアントを信頼するが、仕組みとしてはTLSハンドシェイクの時にクライアントから送られてくる署名データを公開鍵(クライアント証明書)で検証するので、そのためのクライアント証明書をS3に置くという事をやる。

適当なバケットを作成してそこにCloudflareが用意しているクライアント証明書をアップロードする(1. Upload certificate to originからCloudflareのクライアント証明書は取得できる)。

API Gatewayのカスタムドメインの設定ではS3のURIを利用するので、それを控えておく。

s3://<バケット名>/ssl/authenticated_origin_pull_ca.pem

4. API Gatewayでカスタムドメインの作成をする(mTLSは設定しない)

続いて、API Gatewayのカスタムドメインの設定をする。まずはカスタムドメインの設定後に疎通できるか?を確認したいので、シンプルにmTLSの設定はしないでやってみる。

API Gatewayの画面のカスタムドメイン名メニューから作成する。エンドポイント設定で1の手順で登録したACMのサーバ証明書を選択する。
image.png

設定がうまくいくと以下のように利用可能なステータスになる。
image.png

どのAPIをこのカスタムドメインにマッピングするのかを設定するために、APIマッピングから作成済みのAPIとのマッピングを作成する。
image.png

これでカスタムドメインの設定は(mTLS以外)については一旦完了になる。

参考

5. 4の手順で設定したカスタムドメインの名前解決が行われるようにDNSでCNAMEレコードを設定する

4の手順を完了しただけでは、カスタムドメインで設定したドメインrestapi.<取得したドメイン>.comの名前解決が行われないので以下のようにエラーになる。

$ curl restapi.<取得したドメイン>.com
curl: (6) Could not resolve host: restapi.<取得したドメイン>.com

$ nslookup restapi.<取得したドメイン>.com
Server:         192.168.11.1
Address:        192.168.11.1#53

** server can't find restapi.<取得したドメイン>.com: NXDOMAIN

名前解決されるように、DNSで以下のようにCNAMEレコードを設定する。
image.png

ここでCNAMEレコードの値に設定するFQDNはカスタムドメインの詳細のAPI Gatewayドメイン名に記載されたものなので注意。
image.png
APIのステージから確認できる以下のURIのドメインではない。
image.png

この設定をすれば以下のようにAPI Gatewayのカスタムドメインで設定した(サブ)ドメインで名前解決されるようになり、API GatewayのエンドポイントのURIではなく、カスタムドメインでAPI Gatewayのエンドポイントにアクセスできる。

$ nslookup restapi.<取得したドメイン>.com
Server:         192.168.11.1
Address:        192.168.11.1#53

Non-authoritative answer:
Name:   restapi.<取得したドメイン>.com
Address: 104.21.76.226
Name:   restapi.<取得したドメイン>.com
Address: 172.67.201.235

$ curl https://restapi.<取得したドメイン>.com/api/test
hello world

ちなみに、CloudflareのCND経由でAPI Gatewayにアクセスしているので、レスポンスヘッダーをみると色々各サービスでヘッダーが追加されていることも確認できる(セキュリティ的にはこれらのヘッダーは意図的に隠すべきものもあると思われる)。

$ curl https://restapi.<取得したドメイン>.com/api/test -I
HTTP/2 200 
date: Fri, 31 May 2024 06:50:10 GMT
content-type: application/json
content-length: 11
x-amzn-requestid: 6ba1beda-4dae-4365-8e3b-ae9a854e5cec
x-amzn-remapped-content-length: 11
x-amz-apigw-id: ........
x-amzn-trace-id: Root=........;Parent=........;Sampled=0;lineage=71f4e8d9:0
cf-cache-status: DYNAMIC
report-to: {"endpoints":[{"url":"........"}],"group":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
server: cloudflare
cf-ray: ........
alt-svc: h3=":443"; ma=86400

CloudWatchのログも確認してみると、以下のように確認できるので疎通に問題ないと判断できる。
image.png

6. mTLSの設定を有効にする

API Gatewayのカスタムドメインの設定で、mTLSを有効にしCloudflareからのリクエストのみを受け付けるように設定を変更する。カスタムドメインの設定画面のドメインの詳細から以下のように相互TLS認証を有効にする。
image.png

あとは更新が完了するまで待つだけだが、1点注意として、以下のようにAPIマッピングで指定しているAPIのデフォルトのエンドポイントが有効になっていると迂回される1

image.png

そこでAPI の設定メニューからデフォルトのエンドポイントを無効にする設定をする。これで設定したカスタムドメイン以外でのオリジンサーバのエンドポイントにアクセスできないくなり、セキュアな状態を作れる。
image.png

カスタムドメインの設定をする際にCNAMEレコードの値に設定したAPI Gateway ドメイン名にはアクセスできないのか?という疑問は残っている。curlで直接リクエストを送ってみると403になったので、何らかの方法ではじいているのかもしれない。
ここは調べてみたが情報は出てこずだった。

API GatewayのmTLSの設定ができたら、Cloudflareの方でも認証済み Origin Pullを有効にする。これを有効にするとCloudflareからのリクエスト時のTLSハンドシェイクでクライアント証明書を送るようになり、mTLSができるようになる。
image.png

設定後、再度リクエストが通るか?を確認してみると以下のように問題なく通ることが確認できる。

$ curl https://restapi.<取得したドメイン>.com/api/test
hello world

また、Cloudflareの設定で認証済み Origin Pullを無効にしてみると、以下のように525エラーになることが確認でき、mTLSが有効になっていることが確認できる。

$ curl https://restapi.<取得したドメイン>.com/api/test
error code: 525

ここまでmTLSの導入はおしまいになる。続いて、WAFによるHTTPヘッダーの検証の設定をやってみる。

API GatewayでのWAFの設定

この設定でもいくつかやるべきことがあるので、以下の順にやっていく。

  1. Web ACLでHTTPヘッダーのあるキーに対する値の有無で許可するか?のルールを作成する
  2. API GatewayでWeb ACLを適用する
  3. CloudflareでカスタムHTTPヘッダーを追加する

1. Web ACLでHTTPヘッダーのあるキーに対する値の有無で許可するか?のルールを作成する

AWS WAFのWeb ACLsの作成を行う。今回はHTTPヘッダーのあるキーでのルールになるので、カスタムルールを追加する。
image.png

以下のようなX-CDN-Secret-Keyというヘッダーキーに対する値が一致するか?というルールを設定する。これでこのカスタムHTTPヘッダーを持たないリクエストはCloudflare(CDN)経由のリクエストではなく、オリジンサーバへの直リクエストと判定できるようになる。
image.png

2. API GatewayでWeb ACLを適用する

API Gatewayのステージメニューからステージを編集で以下のように1の手順で作成したWeb ACLを設定する。
image.png

設定後、カスタムヘッダーがないリクエストを送ってみると、以下のようにForbiddenではじかれることが確認でできる。逆に、ヘッダーがある場合は意図通りにレスポンスが返ってくることも確認できる。

$ curl https://restapi.<取得したドメイン>.com/api/test?query=waf-reject
{"message":"Forbidden"}

$ curl https://restapi.<取得したドメイン>.com/api/test?query=waf-ok -H "x-cdn-secret-key: ....."
hello world

また、Web ACLのダッシュボードでも以下のようにブロックされていること・許可されていることが確認できる。
image.png
image.png

※ちなみに、WAFのログをCloudWatchで確認できるようにするには、Web ACLを作成後、Logging and metricタブのLoggingから有効にする必要がある。

3. CloudflareでカスタムHTTPヘッダーを追加する

Transform Rulesに書かれている通り、リクエストヘッダーを書き換える(追加・変更・削除)ことができる(Cloudflareの機能としては他にも色々できる)。今回はすべてのリクエストにWeb ACLのルールで設定したカスタムヘッダーを追加するようにするので、ルールのメニューの変換ルールから以下のように設定する。
image.png

設定後、今度は2でブロックされていた時と同じように、ヘッダーを指定しないでリクエストを送ってみると、問題なくリクエストが成功することを確認できる。

$ curl https://restapi.<取得したドメイン>.com/api/test?query=not-set-header 
hello world

WAFのチェック結果を見ても許可されていることが確認できる。
image.png

以上で今回やってみたかったことは終わりになる。

まとめとして

今回は、Cloudflare(CDN)とAPI Gatewayを利用して、mTLSとWAFでのHTTPカスタムヘッダーの検証の2つを行い、オリジンサーバのエンドポイントを保護するという事をやってみた。今回やってみたことはProtect your origin serverに書かれている一部で、アプリケーション層とトランスポート層の2層での保護になっている。

この構成のメリットとしては、

  • WAFを除いて従量課金(Cloudflareのフリープランを利用している場合)
  • API GatewayはLambdaをはじめとして、様々なAWSのリソースにリクエストを転送できる(ただし、追加の設定が必要になったりするのでデメリットになりうる)
  • mTLSでの保護ができる

などがあげられるのではないかと思う。

さらにセキュリティを高めたり、Web APIのベストプラクティスに倣う意味でも、OAuthのアクセストークンやJWTによる制御も追加するのがよいと思われる。

おまけ

CDNをCloudFrontにするというアイディアについて

単純にCloudFrontではmTLSができないため。クライアント側の SSL 認証ALB の相互TLS認証(mTLS)は AWS WAF や CloudFront を経由した場合でも利用可能か教えてくださいに書かれている通り、クライアント証明書をサポートしていない。

ALBによるmTLSについて

ALBではApplication Load Balancer での TLS による相互認証にあるように、mTLSをサポートしている。

今回は従量課金で、というのを重視していたのでAPI Gatewayを利用していたが、ALBでもmTLSはサポートされているので選択肢には入ってくると思う。また、オリジンサーバをLambdaではなくEC2やECSなどにするのであれば、ALBを利用した方が設定は楽だったりするかもしれない(API Gatewayの後ろをEC2等にするときは、HTTPカスタム統合などの設定が必要になったりするらしいので(詳細はHTTP カスタム統合の API Gateway でバックエンドのステータスコードと異なるコードが返ってくるのはなぜでしょうかを参照))。

料金について

  • AWS
    • API Gateway、Lambdaは無料枠で収まるが、Web ACL(WAF)は時間課金があるので注意
  • Cloudflare
    • 無料(今回やった内容であれば完全に無料枠内で収まる。HTTPヘッダーの書き換えもルール10個までは無料枠がある。いや~すごい。)

API GatewayのログをCloudWatchでみる

IAMの管理ページでAPI GatewayからCloudWatchにアクセスできるロールを作成する。AWS管理ポリシーでAmazonAPIGatewayPushToCloudWatchLogsというのがあるのでこれを利用する。

作成したIAMロールのARNを控えてAPI Gatewayの設定メニューから以下のように設定する。
image.png

  1. 本来は、カスタムドメインに対してリクエストを送ってもらいCloudflareのCDN経由でAPI GatewayのエンドポイントにアクセスすることでmTLSが行われる。ただ、APIをデプロイしたときに発行されるデフォルトのエンドポイントがある状態では、万が一それがバレルとそこに直接リクエストが送られてきてしまう=CDNを迂回してオリジンサーバに直接リクエストが来る状態になってしまうので非常にまずい、という事。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?