SCSKの畑です。
2個前のエントリで少し言及した通り、外部公開されるサービスの AWS WAF 設定でハマった話について記載します。
アーキテクチャ図
いつものやつです。今回は AWS WAF 設定に関連する内容ということで、対象は以下構成において外部からアクセス可能なサービスとなる、Amazon CloudFront、Amazon Cognito、AWS AppSync の3つです。
背景
さて、その2個前のエントリにおいて、お客さんの環境ポリシーにより CloudFront に AWS WAF を設定する必要があったことを取り上げました。本アプリケーションはインターネット上に公開せず、アクセスできるネットワーク/IP アドレスのレンジを限定する必要があったためですが、アーキテクチャ図の通り Cognito ユーザプール及び AppSync の GraphQL API エンドポイントについても同様にクライアント端末からアクセスすることになるため、AWS WAF を設定する必要がありました。
それらのサービスについても外部からアクセスしてくるのは同じようにクライアント端末の属するネットワークであるため、CloudFront と同じような AWS WAF 設定をすれば良いと考え、お客さんにその旨依頼していたのですが・・
エラー発生(AppSync)
お客さんの AWS 環境にアプリケーション一式を移行して動作確認を行っていたところ、バックエンド用 Lambda 処理を伴うクライアント処理時に以下のようなエラーが出力されました。(汎用エラー表示用コンポーネント上での扱いから JSON 形式です)
{ message: 'Connection failed: {"errors":[{"errorType":"WAFForbiddenException","message":"403 Forbidden" }
クライアント端末上のアプリケーションから Lambda を介さない AppSync API は実行できていたため当初はちょっと戸惑ったものの、errorType の通り WAF でアクセス拒否されていることは分かったので、先述の通りクライアント端末以外からのアクセスを許可していない WAF 設定が怪しいことにすぐ思い至りました。確かに Lambda も クライアント端末上のアプリケーションも、AppSync の GraphQL API エンドポイントにアクセスする立場は同じなので、バックエンド用 Lambda についてもクライアント端末と同様に AWS WAF でのアクセス許可が必要なのではないか?という(その時点での)推測です。
バックエンド用 Lambda がエラーで正常動作しない以上、アプリケーションの移行や動作確認も進まないことと同義であるため、いの一番に対応しました。解決策については Cognito 側と全く同じなので後述します。
エラー発生(Cognito ユーザプール)
一方、Cognito ユーザプールについては、バックエンド用 Lambda から boto3 経由でアクセスする処理が正常に動作していたこと、アプリケーションの一連の動作確認でもユーザ認証部分は特に問題がなかったことの2点から、当初はあまり深く理由を考えないまま大丈夫なのかな?と考えていたのですが・・
たまたま違う日に Web ブラウザ上で開いているアプリケーション(ログイン中)をリロードしたところ、TOP ページにリダイレクトしてしまう事象が発生しました。弊社の AWS 環境では事象が再現しなかったためお客さんの AWS 環境の問題と考え、当初は CloudFront や AWS WAF の設定あたりに問題があると考えたのですが、特に原因となるような設定はありませんでした。また、ブラウザのコンソール上にも特にエラーメッセージは表示されていませんでした。
そこで改めて TOP ページへのリダイレクト履歴を Web ブラウザから追いかけたところ
(リロードした時点のページ) ⇒ /login(ログイン後の初期処理ページ) ⇒ /(TOPページ)
という順番でリダイレクトしていることが分かりました。アプリケーション側のルーティングロジックの詳細は過去のエントリで記載していますが、簡単に整理すると
- /login へのリダイレクト: Cognito ユーザ未認証の場合のリダイレクトパターン
- / へのリダイレクト: Cognito ユーザ認証済みのリダイレクトパターン
となります。
つまり、上記のルーティングロジックに従うと、リロードした時点では Cognito ユーザ未認証と判定されているものの、Cognito ユーザ認証自体は完了しているため / へリダイレクトされる時点では Cognito ユーザ認証済みと判定されていることになります。Nuxt3 によるルーティングはサーバ側でも処理されるため、フロントエンド側の Lambda で Cognito ユーザ未認証とみなされているのではないかと推測しました。そこで、アプリケーションをデプロイした CloudFront 配下のフロントエンド用 Lambda のログを CloudWatch から見てみると・・
2025-02-18T06:41:20.957Z 9dd22dc5-039f-45ee-86f6-b3ce9230ec0a INFO ForbiddenException: Request not allowed due to WAF block. at file:///var/task/.output/server/node_modules/@aws-amplify/auth/dist/esm/foundation/factories/serviceClients/cognitoIdentityProvider/shared/serde/createUserPoolDeserializer.mjs:11:15 at process.processTicksAndRejections (node:internal/process/task_queues:105:5) at async fetchUserAttributes (file:///var/task/.output/server/node_modules/@aws-amplify/auth/dist/esm/providers/cognito/apis/internal/fetchUserAttributes.mjs:31:32) at async runWithAmplifyServerContext (file:///var/task/.output/server/node_modules/aws-amplify/dist/esm/adapter-core/runWithAmplifyServerContext.mjs:20:24) at async useUserInfo (file:///var/task/.output/server/chunks/build/server.mjs:7155:26) at async file:///var/task/.output/server/chunks/build/server.mjs:7526:113 at async Object.callAsync (file:///var/task/.output/server/chunks/nitro/nitro.mjs:5850:16) at async file:///var/task/.output/server/chunks/build/server.mjs:7733:26 { underlyingError: undefined, recoverySuggestion: undefined, constructor: [class AuthError extends AmplifyError] }
1行目にそのものズバリなメッセージ「ForbiddenException: Request not allowed due to WAF block.」が出ている通り、フロントエンド側の Lambda から Cognito ユーザプールへのアクセスが WAF によりブロックされていたことが分かりました。過去のエントリに記載している middleware のコード通り、Cognito ユーザ認証されない場合も今回のように WAF でブロックされた場合も同じように例外を catch して /login にリダイレクトしているので、このような挙動になったということになります。
なお、Cognito ユーザの認証処理自体が正常に実施されていた理由もシンプルで、クライアント側の処理だったためです。そもそも、URL を叩いて表示されたログイン画面にユーザ/パスワードを入力してログインしようとしている時点で、クライアント側の処理であることは自明ですね。。また、実装の都合上他の AppSync API を叩くような処理(=Cognito ユーザ認証が必要な処理)は基本的にほぼクライアント側で明示的に実行するようにしているため、本事象の影響は出ていませんでした。このあたりは Nuxt3 で Universal Rendering (Server-Side Rendering) を使用しているが故の内容と思います。
ということで、こちらも後述する解決策により対応したのですが・・納得できない部分が残りました。
エラー内容の深掘(AWS サポート問い合わせ)
というのも、先般の通りバックエンドの Lambda 関数から boto3 経由で Cognito ユーザプールにアクセスする処理は元々問題なく動作していたためです。AppSync については、バックエンドの Lambda 関数からも実質的にアプリケーションからのアクセスと同じように GraphQL API を叩くような実装となっており、boto3 は使用していなかった(使えなかった)ため同じように AWS WAF にブロックされることも納得できたのですが、では boto3 経由のアクセスが AWS WAF にブロックされないのは何故なのか?
ということで、この点を AWS サポートに問い合わせてみたところ、実に明快な回答を頂きました。というかドキュメントをもうちょっと読んでから問い合わせれば良かったと思ったくらいには明確に書いてあったので、少なからず申し訳ない気分になりましたが・・
Cognito ユーザプールの場合
上記 URL に記載の通り、正に今回の事象を端的に説明できる内容でした。
- AWS 認証情報による認証を不要とするパブリックなユーザープール API へのリクエストに対しては適用される
- クライアント端末やフロントエンド用 Lambda で Cognito 認証情報を扱うような処理に該当
- AWS 認証情報による認証が必要なユーザープール API へのリクエストに対しては適用されない
- バックエンド用 Lambda から boto3 を叩くような処理に該当
確かに理屈というか考え方としても納得できるところで、AWS 認証情報による認証が必要な API については認証情報の有無がそのままアクセス制限となりますし、アプリケーションのログイン処理においてはその時点で AWS 認証情報は持たない(ログイン後に Cognito 経由で付与される)以上 API としてはパブリックとすべきであり、その上でアクセス制限をしたい場合の機能として WAF が適用できるというように理解できました。
また、先述したような「boto3 経由のアクセスについて許可されている」という表現ももちろん間違っており、バックエンド用の Lambda において boto3 経由で実行していたのが AWS 認証情報による認証が必要なユーザープール API へのリクエストのみであったため、結果的にそのような挙動になっていたというのが実態でした。これまでのエントリやアーキテクチャ図に記載の通り、バックエンド用 Lambda は AppSync 経由で実行される想定のため、Lambda 内で Cognito の認証情報を直接扱う必要がないことから必然的にそのような実装になったとも言えるかもしれません。
AppSync の場合
こちらも上記 URL に記載がありましたが、Cognito ユーザプールの場合とは少し考え方が異なるようでした。
- AppSync にて作成された GraphQL API に WAF が適用されている場合、同 API へのリクエストに対して一律で WAF が適用される
- 2つ目 の URL に記載されている API へのリクエストについては WAF は適用されない
考え方としてはシンプルで、GraphQL を含む AppSync API 自体の管理操作自体には WAF が適用されず、作成した AppSync API を叩くような操作については WAF が適用されるというように理解できました。逆に言うと、バックエンド用 Lambda の実行ロールに appsync:GraphQL が付与され対象の GraphQL API を実行できる権限があっても、Cognito の場合と異なり GraphQL API へのアクセスである以上 WAF が適用されるため、先述した通りの挙動となったということですね。作成した GraphQL API のエンドポイント URL が明示的に払い出されることも踏まえ、直感的な仕様だと感じました。
解決策
ということで、Lambda 関数から AppSync/Cognito の API エンドポイントを叩きに行く際の IP アドレスについても許可するよう、AppSync 及び Cognito ユーザプールの AWS WAF 設定をお客さんに修正頂いてクローズと相成りました・・が、お気づきの方もいると思いますが、この解決策が取れるかどうかは Lambda 関数がどこに構成されているかに依存します。デフォルトだと VPC 外(AWS が管理しているネットワーク上)に構成される以上 IP アドレスは不定になると思われ、WAF で指定できない可能性がありました。
幸いアーキテクチャ図の通り、お客さん AWS 環境において Lambda 関数はフロントエンド/バックエンド用どちらも VPC 内のプライベートサブネット上に構成されていたため、Lambda から AppSync/Cognito の API エンドポイントを叩きに行く際のソース IP アドレスとなる、NAT Gateway のパブリック IP アドレスを指定頂き、無事に解決できました。
なお、本アーキテクチャにおいて AppSync や Cognito ユーザプールの API を叩くためにはどちらも認証が必要であるため、エンドポイント自体がインターネットに公開されていても大きな問題ではなかったとは思います。ただ、お客さんの環境ポリシーに適合しなくなることも確かなので、その場合の調整コストなどを鑑みると今回のような構成を取れて良かったというのが正直なところです。
まとめ
AppSync についてはアプリケーションの動作に直接的な影響があったこともあってすぐに対応できたのですが、そのタイミングで Cognito についてももう少し深掘りするべきだったというのが反省点です。元々の AWS WAF 導入時の観点が外部アクセスの制限にあったことが多少のエクスキューズにはなるかもしれませんが、構成上同じような事象が発生し得るということまでは気付いていただけに。。
また、AWS WAF を適用できる他のサービスについても、今回のようにリクエストの内容などにより適用される範囲が変わりそうだなと感じたので、もし次に触る機会があれば留意しておきたいと感じました。
本記事がどなたかの役に立てば幸いです。
余談ですが、この原因の切り分けもといデバッグにはそこそこ時間を要しました。フロントエンド側 Lambda の CloudWatchLog を見なければいけないというところに思考が至るまでに時間がかかってしまったのが原因です。
普段 Nuxt3 の開発をローカルで実施している時はフレームワーク側が気を利かせてくれて、ローカルで立てている開発用サーバ側のログも Web ブラウザのコンソールで出してくれたりするんですよね。しばらくその感覚でいたので、本来 /login にリダイレクトした際に上記のようなエラーが出ると思っていたのが出なかったこともあり、このロジック以外でリダイレクトしてしまうようなことがあり得るのか?みたいなところに思考が向いてしまっていました。その結果、CloudFront なり AWS WAF の設定あたりを疑うところから始めてしまったという・・