はじめに
WAF の運用は大変だ、とよく言われます。誤検知対応に時間がかかる、ルールの運用が大変などという話は珍しくありません。
ただ、実際に運用していて感じる大変さは、それだけではありません。
WAF には、レート制限、DDoS 対策、IP 制限、パス単位のアクセス制御、攻撃検知ルールなど、様々な機能が詰め込まれており、その結果「誰が何を考えて設定しているのか」が分かりにくくなりがちです。
本記事では、WAF の細かい設定方法ではなく、運用が破綻しやすくなる構造を整理し、今より少しマシな運用にする方法を考えてみます。
WAF が担っている役割
WAF が担っている役割は、大きく分けると次の2つです。
1. 攻撃の検知・防御
既知の攻撃パターン、プロトコル逸脱、Bot や DoS 的な挙動の検知など。
2. 管理画面や管理用 API へのアクセス制御
管理画面は特定の IP からのみアクセス許可する、APIの入口を制限する、といった制御。
前者は、セキュリティ担当が得意とする領域です。アプリの仕様を把握していなくても、DoS 対策や、SQL インジェクションやパストラバーサルなどの一般的な攻撃に対するルールの設定は可能です。
一方、後者はアプリの画面構成や利用者の前提を理解していないと、正しく設定できません。
こうした役割は、オンプレミス環境などでモノリシックなアプリケーションを運用していた時代には、合理的でした。
管理画面もユーザー向け画面も同じ入口にあり、ネットワーク境界がそのまま信頼境界だったため、WAF は入口で何でも判断する装置として機能していました。
ただ、マイクロサービス化やフロントエンド分離が進んだ現在では、WAF が担う役割が以前より小さくなる構成も増えてきました。
一方で、すべてのシステムがその前提に移行しているわけではありません。管理画面とユーザー向け画面が同じ入口にあり、WAF が「攻撃防御」と「アクセス制御」という2つの役割を同時に担っている構成は、今も現役で使われています。
WAF 運用の責任分界の難しさ
WAF が「攻撃防御」と「アクセス制御」という2つの役割を担い、更に、同じルール体系、同じ運用フローに乗っていることで、責任分界が曖昧になり、運用負担が増大します。
このような構造のまま運用すると、次のような状態になりがちです。
- アプリ担当が WAF を運用する
WAF 特有のルールロジックや攻撃検知まで理解する必要があり、学習コストが高い。 - セキュリティ担当が WAF を運用する
アプリ仕様が分からず、アクセス制御や誤検知対応で毎回確認が必要になる。
全部できる人がいれば理想的ですが、アプリとセキュリティの両方を深く理解している人は多くありません。
WAF のルールの責務を分ける
設定リードタイムの削減のためには、アプリ担当とセキュリティ担当が得意領域に注力できると良さそうです。また、誤検知や想定外のアクセス遮断が発生した際の切り分けは、誰がどういった意図で設定したルールなのかが明確になっていれば、効率よく進められます。
そこで、WAF ルールにおけるアプリ担当とセキュリティ担当の担当分を、もう少し整理できないかと考えました。
考え方としては、WAF のルールを次の3層に分けます。

1. Pre Ruleset(アプリ依存度・低)
まず最初に、明確に危険なリクエストを検知・遮断します。
プロトコル逸脱、高確度のシグネチャなどがここにあてはまります。
ここは、アプリ文脈に依存せず、セキュリティ担当が運用できる領域となります。
2. アクセス制御ルール(完全にアプリ依存)
管理画面や API へのアクセス制限を定義します。
アプリの仕様を理解していないと決められないため、アプリ担当が主体となる領域です。
3. Post Ruleset(アプリ依存度・中)
アクセス制御は通った前提で、1よりは確度の低い攻撃、API の悪用、Bot 濫用などを検知・防御します。
ここも基本的にはセキュリティ担当が見る領域になりますが、アプリ担当と連携したチューニングが必要な領域でもあります。
既存の仕組みによる解決方法と、それでも残る課題
こうした責任分界は、全てを新しく作らなくても、既存の仕組みで部分的に実現できます。
例えば AWS の Firewall Manager を使うと、複数の WAF に対して共通ルールを一元的に適用できます。
これにより、
- 攻撃検知や基本的な防御ルールはセキュリティチームがまとめて管理する
- アプリ固有のアクセス制御は各アプリ側で個別に設定する
といった分担が可能になります。
ただし、Firewall Manager が解決するのは、WAF に共通のルールを配るところまでです。
アプリ担当が WAF 特有のルールロジックや条件の書き方、既存の攻撃検知ルールとの干渉を理解しなければならない、という根本的な課題は残ります。
アプリ依存のルール定義の負荷を下げる
責任分界はそのままに、アクセス制御ルールの定義の負荷を下げるという観点で、以下のような解決策を考えました。
1. アクセス制御条件の宣言的定義
まず、アクセス制御の条件を宣言的に記述します。
アプリ担当は、「どの条件でリクエストを通したいか」だけを宣言的に記載すればよく、WAF のルールロジックを意識する必要はありません。
2. ルールの生成
1の宣言的定義を元に、指定条件以外はアクセスをブロックするような WAF ルールを生成します。
やってみた
ここでは AWS WAF を使っていますが、考え方自体は他の WAF にも適用可能です。
※ これはあくまで PoC です。実運用までは想定していません。
まずは、特定 URL へのアクセスを、どの IP セットからだけ許可するか、を定義します。
scope: REGIONAL
ruleGroupName: app-accesscontrol
ipSets:
office:
description: 自社オフィス(東京・大阪)
addresses:
- 203.0.113.0/24 # 東京オフィス
- 198.51.100.10/32 # 大阪オフィス
vpn:
description: 自社リモートワーク用VPN
addresses:
- 192.0.2.0/28
endpoints:
admin:
description: WordPress管理画面
paths:
- "^/wp-admin/.*"
apis:
description: 管理用API
paths:
- "^/api/.*"
rules:
- name: admin-from-office
paths:
- admin
ipSets:
- office
- name: api-from-office-and-vpn
paths:
- apis
ipSets:
- office
- vpn
続いて、これを AWS WAF ルールグループ定義に変換するスクリプトを用意します。Python で作成しましたが、ここに掲載するには長くなってしまったので割愛します。
生成されたルールグループは以下のようになっています。
AWSTemplateFormatVersion: '2010-09-09'
Description: Generated WAFv2 RuleGroup from declarative access-control spec
Resources:
IPSetOffice:
Type: AWS::WAFv2::IPSet
Properties:
Name: app-accesscontrol-office
Scope: REGIONAL
IPAddressVersion: IPV4
Addresses:
- 203.0.113.0/24
- 198.51.100.10/32
IPSetVpn:
Type: AWS::WAFv2::IPSet
Properties:
Name: app-accesscontrol-vpn
Scope: REGIONAL
IPAddressVersion: IPV4
Addresses:
- 192.0.2.0/28
RuleGroupAppAccesscontrol:
Type: AWS::WAFv2::RuleGroup
Properties:
Name: app-accesscontrol
Scope: REGIONAL
Capacity: 65
Rules:
- Name: admin-from-office-block
Priority: 0
Statement:
AndStatement:
Statements:
- NotStatement:
Statement:
IPSetReferenceStatement:
Arn:
Fn::GetAtt:
- IPSetOffice
- Arn
- RegexMatchStatement:
RegexString: ^/wp-admin/.*
FieldToMatch:
UriPath: {}
TextTransformations:
- Priority: 0
Type: NONE
Action:
Block: {}
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: app-accesscontrol-admin-from-office-block
- Name: api-from-office-and-vpn-block
Priority: 1
Statement:
AndStatement:
Statements:
- NotStatement:
Statement:
OrStatement:
Statements:
- IPSetReferenceStatement:
Arn:
Fn::GetAtt:
- IPSetOffice
- Arn
- IPSetReferenceStatement:
Arn:
Fn::GetAtt:
- IPSetVpn
- Arn
- RegexMatchStatement:
RegexString: ^/api/.*
FieldToMatch:
UriPath: {}
TextTransformations:
- Priority: 0
Type: NONE
Action:
Block: {}
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: app-accesscontrol-api-from-office-and-vpn-block
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: app-accesscontrol
Outputs:
RuleGroupArn:
Value:
Fn::GetAtt:
- RuleGroupAppAccesscontrol
- Arn
/wp-admin は office からのアクセスだけ許可し、それ以外はブロック。/api へのアクセスは office と vpn からのみ許可し、それ以外はブロック、というルールになっています。
条件の数の上限などは、実際に使用する WAF に依存することになりますが、WAF の仕様を深く理解していなくても、アクセス制御の意図だけを定義できる形になりました。
完全分離はできない
WAF の設定をアプリ担当とセキュリティ担当で分けたとしても、誤検知対応などでアプリ仕様を理解する必要がなくなるわけではありません。
オフィスに出入りする人が分からない状態で警備を任されても、一般的な不審者対策しかできないのと同じです。
この点については、アプリ側の協力が不可欠です。
運用で死なないための現実的な妥協
WAF だけで全てを守るのは無理があります。また、Attack Surface を減らすことは重要ですが、チューニングしすぎないという判断も、運用上は合理的です。
例えば、
- なぜこの除外設定をしたのか
- いつまで必要なのか
を管理するコスト自体が、運用負債になります。
構成や運用体制などの前提に応じて、「どこまで WAF に任せるか」を関係者間で合意しておくことが重要です。
おわりに
ここまで、WAF の運用が破綻しやすい構造をどう整理するかという観点で考えてきました。
完璧な分離はできませんし、構成や組織によっても最適解は変わります。それでも、今より少しマシな責任分界を意識することで、運用が楽になるように改善を進められればと思います。