
- 1. はじめに
- 2. AWS WAF導入の背景
- 3. 導入前の検討事項
- 4. 段階的な導入アプローチ
- 5. ログ設計とコスト最適化
- 6. Blockモードへの切り替え
- 7. 導入後の効果
- 8. まとめ
1. はじめに
DMMオンラインサロンでエンジニアをしている落合です。
本記事では、既存のCloudFront環境にAWS WAFを導入した際の話になります。
「WAFを入れたいけど、どう進めればいいかわからない」「誤検知が怖くて踏み出せない」という方の参考になれば幸いです。
2. AWS WAF導入の背景
定期的な攻撃への対応
DMMオンラインサロンのサービスは、定期的に外部からの攻撃を受けていました。
アプリケーション層での対策により攻撃自体は問題なく弾けていたものの、「そもそもアプリケーションに到達する前にブロックできた方がいいよね」という話から、AWS WAFの導入を検討することになりました。
AWS WAFを選んだ理由
AWS WAFを選定した理由は以下のとおりです。
- 既存のCloudFrontに簡単にアタッチできる
- マネージドルールを活用すれば、ゼロからルールを作成する必要がない
- AWSの他サービスとの連携が容易
3. 導入前の検討事項
対象リソースの選定
今回はCloudFrontを対象にWAFを導入しました。
すでにCloudFrontを利用していたため、CloudFrontへの適用がもっともシンプルな選択でした。
リージョンの注意点
CloudFront用のWAFを作成する際は、us-east-1(バージニア北部)リージョンで作成する必要があります。
これはCloudFrontがグローバルサービスであるため、WAFもグローバルスコープ(us-east-1)で作成する必要があるためです。
参考:【小ネタ】AWS WAF v2 の WebACL (CloudFront用)を東京リージョンから CloudFormation で作成しようとしたら怒られた - クラスメソッド、AWS公式ドキュメント
WCU(Web ACL Capacity Units)の制約
AWS WAFにはWCUという概念があり、1つのWeb ACLあたり上限5000WCUという制約があります。
ただし、1500WCUを超えると追加料金が発生するため、コストを抑えるには1500WCU以内に収めることが望ましいです。
マネージドルールはそれぞれWCUを消費するため、「とりあえず全部入れる」というわけにはいきません。
特にWCUの消費が大きいルールグループを使用する場合、他のルールとの組み合わせを慎重に検討する必要がありました。
どのルールを選ぶべきか
ルール選定はもっとも悩んだポイントです。
マネージドルールには様々な種類がありますが、すべてを有効にすると1500WCUを超えてコストが増加してしまいます。
最終的には、サービスの特性と想定される攻撃パターンを考慮し、1500WCUギリギリに収まる範囲でルールを選定しました。
4. 段階的な導入アプローチ
まずはCountモードで開始
AWS WAFのルールには、リクエストをブロックするBlockと、検知のみ行うCountの2つのアクションがあります。
いきなりBlockモードで本番投入すると、正常なリクエストまでブロックしてしまうリスクがあります。
そこで、まずは全ルールをCountモードで設定し、どのようなリクエストが検知されるかを確認することにしました。
誤検知がある前提で、まずは確認から始める。これがWAF導入の鉄則です。
誤検知の発見と対応
Countモードで運用を開始したところ、案の定誤検知が発生しました。
誤検知の例:サービス間通信
私たちのシステムでは、サービス間で特殊なフォーマットのリクエストをやり取りしていました。
このリクエストがWAFのルールに引っかかり、攻撃として分類されていたのです。
このような誤検知に対しては、該当するルールを個別にCountモードに設定することで対応しました。
ルールグループ全体をCountにするのではなく、特定のルールだけをCountにすることで、他の防御は維持しつつ誤検知を回避できます。
検証期間について
当初は1ヶ月間のCountモード検証を想定していました。
しかし、検証期間中も攻撃は継続しており、「早くBlockモードにして攻撃を止めたい」という状況でした。
2週間の検証で主要な検知パターンを把握できたため、予定を前倒ししてBlockモードへ移行することを決断しました。
5. ログ設計とコスト最適化
CloudWatch LogsではなくS3を選択
WAFのログ出力先としては、以下の選択肢があります。
- Amazon CloudWatch Logs
- Amazon S3
- Amazon Kinesis Data Firehose
今回はコスト面を考慮してS3への直接出力を選択しました。
CloudWatch Logsはリアルタイムでの分析に便利ですが、ログ量が多いとコストがかさみます。
S3バケットの命名規則に注意
S3にWAFログを出力する際、バケット名に aws-waf-logs- というプレフィックスが必要です。
例:aws-waf-logs-my-service-production
この命名規則を満たしていないバケットにはログを出力できないため、注意が必要です。
ルールの適用順番とコスト最適化
【re:invent 2025】AWS WAF の誤検知回避 - カスタムルールのテクニックを学ぶ (NET-301参加レポート)で知ったのですが、ルールの適用順番もコストに影響します。
Bot Controlなど一部のルールはリクエスト単位で追加料金が発生するため、これらを優先順位の上位に配置するとコストがかさみます。
安価なルールを先に配置して不要なリクエストをブロックし、高額なルールに到達するリクエスト数を減らすことでコストを抑えられます。
Athenaでのログ分析
S3に出力したWAFログは、Amazon Athenaを使って分析できます。
Partition Projectionを活用することで、特定の日時を指定して効率的にログを抽出できます。
以下はAthenaでWAFログを分析するためのテーブル作成クエリの例です。
CREATE EXTERNAL TABLE IF NOT EXISTS waf_logs ( `timestamp` bigint, `formatversion` int, `webaclid` string, `terminatingruleid` string, `terminatingruletype` string, `action` string, `terminatingrulematchdetails` array<struct<conditiontype:string,sensitivitylevel:string,location:string,matcheddata:array<string>>>, `httpsourcename` string, `httpsourceid` string, `rulegrouplist` array<struct< rulegroupid:string, terminatingrule:struct< ruleid:string, action:string, rulematchdetails:array<struct<conditiontype:string,sensitivitylevel:string,location:string,matcheddata:array<string>>> >, nonterminatingmatchingrules:array<struct< ruleid:string, action:string, overriddenaction:string, rulematchdetails:array<struct<conditiontype:string,sensitivitylevel:string,location:string,matcheddata:array<string>>>, challengeresponse:struct<responsecode:string,solvetimestamp:string>, captcharesponse:struct<responsecode:string,solvetimestamp:string> >>, excludedrules:array<struct< exclusiontype:string, ruleid:string >> >>, `ratebasedrulelist` array<struct<ratebasedruleid:string,limitkey:string,maxrateallowed:int>>, `nonterminatingmatchingrules` array<struct<ruleid:string,action:string,rulematchdetails:array<struct<conditiontype:string,sensitivitylevel:string,location:string,matcheddata:array<string>>>,challengeresponse:struct<responsecode:string,solvetimestamp:string>,captcharesponse:struct<responsecode:string,solvetimestamp:string>>>, `requestheadersinserted` array<struct<name:string,value:string>>, `responsecodesent` string, `httprequest` struct<clientip:string,country:string,headers:array<struct<name:string,value:string>>,uri:string,args:string,httpversion:string,httpmethod:string,requestid:string,fragment:string,scheme:string,host:string>, `labels` array<struct<name:string>>, `captcharesponse` struct<responsecode:string,solvetimestamp:string,failurereason:string>, `challengeresponse` struct<responsecode:string,solvetimestamp:string,failurereason:string>, `ja3fingerprint` string, `ja4fingerprint` string, `oversizefields` string, `requestbodysize` int, `requestbodysizeinspectedbywaf` int) PARTITIONED BY ( `log_time` string) ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe' STORED AS INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat' LOCATION 's3://your-bucket-name/AWSLogs/your-account-id/WAFLogs/cloudfront/web-acl-name/' TBLPROPERTIES ( 'projection.enabled'='true', 'projection.log_time.format'='yyyy/MM/dd/HH/mm', 'projection.log_time.interval'='5', 'projection.log_time.interval.unit'='minutes', 'projection.log_time.range'='2025/01/01/00/00,NOW', 'projection.log_time.type'='date', 'storage.location.template'='s3://your-bucket-name/AWSLogs/your-account-id/WAFLogs/cloudfront/web-acl-name/${log_time}' );
参考:AWS WAFのアクセスログから特定日時のログをAmazon Athenaで抽出する - クラスメソッド
誤検知判断のためのクエリ
Countモードで検知されたリクエストが誤検知かどうかを判断するには、以下のクエリが便利です。
特定のルールでカウントされたリクエストの詳細を確認し、正常なリクエストかどうかを判断できます。
SELECT w.ja4fingerprint, nn.ruleid, /* ルール名 */ nn.action, /* COUNT or BLOCK or ALLOW */ w.log_time, w.httprequest.uri, w.httprequest.args, /* パラメータ */ nnn.conditiontype, /* 何に引っかかったか */ nnn.location, nnn.matcheddata, w.labels, /* 引っかかったルールのラベル */ w.httprequest.clientip, w.httprequest.headers, nn.ruleMatchDetails FROM waf_logs AS w CROSS JOIN UNNEST(w.nonterminatingmatchingrules) AS m(nn) CROSS JOIN UNNEST(nn.ruleMatchDetails) AS m(nnn) CROSS JOIN UNNEST(w.httprequest.headers) AS m2(h) CROSS JOIN UNNEST(w.labels) AS m3(l) WHERE w.log_time >= '2025/01/01/00/00' AND w.log_time <= '2025/01/02/00/00' AND nn.action = 'COUNT' AND nn.ruleid = 'AWS-AWSManagedRulesCommonRuleSet' /* 絞りたいルール名 */ AND l.name = 'awswaf:managed:aws:linux-os:XXX_XXXXXXX' /* 絞りたいラベル名 */ AND w.ja4fingerprint != 'xxxxxxxx_xxxxxxxxxxxx_xxxxxxxxxxxx' /* 除外するフィンガープリント */
私たちの場合、ja4fingerprint(TLS接続の特徴を識別するフィンガープリント)を使ってユーザー単位でリクエストの中身を確認し、悪質なリクエストを行ったユーザーをWHERE句の除外条件に追加していきました。
カウントが0になるまでこの作業を繰り返すことで、正常なユーザーがCountされていないことを確認しました。
6. Blockモードへの切り替え
2週間のCountモード検証を経て、Blockモードへ切り替えました。
切り替え時のポイントは以下のとおりです。
- 誤検知が確認されたルールは個別にCountを維持
- 切り替え直後はログを注視
- 問題があれば即座にCountに戻せる体制を確保
幸い、大きなトラブルなく切り替えを完了できました。
7. 導入後の効果
攻撃アラートの消失
WAFをBlockモードにした後、攻撃によるアラートがほぼなくなりました。
これまでは攻撃のたびにアラートが飛び、状況確認に時間を取られていましたが、WAFが事前にブロックしてくれるため、アプリケーションに到達する攻撃が激減しました。
運用面での安定化
アプリケーションサーバーへの負荷が軽減され、サービス全体が安定しました。
攻撃対応に追われることがなくなり、本来の開発業務に集中できるようになったのは大きな成果です。
これにより、日々の安寧を手に入れることができました。
8. まとめ
AWS WAF導入のポイントを振り返ります。
| ポイント | 内容 |
|---|---|
| 段階的な導入 | いきなりBlockではなく、Countモードから開始 |
| WCUの意識 | コスト増を避けるため1500WCU以内でルール選定 |
| 誤検知対策 | サービス固有の通信パターンを把握し、個別ルールで対応 |
| 検証期間 | 状況に応じて柔軟に調整 |
| ログ設計 | コストを考慮してS3出力を選択 |
| ルールの順番 | 安価なルールを優先し、高額なルールは後回しに |
WAF導入は「入れて終わり」ではなく、継続的な監視と調整が必要です。
しかし、一度適切に設定してしまえば、セキュリティ面での安心感は大きく向上します。
これからWAF導入を検討している方は、ぜひCountモードからの段階的導入を試してみてください。