
WAFルールを「守る対象」から決める ― 保護プロファイルの設計と実装
はじめに
こんにちは、セキュリティサービスを開発しているクラスメソッドセキュリティのよつやんです。
突然ですが、WAF のルールって、どうやって選んでいますか?
「SQL インジェクションを止めたいからこのルール」「XSS も怖いからこれも」というふうに、止めたい攻撃から選ぶことが多いと思います。でも、よく考えると本当に大事なのは「自分たちは何を守っているのか」のほうなんですよね。
そこで私たちは、AWS WAF の運用を自動化する仕組みの中に、「保護プロファイル」という機能を作りました。WAF の後ろで動いているソフトウェアの構成を集めて、それに合わせてルールを組み立てる機能です。
この記事では、次の 3 つをさくっと紹介します。
- そもそも、なぜ保護プロファイルが必要なのか
- Amazon Inspector を使ってどう実現したのか
- 開発中にどこでハマったのか
Inspector と WAF を組み合わせてみたい方や、WAF のルール選びに悩んでいる方の参考になればうれしいです。
なぜ保護プロファイルが必要なのか
汎用のルールセットだけだと、守る対象にぴったり合ったルールにはなかなかなりません。理由は大きく 4 つあります。
1. 関係ないルールは多いのに、肝心のルールが足りない
AWS マネージドルールのような汎用ルールは、いろんな攻撃を「そこそこ」止められるように作られています。なので、自分のサイトには関係のないアプリ向けの検査がかかります。
逆に、特定の製品の脆弱性を突く攻撃はすり抜けることがあります。たとえば Next.js のミドルウェア認可回避(CVE-2025-29927)。特定のヘッダーを 1 つ付けるだけで、見た目はごく普通のリクエストです。これは汎用ルールでは止まりません。
2. WAF の容量には限りがある
AWS WAF の Web ACL には WCU(Web ACL Capacity Units)という容量の上限があります。思いつくルールを全部載せる、というわけにはいきません。何を優先して載せるかを決めるには、守る対象のことを知る必要があります。何を守るのかはセキュリティの基本です。
3. 関係ないルールは誤検知のもとになるだけ
使っていない製品向けのルールは、攻撃を止める役には立ちません。残るのは「正常なリクエストを間違って止めてしまうかも」というリスクだけです。また運用しているサイトには関係のない攻撃でも、WAFだけ見るとこれに対応しないといけないのか、判断できないことがあります。スキャナーによる大量のリクエストで重大イベントが出力されると運用は回らないですよね。
4. パッチを当てるまでのすき間を埋めたい
脆弱性が公開されてから修正版を当てるまでには、検証や調整でどうしても時間がかかります。そのすき間を WAF で一時的に守るのが、いわゆる「仮想パッチ」です。ただ、どの製品のどのバージョンが動いているのか分からないと、どのルールを当てればいいのかも決められません。
「じゃあ利用者に構成を書いてもらえば?」とも考えました。実際、最初の実装ではバージョンを手で入力してもらっていました。ところが、アップデートのたびに情報は古くなりますし、バージョンの書き方も人によってバラバラです。結局この方式はやめました。また出力されているログで手掛かりをつかもうともしてました。
構成は人に聞かずに、実際の環境から取ってくる。 これが出発点になりました。
保護プロファイルの考え方
保護プロファイルは、Web ACL ごとに「その後ろで何が動いているか」をまとめたものです。中身はこんな感じです。
| 項目 | 例 | どこから取るか |
|---|---|---|
| スタック(製品・フレームワーク) | WordPress、Next.js、Django、nginx | パッケージ名から判定。利用者が選ぶこともできる |
| パッケージとバージョン | next 14.1.0、log4j-core 2.14.1 | Amazon Inspector(検出結果と SBOM) |
| 未修正の CVE | CVE-2025-29927 | Amazon Inspector の検出結果 |
このプロファイルを見て、3 種類のルールを選びます。
- 予防ルール:バージョンに関係なく役に立つルールです。設定ファイルの丸見え、公開しちゃいけないデバッグ画面や管理画面へのアクセス、普通のリクエストにはまず出てこない攻撃構文などをチェックします。スタックに合わせて選び、どの環境にも共通するものはすべてに当てます。
- 脆弱性ルール:特定の CVE 専用のルールです。「その CVE がまだ直っていない」「脆弱なバージョンのパッケージが入っている」と Inspector が教えてくれたときだけ当てます。スタックが合うだけでは当てません。
- 緊急ルール:新しく見つかった脆弱性に対して、あとから配るルールです。対象(CVE、パッケージのバージョン範囲、スタック)を持っていて、プロファイルが合う環境にだけ届きます。
いまのカタログには、予防ルールが 51 本、脆弱性ルールが 13 本あります。対応しているスタックは 49 種類です(Laravel、Rails、ASP.NET、Next.js、Nuxt、Django、Jenkins、GitLab、Movable Type、IIS など)。ちなみに「どのスタックにも予防ルールが最低 1 本ある」ことは、テストでチェックしています。
ルールは、まず Count(検知だけ)で入れます。誤検知がないのを確かめてから Block に切り替える、という流れです。利用者がルールごとに選んだ設定は、プロファイルが更新されても勝手に上書きしません。

ルールとポリシー:Inspector が見つけた CVE に合わせて脆弱性ルールが選ばれ、ルールごとに Block / Count を選べます
もう 1 つ、Inspector が見つけた CVE ごとに「WAF でどれくらい守れているか」を 4 つに分けて見せています。
- Block:その CVE を止めるルールが Block で動いている
- Count:ルールはあるけど、まだ Count で様子見中
- WAF 対象外:カーネルなど、そもそも WAF では守れない脆弱性
- 未対応:WAF で守れるはずなのに、ルールがない
「未対応」が残っていたら、パッチを急ぐか、ルールを足すかを考えます。

概要:CVE ごとの防御状況を 4 つに分けて表示します
Amazon Inspector でどう実現したか
全体の流れはこんな感じです。
1. Web ACL から後ろのリソースをたどる
Inspector は、EC2 インスタンスやコンテナイメージ、Lambda 関数の単位で脆弱性を教えてくれます。一方の WAF は Web ACL 単位で動きます。まずはこの 2 つをつなぐところから始めます。
- Web ACL が ALB に付いていれば、その ALB のターゲットグループからターゲットを拾います。
- CloudFront に付いていれば、オリジンをたどって ALB を見つけ、そこからターゲットを拾います。
- ターゲットは EC2、ECS タスク、Lambda のどれかです。ECS の場合は、動いているタスクからコンテナイメージを特定します。
2. 検出結果を取ってくる
見つけたリソースについて、Inspector の検出結果からパッケージの脆弱性で、まだ ACTIVE なものを取ってきます。これで「直っていない CVE」と「脆弱なパッケージのバージョン」が分かります。

脆弱性:CVE ごとに、影響を受けるパッケージとリソース、対応するルールがあるかどうかを表示します
3. SBOM で「脆弱性のないパッケージ」も知る
ここで 1 つ問題があります。検出結果に出てくるのは、脆弱性のあるパッケージだけなんです。たとえば nginx に未修正の CVE がなければ、検出結果からは「nginx が動いている」ことすら分かりません。スタックを判定したいのに、これは困ります。
そこで、Inspector の SBOM エクスポート(CycloneDX 形式)を使って、全パッケージを取り込むことにしました。
- 1 日 1 回
CreateSbomExportを呼んで、利用者のアカウントの S3 バケットに書き出してもらいます。 - 15 分ごとに、書き出し終わったファイルを取り込みます。古いデータは世代ごと入れ替えます。
- 6 時間たっても終わらない書き出しは、あきらめて打ち切ります。
検証環境では、検出結果だけで分かったスタックは django、nextjs、nodejs、python の 4 つでした。SBOM を足したら nginx も見えるようになり、パッケージは全部で 763 個になりました。

対象範囲:Web ACL の後ろでたどれたリソースと、SBOM から判定した技術スタック
4. パッケージからスタックを判定して、ルールを選ぶ
パッケージ名とスタックの対応表を使って、スタックを判定します。あとは、スタックに合う予防ルールと、検出結果の CVE に合う脆弱性ルールを選ぶだけです。緊急ルールは、対象の CVE やバージョン範囲がプロファイルと合うときだけ選びます。
5. 容量に収めて配備する
選んだルールは、もともとあるルールと同じ Web ACL の容量を分け合います。足りないときは、優先度の低いものから外します。優先度は「緊急 → 脆弱性 → 予防 → 共通の予防」の順です。外したルールは「容量不足で未適用」として画面に出します。知らないうちに消えている、ということがないようにしたかったからです。
6. 権限は読み取りだけ
利用者のアカウントには、Inspector、ELB、CloudFront、ECS を読むだけの権限を持ったロールを作ってもらいます。権限が足りない手順があっても、全体をエラーにはしません。「どこで何が足りなかったか」を記録して、取れた範囲でプロファイルを作ります。
プロファイルは毎日 1 回更新されます。パッチを当てて CVE が消えれば脆弱性ルールも外れますし、新しい CVE が出てくればルールが増えます。
開発でハマったところ
ここからは、開発中につまずいたポイントを正直に書いていきます。
CloudFront のオリジンが独自ドメインだと、後ろがたどれない
最初は、オリジンのドメインが *.elb.amazonaws.com のときだけ ALB をたどっていました。ところが実際には、origin.example.com みたいな独自ドメインを ALB に向けている構成がすごく多いんです。そうなるとプロファイルが空っぽ、という悲しい状態になっていました。
そこで、次の 3 パターンでたどれるようにしました。
- CNAME:DNS を引いて、正規名が ALB の名前と合うかを見ます。
- Route 53 のエイリアス:これがやっかいで、エイリアスは CNAME を返さず IP アドレスしか返してくれません。そこで
ip-ranges.jsonから IP アドレスのリージョンを割り出し、そのリージョンの ALB の IP アドレスと突き合わせることにしました。 - VPC オリジン:
cloudfront:GetVpcOriginを使えば、ALB や EC2 インスタンスの ARN を直接取れます。
なお、S3 や API Gateway みたいな AWS のエンドポイントは調べません。名前解決するオリジンも 1 回あたり 20 件までにしています。
SBOM は同じリージョンのバケットにしか書き出せない
最初は「東京リージョンのバケット 1 つに全リージョンの SBOM を集めればいいや」と思っていました。ところが、バージニア北部のリソースを書き出そうとしたら、S3 の AuthorizationHeaderMalformed で失敗。調べてみると、Inspector は書き出し元と同じリージョンのバケットにしか書けないんですね。
結局、リージョンごとにバケットと KMS キーを作る形に変えました。リージョン用の小さな CloudFormation テンプレートを用意して、バケットがないリージョンはエラーにせず「未対応リージョン」として表示しています。
さらに、実際の書き出しファイルは想定と形が違っていました。metadata.component にはリソースではなく OS 名(AMAZON_LINUX_2023)が入っていて、リソースの ID は metadata.properties のほうにありました。テスト用のサンプルも、本物の形に合わせて作り直しています。
Inspector がパッケージを見つけてくれない
検証用の EC2 にわざと古いパッケージを入れたのに、検出がずっと 0 件。これにはかなり悩まされました。原因は 2 つありました。
- 最初のスキャンが、ユーザーデータでのインストールが終わる前に走っていました。次のスキャンまで数時間待つはめに。
- EC2 の言語パッケージのスキャンは、標準では決まったパス(
/usr/lib/python3.9や/usr/lib/node_modulesなど)しか見ていませんでした。独自のディレクトリに置いたパッケージは見てくれません(スキャンするパスは設定で追加できます)。
パッケージを標準のパスに置き直してスキャンし直したら、数分で 41 件が出てきました。Inspector は「入れればすぐ全部見える」ものではない、というのは覚えておくといいと思います。
OS のパッケージはバージョン範囲で比べられない
緊急ルールの対象をバージョン範囲で指定するとき、npm や PyPI のような言語パッケージなら、SBOM のバージョンと比べれば判定できます。
ところが、deb や rpm のような OS のパッケージは、修正がバックポートされることがあります。バージョン番号は古く見えても、実はもう直っている、ということがあるんです。なので OS のパッケージは範囲では比べず、Inspector が報告する CVE で判定することにしました。
画面が「未対応」だらけに
リリース後に防御状況を見てみたら、「未対応」がずらっと並んでいて焦りました。中身を見ると、sssd、ssm-agent、sqlite、perl のモジュールなど、OS のパッケージの CVE ばかり。これらは攻撃元区分が「ネットワーク」(AV:N)と採点されているので、未対応に入ってしまっていたんです。でも、こういうものは WAF を前に置いて守るものではなく、OS のアップデートで直すものですよね。
そこで「WAF の対象になるのは、そのパッケージがアプリの一部のときだけ」と決めました。具体的には、言語パッケージか、httpd、nginx、php、tomcat、nodejs のような Web を配信・実行するパッケージです。それ以外は「WAF 対象外」に分けています。
数字として正しくても、見た人が「で、何をすればいいの?」となる画面では意味がない。これはいい教訓になりました。
WCU の見積もりでやらかす
容量の計算でもやらかしました。利用者が作ったルールの WCU を 1 本ずつ足していたら、23 本で 440 WCU という大きな見積もりに。その結果、プロファイルのルールが全部「容量不足」で外れる、という事態になりました。これはもちろん検証環境での話です。
実際の WCU は、ルールをまとめて見積もるとずっと小さくなります。なので、ルール全体を 1 つのセットとして CheckCapacity で見積もるように直しました。
それから、容量が足りないときに外すルールの本数を一度に決めていたせいで、必要以上に外していました。今は 1 本外すたびに見積もり直しています。
誤検知を出さないための工夫
プロファイルのルールは、利用者が意識しないうちに追加されていきます。だからこそ、誤検知は本当に避けたいところです。
- カタログの全ルールについて、「攻撃のサンプルには当たる」「普通のリクエストには当たらない」ことをシミュレーターで確かめています。シミュレーターは AWS WAF の変換処理をまねて作りました。実はこの作業中に、パスの正規化(
NORMALIZE_PATH)の扱いを間違えていたルールが見つかっています。 - パッケージ名からスタックを判定するとき、
next、vue、railsのような短い名前は完全一致にしています。部分一致にすると、nextcloudが Next.js 扱いされてしまうからです。 - 入れるときはまず Count から始めて、Block への切り替えは段階的に行います。
まとめ
保護プロファイルは、WAF のルールを「止めたい攻撃」ではなく「守っているもの」から選ぶための仕組みです。ポイントをおさらいすると、こんな感じです。
- 構成は人に聞かず、Amazon Inspector の検出結果と SBOM から取ってくる
- ルールは「予防」「脆弱性」「緊急」の 3 つに分ける
- 容量は優先度で配る。外したものや WAF では守れないものも、隠さず画面に出す
作ってみて意外だったのは、一番時間がかかったのがルールそのものではなかったことです。手間がかかったのは、Web ACL と Inspector の間をつなぐ部分でした。CloudFront のオリジンの解決、リージョンごとの SBOM、Inspector のスキャン範囲などです。Inspector も WAF もマネージドサービスですが、2 つを「同じ環境の話」として結び付けるには、思った以上に地道な作業が必要でした。
WAF はパッチの代わりにはなりません。それでも、何が動いているかを分かったうえで置く WAF なら、パッチを当てるまでの時間を安心して稼げる。そんなふうに考えています。最近話題になった PHP の脆弱性(PHP2Shell、CVE-2026-109618 / CVE-2026-109619)では、皆さんも同じことを感じたのではないでしょうか。








