IP分散型ボットで13分間の5xxが発生したため、WAFのJA4 Rate-basedルールとECSオートスケール高速化で対処してみた

IP分散型ボットで13分間の5xxが発生したため、WAFのJA4 Rate-basedルールとECSオートスケール高速化で対処してみた

IP分散型ボットによるスパイクで5xxが13分間発生しました。WAFログからJA4フィンガープリントの一致を発見し、JA4集約キーのRate-basedルールで再発防止しています。調査手順とルール設計の判断過程を記録します。
2026.06.30

はじめに

2026年6月、当サイト(dev.classmethod.jp)で5xxエラーが13分間にわたり発生しました。原因はIP分散型ボットによるリクエストスパイクです。89のIPアドレスから毎分約4,000リクエストが流入し、ECS全タスクが処理能力を超えてダウンしました。

WAFログを分析して攻撃元を特定し、再発防止策として以下の2つを実施しています。

  1. WAF: JA4フィンガープリントを集約キーにしたRate-basedルールの追加
  2. 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_KEYSJA4Fingerprint の組み合わせで、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フィンガープリントだけでなくブラウザーの挙動検証まで行うため、より巧妙な偽装の検知力が高まります。ただしリクエスト単価が高いため、当サイトでは普段は未稼働としています。

https://dev.classmethod.jp/articles/aws-waf-scrapling-bot-detection/

施策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の発生量を抑えられる見込みです。

タスク起動時間そのものは短縮されませんが、検知遅延は大幅に短縮されます。

https://dev.classmethod.jp/articles/ecs-high-resolution-metrics-faster-autoscaling/

まとめ

IP分散型ボットがWAFを通過し、ECSタスクが過負荷となった結果、13分間にわたり5xxが発生しました。WAFログを分析したところ、89のIPアドレスからのリクエストの大半が同一のJA4フィンガープリントを持っており、同一系統のクライアントによるアクセスである可能性が高いと判断しました。

対策として、JA4を集約キーにしたWAF Rate-basedルールを追加し、IP分散に関係なく同じJA4のスパイクを検知・緩和できるようにしました。さらに、ECSオートスケーリングの高解像度メトリクスを導入し、WAFを通過した場合のスケールアウト検知遅延も短縮しています。

JA4の偽装や分散には別の対策が必要ですが、今回観測されたボットパターンへの対応例としてご覧ください。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事