AWS WAFマネージドルールを全部有効化して本番が死んだ話と1年間の反省
「マネージドルールを有効化すれば終わり」と思っていたら本番ログインAPIが落ちた。SOC2対応をきっかけにWAF v2と1年間格闘して見えた誤検知・コスト・チューニングの現実をまとめます。
マネージドルールを「とりあえず全部有効化」して大惨事になった話
去年の春、うちのサービスでAWS WAF v2のマネージドルールをフル活用しようという話になった。きっかけはSOC2の継続審査対応で、WAFの設定が「Countモードのまま放置」になっていたのを指摘されたからだ。SOC2審査でCloudTrail・Config設計が半年泥沼になった実録でも触れたような状況で、今度はWAFを本気でブロックモードに切り替えようとしたわけだ。
正直最初は「マネージドルールってAWSが管理してくれるし、有効化すれば終わりでしょ」くらいに思っていた。これが甘かった。
AWSManagedRulesCommonRuleSetとAWSManagedRulesSQLiRuleSetを一気にブロックモードに変更したその夜、本番ログインAPIが突然503を返し始めた。調べてみるとB2Bの顧客が使っているSFTPゲートウェイからのリクエストがSizeRestrictions_BODYルールに引っかかっていた。本文が10MBを超えるファイルアップロードが業務フローに普通に組み込まれていたのに、誰も把握していなかったのだ。
以来1年、マネージドルールと真剣に向き合い続けてきた。その知見をこれからまとめていく。
2026年時点のマネージドルールグループ全体像
2026年7月現在、AWS WAF v2で使えるAWS管理ルールグループはかなり充実している。主要なものを整理するとこうなる。
| ルールグループ名 | カテゴリ | WCU | 主な用途 |
|---|---|---|---|
| AWSManagedRulesCommonRuleSet | 汎用 | 700 | OWASP基本対策、SQLi/XSSを含む |
| AWSManagedRulesSQLiRuleSet | SQLi | 200 | SQL Injectionに特化 |
| AWSManagedRulesKnownBadInputsRuleSet | 既知悪意入力 | 200 | Log4Shell、Spring4Shell等 |
| AWSManagedRulesLinuxRuleSet | OS | 200 | Linuxコマンドインジェクション |
| AWSManagedRulesUnixRuleSet | OS | 100 | POSIX/Unixパス操作 |
| AWSManagedRulesPHPRuleSet | 言語 | 100 | PHP固有の脆弱性 |
| AWSManagedRulesWordPressRuleSet | CMS | 100 | WordPress固有の攻撃 |
| AWSManagedRulesAmazonIpReputationList | IP評判 | 25 | 既知の悪意あるIP |
| AWSManagedRulesBotControlRuleSet | Bot対策 | 50〜(Targeted: +50) | ボット検出・制御 |
| AWSManagedRulesATPRuleSet | ATO | 50〜 | Account Takeover Protection |
| AWSManagedRulesFraudControlACFP | 詐欺対策 | 50〜 | アカウント不正作成防止 |
2025年後半から2026年にかけてBot Controlルールグループの「Targetedモード」が強化されて、ブラウザフィンガープリントベースの検出精度が上がっている。ただ個人的には、料金が上がる割にFalse Positiveのリスクも増えるので今のところTargetedモードはCountのままにして様子見している。「精度が上がる=副作用も増える」という関係はどのルールグループでもだいたい成り立つ。
WCU(WAF Capacity Units)の実態
WCUはルールグループのリソース消費量を表す指標で、1つのWebACLあたり上限5,000WCU(デフォルト)。よくある落とし穴として、全部入れようとすると余裕でオーバーすることがある。うちは最初にCommonRuleSet(700) + SQLi(200) + KnownBadInputs(200) + Linux(200) + IpReputationList(25) + BotControl(50) = 1,375WCUで収めた。
構成全体像と実際の設計
うちのサービスの構成を図で示すとこんな感じだ。CloudFront + ALBの二段構えで、WAFはCloudFront側とALB側の両方にアタッチしている。
graph TB
subgraph Internet["インターネット"]
User[ユーザー]
Attacker[攻撃者]
end
subgraph AWS_Global["AWS グローバルエッジ"]
CF[CloudFront]
WAF_CF["AWS WAF v2\n(CloudFront用)\nus-east-1"]
end
subgraph VPC["VPC: 10.0.0.0/16"]
subgraph Public_Subnet["パブリックサブネット"]
ALB[Application Load Balancer]
WAF_ALB["AWS WAF v2\n(ALB用)\nap-northeast-1"]
end
subgraph Private_Subnet["プライベートサブネット"]
ECS["ECS Fargate\nAPIサービス"]
RDS[("Aurora PostgreSQL")]
end
end
subgraph Security_Services["セキュリティ関連サービス"]
Kinesis["Kinesis Data Streams"]
S3_Log["S3\nWAFログバケット"]
CW["CloudWatch Logs\n+ Metrics"]
Shield["AWS Shield\nAdvanced"]
end
User --> CF
Attacker --> CF
CF --> WAF_CF
WAF_CF --> ALB
ALB --> WAF_ALB
WAF_ALB --> ECS
ECS --> RDS
WAF_CF --> Kinesis
WAF_ALB --> Kinesis
Kinesis --> S3_Log
Kinesis --> CW
Shield --> CF
Shield --> ALB
CloudFront側のWAFはus-east-1に作る必要があって、これが地味に管理がめんどくさい。Terraformで管理するとき、providerを分けないといけなくて最初ハマった(Terraformで3年分の失敗から学んだ、State管理とAI検証の正解に似たような話が書いてある)。
誤検知(False Positive)との付き合い方が9割
これが一番大事なポイントで、マネージドルールを使う上で避けられない問題が誤検知だ。WAF・DDoS対策が本番で失敗した話|AWS WAF v2の地雷と3年の運用知見でも似た話が出ているように、WAFは「入れれば安全」ではない。誤検知を握りつぶしたまま運用すると、気づかないうちに正規ユーザーを弾き続けるという最悪の事態になる。
段階的な有効化フローが必須
うちが実践している手順はこれだ:
flowchart TD
A[ルールグループ追加] --> B[Countモードで1週間運用]
B --> C{False Positiveのログを分析}
C --> |問題なし| D[Blockモードに変更]
C --> |誤検知あり| E[除外ルール / カスタムルールで対応]
E --> F[CountモードでE後1週間再検証]
F --> C
D --> G[CloudWatch アラームで監視開始]
G --> H[月次レビュー]
「1週間は長くないか?」と思うかもしれないが、週末や月末処理など業務パターンが出そろわないと見えてこない誤検知もあるので、これくらい時間をかけた方がいい。
ログからの誤検知特定クエリ
WAFログをAthenaで分析するのが定番で、うちでは以下のクエリを使って誤検知候補を洗い出している。
-- S3に保存したWAFログをAthenaで分析
-- 過去7日間でBlockされたリクエストのうち、
-- 正規ユーザーが多く使うパスへの誤検知候補を抽出
SELECT
terminatingruleid,
httprequest.uri,
httprequest.clientip,
COUNT(*) as block_count
FROM waf_logs
WHERE action = 'BLOCK'
AND from_iso8601_timestamp(timestamp) > NOW() - INTERVAL '7' DAY
AND httprequest.uri NOT LIKE '/admin%'
GROUP BY
terminatingruleid,
httprequest.uri,
httprequest.clientip
HAVING COUNT(*) > 10
ORDER BY block_count DESC
LIMIT 50;
「同じIPが同じURIで繰り返しブロックされている」パターンを探す。それが正規のAPIクライアントであれば、除外ルールの候補になる。地味に便利なのがHAVING COUNT(*) > 10の絞り込みで、1〜2回だけのブロックはノイズが多いのでこのくらいのしきい値にしておくと見通しがよくなる。
CDKでのルール除外設定
除外ルールはこんな感じで設定している(CDK TypeScript):
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
// ALB用WebACL
const webAcl = new wafv2.CfnWebACL(this, 'AppWebACL', {
scope: 'REGIONAL',
defaultAction: { allow: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AppWebACL',
},
rules: [
// IP評判リスト(最優先でブロック)
{
name: 'AWSManagedRulesAmazonIpReputationList',
priority: 0,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesAmazonIpReputationList',
},
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'IpReputationList',
},
},
// CommonRuleSetは特定ルールを除外してBlockモード
{
name: 'AWSManagedRulesCommonRuleSet',
priority: 1,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesCommonRuleSet',
// 大きなボディを扱うAPIエンドポイントの除外
excludedRules: [
{ name: 'SizeRestrictions_BODY' },
// Generic_LFI_QUERYARGUMENTS は自前のLFI対策で代替
{ name: 'Generic_LFI_QUERYARGUMENTS' },
],
// 特定パスのみルールをCountに上書き
ruleActionOverrides: [
{
name: 'CrossSiteScripting_COOKIE',
actionToUse: { count: {} },
},
],
},
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'CommonRuleSet',
},
},
// SQLインジェクション対策
{
name: 'AWSManagedRulesSQLiRuleSet',
priority: 2,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesSQLiRuleSet',
},
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'SQLiRuleSet',
},
},
// カスタムルール: 内部IPからのリクエストを許可(除外)
{
name: 'AllowInternalCIDR',
priority: 10,
action: { allow: {} },
statement: {
ipSetReferenceStatement: {
arn: internalIpSet.attrArn,
},
},
visibilityConfig: {
sampledRequestsEnabled: false,
cloudWatchMetricsEnabled: false,
metricName: 'AllowInternalCIDR',
},
},
],
});
excludedRulesでルール単位の完全除外、ruleActionOverridesでBlock→Countへの上書きができる。この2つの使い分けがポイントで、完全除外は「このルールが業務要件と根本的に相容れない場合」、Countへの上書きは「まだ誤検知か確信が持てない場合の様子見」として使っている。完全除外を乱用するとせっかくのルールが骨抜きになるので、迷ったらまずCountへの上書きにしておく方が無難だ。
コストの現実と最適化
WAFのコスト構造は意外と複雑で、最初に見積もりが甘くて月次請求に驚いたことがある。
xychart-beta
title "WAF月額コスト推移(うちの構成)"
x-axis ["1月", "2月", "3月", "4月", "5月", "6月"]
y-axis "コスト(USD)" 0 --> 600
bar [280, 310, 450, 510, 390, 320]
line [280, 310, 450, 510, 390, 320]
3月・4月が高かったのはBot Controlを試験導入した影響だ。リクエスト数に応じた従量課金が思ったより大きく、結局Targeted BotControlは外してStandardのみにした。「試してみたらコスト2倍近くなった」という経験は一度痛い目を見ないと実感しにくいかもしれないが、本当に跳ね上がるので注意してほしい。
WAFのコスト内訳
| 項目 | 料金 |
|---|---|
| WebACL | $5.00/月/WebACL |
| ルールグループ | $1.00/月/ルールグループ |
| リクエスト(通常ルール) | $0.60/100万リクエスト |
| Bot Control(Common) | $1.00/100万リクエスト |
| Bot Control(Targeted) | $1.50/100万リクエスト |
| ATP/ACFP | $1.00/100万リクエスト |
| ログ出力(Firehose経由S3) | $0.0009/1,000件 |
月間10億リクエストのサービスだと、Bot Controlだけで月額$1,000追加になる計算だ。導入前によく試算したほうがいい。
ログのサンプリングとコスト削減
full loggingは意外とコストがかかる。うちは以下の方針でログ量を絞っている:
{
"LoggingConfiguration": {
"LogDestinationConfigs": [
"arn:aws:kinesis:ap-northeast-1:123456789:stream/waf-logs"
],
"RedactedFields": [
{
"SingleHeader": {
"Name": "authorization"
}
},
{
"SingleHeader": {
"Name": "cookie"
}
}
],
"LoggingFilter": {
"DefaultBehavior": "DROP",
"Filters": [
{
"Behavior": "KEEP",
"Requirement": "MEETS_ANY",
"Conditions": [
{
"ActionCondition": {
"Action": "BLOCK"
}
},
{
"ActionCondition": {
"Action": "COUNT"
}
}
]
}
]
}
}
}
ALLOWされた正常リクエストのログはDROPして、BLOCKとCOUNTのみKEEPしている。これだけでログ量が90%以上削減できた。authorizationとcookieはPIIが含まれる可能性があるのでRedactedに設定しておくのも重要で、SOC2審査的にも突っ込まれやすいポイントなので最初からやっておいた方がいい。
2026年時点で実践してよかった設定と反省
よかったこと
KnownBadInputsRuleSetの有効化は早めにやるべきだった。Log4Shell(CVE-2021-44228)への対応ルールが2026年現在も継続して更新されており、Log4j以外にも定期的に新しい既知悪意入力パターンが追加されている。正直最初はこのルールグループを後回しにしていたが、誤検知リスクが低い割に検出力が高い。最初に入れるべきルールグループを2つ選べと言われたら、これとIpReputationListを迷わず挙げる。
AWSManagedRulesAmazonIpReputationListは副作用が少なくコスパ最高。WCUが25と軽く、既知のTorノード・VPN・ボットIPを弾いてくれる。うちの環境では月間ブロック数の40%近くがこのルールグループによるもので、False Positiveはほぼゼロだった。
スコープダウンステートメントの活用。全リクエストにルールを適用する必要はない。たとえばSQLiRuleSetはDBアクセスが発生するAPIエンドポイント(/api/配下)のみに適用することでWCU消費とコストを抑えられる:
// SQLiルールを /api/ パスのみに適用するスコープダウン
{
name: 'AWSManagedRulesSQLiRuleSet',
priority: 2,
statement: {
andStatement: {
statements: [
{
// /api/ パスへのリクエストのみ
byteMatchStatement: {
searchString: '/api/',
fieldToMatch: { uriPath: {} },
textTransformations: [{ priority: 0, type: 'NONE' }],
positionalConstraint: 'STARTS_WITH',
},
},
{
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesSQLiRuleSet',
},
} as any,
],
},
},
overrideAction: { none: {} },
...
}
反省していること
Account Takeover Protection(ATP)は最初から真剣に検討すべきだった。うちのサービスはBtoB認証が多いので当初は不要と判断していたが、クレデンシャルスタッフィング攻撃が2025年後半から目立って増えてきた。ATPはログインエンドポイントへの総当たりをカウントして遮断してくれるのでかなり有効なのだが、設定が複雑で今も正式導入には至っていない。「BtoBだからボット攻撃は関係ない」という思い込みが甘かった。
マネージドルールの自動更新タイミングを把握していなかった。AWSはマネージドルールを予告なしに更新することがある。一度、ルール更新のタイミングでFalse Positiveが急増してアラートが鳴ったことがあった。OWASP Top 10 2024対策|脆弱性10項目の実装方法と企業の守り方とも絡む話だが、マネージドルールの更新履歴はAWSのドキュメントで確認できるので定期的にチェックする習慣をつけた方がいい。月次レビューのチェックリストに入れておくくらいがちょうどいい。
CloudWatchアラームで異常を早期検知
import boto3
cw = boto3.client('cloudwatch', region_name='ap-northeast-1')
# WAFブロック数の急増を検知するアラーム
cw.put_metric_alarm(
AlarmName='WAF-BlockRate-High',
ComparisonOperator='GreaterThanThreshold',
EvaluationPeriods=2,
MetricName='BlockedRequests',
Namespace='AWS/WAFV2',
Period=300, # 5分
Statistic='Sum',
Threshold=1000, # 5分で1000リクエストブロック
ActionsEnabled=True,
AlarmActions=['arn:aws:sns:ap-northeast-1:123456789:waf-alert-topic'],
AlarmDescription='WAFブロック数が急増しています。誤検知の可能性もあるため確認してください。',
Dimensions=[
{
'Name': 'WebACL',
'Value': 'AppWebACL'
},
{
'Name': 'Rule',
'Value': 'ALL'
},
{
'Name': 'Region',
'Value': 'ap-northeast-1'
}
]
)
# False Positive監視: Countモードのルールで検出が多すぎる場合
cw.put_metric_alarm(
AlarmName='WAF-CountRate-Suspicious',
ComparisonOperator='GreaterThanThreshold',
EvaluationPeriods=1,
MetricName='CountedRequests',
Namespace='AWS/WAFV2',
Period=3600, # 1時間
Statistic='Sum',
Threshold=5000, # 1時間で5000件Count
ActionsEnabled=True,
AlarmActions=['arn:aws:sns:ap-northeast-1:123456789:waf-alert-topic'],
AlarmDescription='Countルールの検出が多すぎます。Blockモード移行前に要確認。',
Dimensions=[
{'Name': 'WebACL', 'Value': 'AppWebACL'},
{'Name': 'Rule', 'Value': 'ALL'},
{'Name': 'Region', 'Value': 'ap-northeast-1'}
]
)
print('WAF監視アラーム設定完了')
ブロック急増はDDoSかFalse Positiveの二択なので、どちらの可能性でも検知できるように設計している。CountモードのアラームはBlockモード移行を判断する前の「事前察知」として機能するので、両方セットで入れておくのが正解だと思っている。
まとめ
1年間WAF v2マネージドルールと格闘してきた結論をまとめると、こうなる。
① マネージドルールは必ずCountモードから始めること。本番でいきなりBlockモードに変更するのは自傷行為。最低1週間のCountモード運用とログ分析が必須だ。
② AWSManagedRulesAmazonIpReputationList + KnownBadInputsRuleSetは最初に入れるべき2枚。副作用が少なく、WCUも軽い。この2つだけで相当数の攻撃をカバーできる。
③ ログはBLOCKとCOUNTのみに絞る。全量ログはコストが跳ね上がる。LoggingFilterでALLOWをDROPするだけでログ費用が1/10以下になるケースもある。
④ 誤検知対応はexcludedRulesとruleActionOverridesの使い分けで。完全除外が必要なケースは意外と少なく、たいていCountへの上書きで十分対応できる。
⑤ コストはBot Controlを試す前に試算せよ。リクエスト数が多いサービスでBot Control Targetedモードを入れると、思った以上に請求が膨らむ。
まず手をつけるとしたら、WAFログをAthenaで分析できる環境を整えることをお勧めする。ルールグループの評価はデータなしには始まらないし、ログ基盤があれば誤検知の発見も格段に速くなる。そこから段階的にBlockモードに移行していけば、本番障害を起こさずにWAFをフル活用できる状態に近づけられるはずだ。
みなさんのチームではWAFのマネージドルールをどこまで有効化してますか?特にBot Controlのコストとの折り合いのつけ方、もし知見があればコメントで教えてほしい。