IP分散型ボットで13分間の5xxが発生したため、WAFのJA4 Rate-basedルールとECSオートスケール高速化で対処してみた
はじめに
2026年6月、当サイト(dev.classmethod.jp)で5xxエラーが13分間にわたり発生しました。原因はIP分散型ボットによるリクエストスパイクです。89のIPアドレスから毎分約4,000リクエストが流入し、ECS全タスクが処理能力を超えてダウンしました。
WAFログを分析して攻撃元を特定し、再発防止策として以下の2つを実施しています。
- WAF: JA4フィンガープリントを集約キーにしたRate-basedルールの追加
- ECS: オートスケーリングの高解像度メトリクス導入による検知高速化
| 項目 | Before | After |
|---|---|---|
| WAF Rate-based (JA4) | なし | 100 req/60秒 超過で CHALLENGE(JP/US・正規Bot・トークン取得済み除外) |
| ECSメトリクス解像度 | 60秒 | 20秒(HighResolution) |
| スケールアウト検知 | 約7分(今回の実測) | 約2分(参考記事の実測値からの推定) |
5xxの発生
発生状況
2026-06-23 03:26 UTC(12:26 JST)にボットがリクエストを開始し、2分後の03:28から5xxが本格化しました。ボットが自主停止する03:41まで、13分間で約43,000件の5xxが発生しています。
| 時刻(UTC) | ALB リクエスト数 | レイテンシ(avg) | 5xx | Healthyホスト数 |
|---|---|---|---|---|
| 03:25(平常) | 350 req/min | 134ms | 0 | 6 |
| 03:26(開始) | 1,164 | 519ms | 5 | 6 |
| 03:28 | 3,675 | 993ms | 415 | 6 |
| 03:29 | 4,633 | 1,873ms | 2,669 | 0 |
| 03:33 | 5,108 | 2,558ms | 3,892 | 0 |
| 03:38 | 5,533 | 2,075ms | 5,190 | 0 |
| 03:41(停止) | 1,477 | 1,073ms | 767 | 回復中 |
| 03:42(収束) | 560 | 306ms | 0 | 回復 |
なぜ5xxが出たか
ボットの毎分4,000リクエストがWAFを全件通過し(判定: ALLOW)、ECS 6台のキャパシティを2分で超過しました。全タスクがunhealthyになり、ALBがターゲットなしの状態で503を返し続けたのが5xxの正体です。
[Bot 4,000 req/min] → [WAF: ALLOW] → [CloudFront] → [ALB] → [ECS 6台: 全滅]
↑ ここで止めるべきだった ↑ 2分で限界
再発防止策では、このWAFのポイントにJA4 Rate-basedルールを追加しています。
サービス回復はボットの自主停止(03:41)によるものです。オートスケールは03:33に発動しましたが、タスク起動完了まで11分かかり、その間ずっとHealthyホスト0のままでした。
調査: WAFログ分析によるBot特定
ALBメトリクスでリクエスト急増を検知後、WAFの判定状況を確認しました。該当時間帯のリクエストは全件ALLOWであり、既存ルールでのブロックはゼロでした。
ここからWAFログ(CloudWatch Logs Insights)を使い、攻撃元の特定と判定軸の選定を行いました。
国コードの集計
-- ロググループ: aws-waf-logs-devio2024-waf-bot
-- 時間範囲: 2026-06-23 03:26〜03:39 UTC
stats count(*) as cnt by httpRequest.country
| sort cnt desc
| limit 10
ロシア(RU)からのリクエストが全体の66%を占めて突出していました。
| 国 | リクエスト数 | 割合 |
|---|---|---|
| RU | 44,977 | 66% |
| US | 9,862 | 15% |
| JP | 2,637 | 4% |
IP分散の確認
filter httpRequest.country = 'RU'
| stats count(*) as cnt, count_distinct(httpRequest.clientIp) as unique_ips
89のユニークIPから44,977リクエスト。1IPあたり約39 req/minです。IP単体のレート制限では正規クローラー(Bingbotは150 req/5min程度)と区別がつかず、IPベースでの対処は困難でした。
JA4フィンガープリントの集計
filter httpRequest.country = 'RU'
| stats count(*) as cnt by ja4Fingerprint
| sort cnt desc
| limit 10
| JA4 | 件数 | 割合 |
|---|---|---|
t13d1713h1_ab0a1bf427ad_ecd0401ec68b |
44,910 | 99.85% |
| その他(8種) | 67 | 0.15% |
89 IPが同一のJA4フィンガープリントを持っており、同一ツール(ボットネット)からのアクセスである可能性が高いと判断しました。
JA4フィンガープリントはTLS ClientHello由来の値で、User-Agentのように任意に書き換えることができません。
このボットはUser-Agentを正規Chrome(Chrome/134.0.0.0)に偽装していましたが、JA4のALPN部分が h1(HTTP/1.1のみ)でした。正規ChromeならHTTP/2(h2)をネゴシエートするのが一般的であり、UAとTLSの挙動が一致しない点も偽装を示唆する材料となりました。
正常時との比較
攻撃前の時間帯(03:19〜03:26 UTC)で同じJA4を検索した結果、一切出現していませんでした。後述の効果確認では攻撃前24時間でも未出現を確認しており、誤検知のリスクは低いと判断しました。
判定軸の選定(消去法)
| 判定軸 | 使えるか | 理由 |
|---|---|---|
| User-Agent | ✗ | Chrome偽装済み。UAで絞ると正規Chromeも巻き込む |
| IPアドレス | ✗ | 89に分散。1IPあたり39 req/minは正規クローラーと区別困難 |
| 国コード単体 | ✗ | RU全体ブロックは副作用が大きい |
| JA4フィンガープリント | ○ | 99.85%一致、正常時に出現しない。JP/US除外と併用で副作用を抑えつつ制限可能 |
再発防止策
当サイトの運用方針として、ボットは基本的にブロックしていません。AIクローラーを含め、正規ユーザーに影響が出ない限り許容しています。今回は5xxが発生し正規ユーザーに影響が出たため、対処に踏み切りました。
施策1: WAF — JA4集約キーによるRate-basedルール
JA4フィンガープリントを集約キーにしたRate-basedルールを追加しました。
ルール設計
| 設計項目 | 値 | 判断根拠 |
|---|---|---|
| 集約キー | JA4 Fingerprint | IP分散でも同一ツールを1グループとして捕捉できる |
| 閾値 | 100 req/60秒 | llms.txt宣言(60 req/60s)と整合。攻撃時は4,000 req/minで大幅超過 |
| Scope-down | AND条件(後述) | 正規ブラウザー・正規Bot・検証済みBotを除外し、誤検知を抑える |
| アクション | CHALLENGE | 正規ブラウザーはCHALLENGEトークンを返せるため通過可能。単純なボットは通過困難 |
| FallbackBehavior | MATCH | 後述 |
CloudFormationテンプレート
# Rate-based rule for JA4 fingerprint (non-JP/US) - Bot mitigation
- Name: !Sub '${AWS::StackName}-Ratebased-JA4-NonJPUS'
Priority: 203
Action:
Challenge: {}
Statement:
RateBasedStatement:
Limit: 100
EvaluationWindowSec: 60
AggregateKeyType: CUSTOM_KEYS
CustomKeys:
- JA4Fingerprint:
FallbackBehavior: MATCH
ScopeDownStatement:
AndStatement:
Statements:
# NOT JP/US
- NotStatement:
Statement:
GeoMatchStatement:
CountryCodes:
- JP
- US
# NOT verified bot (Googlebot, Bingbot, etc.)
- NotStatement:
Statement:
LabelMatchStatement:
Scope: LABEL
Key: awswaf:managed:aws:bot-control:bot:verified
# NOT user-triggered verified bot (ChatGPT User, etc.)
- NotStatement:
Statement:
LabelMatchStatement:
Scope: LABEL
Key: awswaf:managed:aws:bot-control:bot:user_triggered:verified
# Token absent (no prior Challenge/CAPTCHA token)
- LabelMatchStatement:
Scope: LABEL
Key: awswaf:managed:token:absent
# Challengeable request (HTML pages only, not static assets)
- LabelMatchStatement:
Scope: LABEL
Key: awswaf:managed:aws:anti-ddos:challengeable-request
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub '${AWS::StackName}-Ratebased-JA4-NonJPUS'
SampledRequestsEnabled: true
AggregateKeyType: CUSTOM_KEYSとJA4Fingerprintの組み合わせで、JA4ごとにリクエスト数をカウントします。IPが89に分散していても、同一JA4であれば合算して閾値判定されますEvaluationWindowSec: 60(1分間)でLimit: 100です。今回の攻撃は4,000 req/minだったため、余裕を持って検出できますScopeDownStatementのAND条件で、JP/US・正規Bot(verified)・CHALLENGEトークン取得済みブラウザー・静的アセットへのリクエストを除外しています。評価対象は「JP/US以外・Bot未認定・トークン未取得・HTMLページへのアクセス」に絞られます。なおbot-control/anti-ddos/token系ラベルは、同一Web ACL内でこのルールより前に評価されるマネージドルール(Bot Control COMMONは常時稼働)で付与されたものを利用していますFallbackBehavior: MATCHにより、JA4が算出できない(TLSハンドシェイク情報がない等の)リクエストもカウント対象になります。通常のHTTPSブラウザーアクセスではJA4が算出されるため、算出不可のリクエストはボットの可能性が高いという判断です
効果の確認(ログ再集計による近似)
攻撃時のWAFログを主要条件(JP/US以外・token:absent・JA4単位の1分窓)で再集計し、閾値を超過するかを近似的に検証しました。
-- 攻撃中(03:26-03:39 UTC): scope-down条件に合致するJA4を1分窓で集計
filter httpRequest.country not in ['JP','US']
| filter labels.0.name like /token:absent/ or labels.1.name like /token:absent/ or labels.2.name like /token:absent/ or labels.3.name like /token:absent/ or labels.4.name like /token:absent/
| stats count(*) as cnt by ja4Fingerprint, bin(1m)
| filter cnt >= 100
| sort cnt desc
攻撃時のボットJA4は1分窓で最大4,000リクエスト以上に達しており、閾値100を大幅に超過します。一方、正常時(攻撃前24時間)に同条件で閾値を超えるJA4は存在しませんでした。
この施策の限界
JA4が偽装・分散されていたら、このルールでは対処できません。今回のボットがたまたまJA4を統一していただけであり、万能な対策ではありません。より巧妙なボットに対してはBot Control Targetedが有効です。TLSフィンガープリントだけでなくブラウザーの挙動検証まで行うため、より巧妙な偽装の検知力が高まります。ただしリクエスト単価が高いため、当サイトでは普段は未稼働としています。
施策2: ECS — オートスケール高速化(高解像度メトリクス)
WAFで止められなかった場合のフォールバックとして、オートスケールの検知速度を上げました。
問題
今回のインシデントでは、オートスケールの発動に7分かかりました。
| フェーズ | 所要時間 |
|---|---|
| 攻撃開始 → CPU超過検知 | 約7分(03:26 → 03:33) |
| スケール発動 → タスク起動完了 | 約11分(03:33 → 03:44) |
| 合計 | 約18分 |
対策
ECSサービスオートスケーリングの PredefinedMetricType を高解像度版に変更しました。メトリクス解像度が60秒から20秒になり、CPU使用率の変化をより早く検知できます。
| 設定項目 | Before | After |
|---|---|---|
| PredefinedMetricType | ECSServiceAverageCPUUtilization | ECSServiceAverageCPUUtilizationHighResolution |
| メトリクス解像度 | 60秒 | 20秒 |
# CPU-based Scaling Policy
ECSServiceScalingPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: !Sub '${AWS::StackName}-cpu-scaling-policy'
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref ECSServiceScalableTarget
TargetTrackingScalingPolicyConfiguration:
TargetValue: 50.0
ScaleInCooldown: 300
ScaleOutCooldown: 60
PredefinedMetricSpecification:
PredefinedMetricType: ECSServiceAverageCPUUtilizationHighResolution
期待効果
別記事で検証した実測値によると、スケールアウト検知が418秒から120秒に短縮されます(約3.5倍の高速化)。今回のケースに当てはめると、ボット開始から約2分で検知が期待でき、5xxの発生量を抑えられる見込みです。
タスク起動時間そのものは短縮されませんが、検知遅延は大幅に短縮されます。
まとめ
IP分散型ボットがWAFを通過し、ECSタスクが過負荷となった結果、13分間にわたり5xxが発生しました。WAFログを分析したところ、89のIPアドレスからのリクエストの大半が同一のJA4フィンガープリントを持っており、同一系統のクライアントによるアクセスである可能性が高いと判断しました。
対策として、JA4を集約キーにしたWAF Rate-basedルールを追加し、IP分散に関係なく同じJA4のスパイクを検知・緩和できるようにしました。さらに、ECSオートスケーリングの高解像度メトリクスを導入し、WAFを通過した場合のスケールアウト検知遅延も短縮しています。
JA4の偽装や分散には別の対策が必要ですが、今回観測されたボットパターンへの対応例としてご覧ください。








