累計1,500万リクエストの大量ボットにAWS WAF Monetizeで402 Payment Requiredを返してみた
はじめに
前回の記事で、AWS WAFの新機能Monetize(402 Payment Required)を検証環境で動作確認しました。Monetizeは、条件に一致したリクエストに対してHTTP 402を返し、支払い情報(x402 price manifest)を提示する機能です。
今回はこの機能を本番環境で実際に発生していた問題に対して適用した記録です。対象環境はdev.classmethod.jp(CloudFront + AWS WAF)です。
問題の発見: UA偽装ボットの特定
GA4 Direct の異常増加
5月末、GA4のDirectチャネルが急増していることに気づきました。CloudFrontのアクセスログを分析した結果、特定の /24帯から3IPで大量のアクセスが来ていることを確認しました。
観測された技術的特徴
WAFのSampled Requestsおよびアクセスログから、以下の事実を観測しました。
- UA: Chrome/114.0.0.0固定。Chrome/114は2023年6月リリースであり、2年以上前のバージョンです。現行ブラウザーが自動更新されずこのバージョンに留まっていることは考えにくく、固定されたUA文字列である可能性が高いと判断しました
- accept-language:
enのみ(品質値なし)。通常のブラウザーはen-US,en;q=0.9のように指定するため、簡素化されたヘッダ設定です - JSアセット取得あり:
_next/static/chunks/*.jsの取得が確認されました。JavaScript実行環境を伴うフルブラウザー型アクセスの可能性を示す材料と判断しました - JA3/JA4フィンガープリント: 3IPすべてで同一値。同一のTLS実装・設定で動作していると推定されます
- HTTP/2.0 + HTTP/3.0 混在: プロトコル自動ネゴシエーション
ARIN whoisでは、このIP帯は有名SaaS企業に割り当てられていました。使用されているUAはChrome UAであり、Botを明示する文字列は含まれていません。
定量的な裏付け
対象IP帯からのアクセスを日別・IP別に集計した結果です。
| 日付 | IP-A | IP-B | IP-C | 合計 |
|---|---|---|---|---|
| 06/09 | 290,562 | 290,702 | 291,517 | 872,781 |
| 06/10 | 289,268 | 293,359 | 290,464 | 873,091 |
| 06/11 | — | — | — | (※) |
| 06/12 | 290,553 | 292,475 | 292,013 | 875,041 |
| 06/13 | 292,203 | 289,851 | 291,722 | 873,776 |
| 06/14 | 287,925 | 292,189 | 294,076 | 874,190 |
| 06/15 | 291,085 | 291,778 | 291,746 | 874,609 |
※ 6/11はログデータを取得できませんでした。
- 日次総量が約87万で安定。日ごとの振れ幅は0.3%以下であり、レート固定の自動化アクセスである可能性が高いと判断しました
- 3IP間の分散が精密。各日のIP間の差は概ね2%以内に収まっており、送信元で何らかの均等分散(ロードバランサ、IPローテーション等)が行われていると推定されます
- 同一IP帯からChrome/148.0.7778.97(Linux)の微量アクセス(計4件)も観測されましたが、主力UAとの関係は不明です
18日間の保留
5/29に検知してから6/16の対策実施まで18日間が経過しています。Block等の対処手段はありましたが、CloudFrontのキャッシュヒット率が高くオリジンへの負荷は軽微であり、帯域・レスポンスタイムにも目立った悪化がなかったため、対応を保留していました。
対応に踏み切った契機は、副作用の小さい新たな選択肢(Monetize機能)のリリースです。
なぜBlockでもChallengeでもなく402か
前章の観測事実(JSアセット取得・固定UA・3IP同一JA3/JA4)から、自動化されたフルブラウザー型アクセスの可能性が高いと判断しました。この判断を前提に、対応方針を検討しました。
Challenge見送り: 自動化されたフルブラウザー型アクセスであれば、JS Challengeを機械的に通過する可能性があります。これは未検証であり、突破された場合は対策になりません。
Block見送り: IPアドレスの保有者が有名SaaS企業であることから、以下のシナリオを考慮しました。
- 保有者自身のサービスが設定ミスや仕様としてクロールしている可能性
- 保有者に連絡・確認するまでBlockは避けたい
402の誤判定リスク: 402はChallengeと異なり、誤判定時の影響が大きい選択肢です。
| Challengeの誤判定 | 402の誤判定 | |
|---|---|---|
| 有人ブラウザーの場合 | 多くの通常ブラウザーではJS実行により透過的に通過。影響は限定的 | 人間にとっては実質Block。暗号通貨で支払える一般ユーザーはほぼいない |
今回のケースでは「特定の /24帯に属するIP」かつ「そのIP個別で評価ウィンドウ60秒に100リクエスト超過」の2条件ANDで判定しています。一般的な有人ブラウザーがこの2条件を同時に満たす可能性は極めて低いと判断しました。
有人利用の可能性があるトラフィックに402を適用する場合は、複数条件のAND判定で十分に絞り込んだうえでの適用を推奨します。
以上の理由から402を選択し、レートベースとの組み合わせにより「通常のアクセス量では許可し、しきい値超過状態と判定された送信元には402を返す」という制御を実現します。
実装
IP Set作成
対象の /24(216.198.x.x/24)をIP Setに登録しました。
/24を採用した理由は、同一レンジ内の3IPが均等に使用されており今後の追加IPにも対応するためです。/24の副作用(対象範囲が広がる)は、レートベース超過とのAND条件によって「同一/24内かつ短時間に大量アクセスした送信元」に絞り込むことで抑制しています。ただし範囲内の別ホストがレート超過すれば対象になり得る点は許容したうえで適用しています。
RateBasedルール
| 設定項目 | 値 |
|---|---|
| 評価ウィンドウ | 60秒 |
| しきい値 | 100 |
| 集計キー | IP(各IP個別に評価) |
| ScopeDown | 上記IP Set |
| アクション | Count |
| ラベル | custom:rate-exceeded:headless-bot |
アクションをCountにしている理由は、RateBasedルール単体ではMonetizeアクションを指定できないためです。Countでリクエストを終端させずラベルを付与し、後続のMonetizeルールでそのラベルを参照して402を発動させる構成にしています。
Monetizeルール
| 設定項目 | 値 |
|---|---|
| 条件 | ラベル custom:rate-exceeded:headless-bot 一致 AND IP Set一致 |
| アクション | Monetize |
| PriceMultiplier | 10 |
条件にIP Setを再度含めているのは安全策です。RateBasedのScopeDownで既にIP Setに絞っていますが、将来ラベル名を他ルールで流用した場合に意図しないMonetize発動を防ぐためです。
単価の計算:
MonetizationConfig の Prices[].Amount: 0.001 USDC(Base Price)
× PriceMultiplier: 10
= 0.01 USDC/request
MonetizationConfig
{
"CryptoConfig": {
"PaymentNetworks": [
{
"Chain": "BASE_SEPOLIA",
"WalletAddress": "0x2f0cb3deddd256f4c889...xxxxxxxxxxxx",
"Prices": [{"Amount": "0.001", "Currency": "USDC"}]
}
]
},
"CurrencyMode": "TEST"
}
今回はTEST mode / Base Sepoliaテストネットでの適用です。実課金・実収益ではありません。
CLI直接操作
CloudFormation未対応のため、AWS CLIで直接操作しました。手順は get-web-acl でLockTokenを取得し、そのトークンを update-web-acl に渡してルールを追加します。
aws wafv2 get-web-acl \
--name devio2024-waf-bot-webacl \
--scope CLOUDFRONT \
--id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
--region us-east-1
出力に含まれる LockToken を次のコマンドに渡します。実際の update-web-acl では Rules だけでなく既存の DefaultAction・VisibilityConfig 等を保持した状態で渡す必要があります(以下は簡略例です)。
aws wafv2 update-web-acl \
--name devio2024-waf-bot-webacl \
--scope CLOUDFRONT \
--id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
--lock-token <取得したLockToken> \
--rules file://rules.json \
--region us-east-1
LockTokenは楽観ロックです。get-web-acl で取得した後に別の更新が入ると update-web-acl が失敗します。
動作確認
WAFログで、設計通りに動作していることを確認しました。レート超過時のログレコードです。
{
"timestamp": 1781595891917,
"terminatingRuleId": "Monetize-HeadlessBot",
"terminatingRuleType": "REGULAR",
"action": "MONETIZE",
"rateBasedRuleList": [
{
"rateBasedRuleName": "RateLabel-HeadlessBot",
"limitKey": "IP",
"maxRateAllowed": 100,
"evaluationWindowSec": 60
}
],
"nonTerminatingMatchingRules": [
{
"ruleId": "RateLabel-HeadlessBot",
"action": "COUNT"
}
],
"responseCodeSent": 402,
"httpRequest": {
"clientIp": "216.198.x.x",
"country": "US",
"uri": "/articles/securityhub-amazon-managed-grafana",
"httpVersion": "HTTP/2.0",
"httpMethod": "GET",
"headers": [
{"name": "user-agent", "value": "Mozilla/5.0 (Macintosh; ...) Chrome/114.0.0.0 Safari/537.36"},
{"name": "accept-language", "value": "en"}
]
},
"labels": [
{"name": "awswaf:xxxxxxxxxxxx:webacl:devio2024-waf-bot-webacl:custom:rate-exceeded:headless-bot"}
],
"ja3Fingerprint": "885a2f978c1b08c89c8baba21a1625b5",
"ja4Fingerprint": "t13d1516h2_8daaf6152771_d8a2da3f94cd"
}
このログから、二段構成の動作が確認できます。
nonTerminatingMatchingRulesにRateBasedルール(Count)が記録されている → リクエストを終端させずラベルを付けて後続に渡した証跡terminatingRuleIdがMonetizeルール → 最終的にMonetizeルールが終端し402を返したlabelsにレート超過ラベルが付与されている → RateBasedからMonetizeへのラベル連携が機能
対象IP帯の3IPすべてで、レート超過時に402が発動しました。一方、同じ送信元でもしきい値内(60秒間に100リクエスト以下)のアクセスは action: ALLOW で通常通り許可されており、レート状態に応じて切り替わることを確認しています。
x402 price manifest(402レスポンスボディに含まれる支払い情報)の詳細は前回記事を参照してください。
運用上の注意・今後の予定
402はリクエスト停止を保証しません。 bot側が行動を変えない限り、CloudFront/WAFまではリクエストが到達し続けます。
今後の予定です。
- 支払いが確認された場合: CurrencyModeを
REALに変更し、Baseメインネットに移行。受け取り用のウォレット(Base対応)手続きを行います - 支払いも停止もない場合: 改善が望めないリクエスト元としてBlock対象へ移行します。402は期間限定の猶予措置です
- IaC化: 現状はCloudFormation管理外の手動変更のためドリフトが発生しています。CFn対応後にIaC化する予定です
効果測定(アクセス量の変化・GA4への影響)は本記事のスコープ外です。適用後十分な期間を経た後に別途検証する予定です。
まとめ
UA偽装が疑われる自動化アクセスによる大量クロールに対し、AWS WAFのMonetize機能で402 Payment Requiredを返す対応を行いました。402は「敵対」ではなく「取引」のシグナルであり、相手に選択肢を残す対応です。Block / Challenge / 402のどれを選ぶかは、アクセス主体の性質と誤判定リスクに応じた判断です。技術的にはRateBased(Count)+ Monetizeの二段構成で「通常量は許可、超過は有料」を実現しました。TEST modeで動作を確認したうえで、REALへ段階的に移行できる構成です。







