はじめに
Web API(REST APIのバックエンドのサーバ)を構築するときに、できるだけセキュアで時間課金ではなく従量課金にする方法を考えていた。そしてAWSのリソースを利用してサーバを構築し、オリジンサーバへのアクセスを保護するためにWAFを導入したかった。
ふと、Cloudflare(CDN)とAPI Gatewayの組み合わせで、
- mTLS
- WAFによるHTTPヘッダー検証
の組み合わせで保護するのが結構いい感じなのではと思いやってみた。
※AWSでCDNと言えばCloudFrontだが、CloudFrontについて検討した事はおまけで取り上げたいと思う。
今回やろうとしていることの完成図
上記のような構成をとることで、多層防御(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を返す関数とする。以下のテンプレートを少しいじって作ると楽。
続いて、API Gatewayと統合する。マネージメントコンソールの「トリガーの追加」からだとAPIキーが必要になってしまったり不便なのでAPI Gatewayのメニューから作成する。
REST API を作成
からAPIを作成し、リソースを作成
からプロキシのリソースを作成する。
続いてメソッドを作成
から/{proxy+}
に対してANY
を設定し、統合タイプ
にLambdaを選択して先ほど作成したLambda関数を選択する。ここまで出来たらいったんテスト
からLambdaとうまく統合できているか?を確認しておく。
問題なければAPIをデプロイ
から適当なステージ名をつけてデプロイし、今度はcurl等で今設定したAPIを呼び出してみる。
$ 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の設定時にはクライアント証明書の指定も必要になる。
色々やることがあるが、以下の順にやっていく。
- SSLサーバ証明書をACM(AWS Certificate Manager)に登録する
- ドメイン所有のパブリック証明書の発行する
- 信頼ストアとしてmTLSで利用するクライアント証明書(公開鍵)を配置する
- API Gatewayでカスタムドメインの作成をする(mTLSは設定しない)
- 4の手順で設定したカスタムドメインの名前解決が行われるようにDNSでCNAMEレコードを設定する
- mTLSの設定を有効にする
1. SSLサーバ証明書をACM(AWS Certificate Manager)に登録する
まずはACMにカスタムドメインに対するSSLサーバ証明書が必要になる。今回はCloudflareをCDNとして利用するので、Cloudflareで発行したオリジン証明書(サーバ証明書)を利用する。
ACMの証明書をインポート
からCloudflareで発行したサーバ証明書を入力する(サーバ証明書の発行についてはCloudflareのSSL/TLSについてを参照)。インポートできると以下のように表示される。なお、Cloudflareのオリジン証明書は自前で発行したものに等しいので自動更新されないことに注意。
2. ドメイン所有のパブリック証明書の発行
パブリック証明書とは、カスタムドメインの設定の以下で指定するACMの証明書のことで、ドメインの所有者であることを証明するために必要。
ドメインの所有確認は、DNSのCNAMEを設定して行う方法が簡単なのでそれを行う。まず証明書をリクエスト
から証明書の発行を依頼する。依頼すると保留中になってDNSで設定すべきCNAMEレコードの内容が表示されるのでその通りに設定する。
お名前ドットコムでドメインは取得したが、ドメインの管理はCloudflareのネームサーバで行うように設定していたので、CloudflareのDNSメニューから以下のようにCNAMEを設定する。
うまくいくと以下のようにステータスが〇〇になる(DNSの設定を更新してから10分くらいはかかった気がする)。
※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のサーバ証明書を選択する。
どのAPIをこのカスタムドメインにマッピングするのかを設定するために、APIマッピング
から作成済みのAPIとのマッピングを作成する。
これでカスタムドメインの設定は(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レコードを設定する。
この設定をすれば以下のように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のログも確認してみると、以下のように確認できるので疎通に問題ないと判断できる。
6. mTLSの設定を有効にする
API Gatewayのカスタムドメインの設定で、mTLSを有効にしCloudflareからのリクエストのみを受け付けるように設定を変更する。カスタムドメインの設定画面のドメインの詳細
から以下のように相互TLS認証
を有効にする。
あとは更新が完了するまで待つだけだが、1点注意として、以下のようにAPIマッピングで指定しているAPIのデフォルトのエンドポイントが有効になっていると迂回される1。
そこでAPI の設定
メニューからデフォルトのエンドポイント
を無効にする設定をする。これで設定したカスタムドメイン以外でのオリジンサーバのエンドポイントにアクセスできないくなり、セキュアな状態を作れる。
カスタムドメインの設定をする際にCNAMEレコードの値に設定したAPI Gateway ドメイン名
にはアクセスできないのか?という疑問は残っている。curlで直接リクエストを送ってみると403になったので、何らかの方法ではじいているのかもしれない。
ここは調べてみたが情報は出てこずだった。
API GatewayのmTLSの設定ができたら、Cloudflareの方でも認証済み Origin Pull
を有効にする。これを有効にするとCloudflareからのリクエスト時のTLSハンドシェイクでクライアント証明書を送るようになり、mTLSができるようになる。
設定後、再度リクエストが通るか?を確認してみると以下のように問題なく通ることが確認できる。
$ 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の設定
この設定でもいくつかやるべきことがあるので、以下の順にやっていく。
- Web ACLでHTTPヘッダーのあるキーに対する値の有無で許可するか?のルールを作成する
- API GatewayでWeb ACLを適用する
- CloudflareでカスタムHTTPヘッダーを追加する
1. Web ACLでHTTPヘッダーのあるキーに対する値の有無で許可するか?のルールを作成する
AWS WAFのWeb ACLsの作成を行う。今回はHTTPヘッダーのあるキーでのルールになるので、カスタムルールを追加する。
以下のようなX-CDN-Secret-Key
というヘッダーキーに対する値が一致するか?というルールを設定する。これでこのカスタムHTTPヘッダーを持たないリクエストはCloudflare(CDN)経由のリクエストではなく、オリジンサーバへの直リクエストと判定できるようになる。
2. API GatewayでWeb ACLを適用する
API Gatewayのステージ
メニューからステージを編集で以下のように1の手順で作成したWeb ACLを設定する。
設定後、カスタムヘッダーがないリクエストを送ってみると、以下のように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のダッシュボードでも以下のようにブロックされていること・許可されていることが確認できる。
※ちなみに、WAFのログをCloudWatchで確認できるようにするには、Web ACLを作成後、Logging and metric
タブのLogging
から有効にする必要がある。
3. CloudflareでカスタムHTTPヘッダーを追加する
Transform Rulesに書かれている通り、リクエストヘッダーを書き換える(追加・変更・削除)ことができる(Cloudflareの機能としては他にも色々できる)。今回はすべてのリクエストにWeb ACLのルールで設定したカスタムヘッダーを追加するようにするので、ルール
のメニューの変換ルール
から以下のように設定する。
設定後、今度は2でブロックされていた時と同じように、ヘッダーを指定しないでリクエストを送ってみると、問題なくリクエストが成功することを確認できる。
$ curl https://restapi.<取得したドメイン>.com/api/test?query=not-set-header
hello world
WAFのチェック結果を見ても許可されていることが確認できる。
以上で今回やってみたかったことは終わりになる。
まとめとして
今回は、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の設定
メニューから以下のように設定する。
-
本来は、カスタムドメインに対してリクエストを送ってもらいCloudflareのCDN経由でAPI GatewayのエンドポイントにアクセスすることでmTLSが行われる。ただ、APIをデプロイしたときに発行されるデフォルトのエンドポイントがある状態では、万が一それがバレルとそこに直接リクエストが送られてきてしまう=CDNを迂回してオリジンサーバに直接リクエストが来る状態になってしまうので非常にまずい、という事。 ↩