本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント等と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。

TL;DR

  • 運営する文化アーカイブ系サイトに、ボットスクレイピングが発生
  • 主要発信源は AWS Singapore (ap-southeast-1) から始まり、対処後に US、CN へと pivot
  • rate-limit では捕捉できず、IP block → Geo block → JA3 fingerprint block → UA block と段階的に対処
  • 同種攻撃に対する再現可能な手順とコスト感をまとめます

発端: GA4 の異常値で発覚

複数の文化アーカイブ系公開サイトを運営しています。GA4 でプロパティを棚卸ししていた時に、メインサイトの数値に違和感を覚えました。

PV ≒ Sessions ≒ Users という階段が完全に潰れた数値です。通常 1 人が複数ページを見るので PV > Sessions > Users の階段になるはずで、人間のトラフィックでは考えにくい比率でした。

自作スクリプトで多軸プロファイリング

GA4 Data API で多軸分析するスクリプトを書いて分析しました。要点だけ抜粋:

PV/session = 約 1.0    (人間なら 2〜5、1.0 付近はボット)
sessions/user = 約 1.0 (人間なら 1.3〜2+、1.0 付近は Cookie 無し)
平均セッション秒数 = 一桁秒  (< 5 秒はボット疑い)
bounce rate = 極端に高い (90%後半)

国別の傾向:

Singapore: 圧倒的多数  (全体の 9 割超)
Vietnam, China, US: 少量
Japan:    ごく少量  ← 正規ユーザーはこの辺

ブラウザ / OS / デバイス / リファラ:

Chrome on Windows: ほぼ 100%
desktop:           ほぼ 100%
(direct)/(none):   ほぼ 100%   ← referrer 無し
時間帯:             24h フラット (昼夜の概念なし)

ランディングページは書籍詳細パスに偏っていて、書籍 DB を全件巡回されているように見えます。

総合すると:

  • Chrome on Windows を偽装 (おそらく Puppeteer / Playwright headless)
  • Cookie を保持せず毎回新規 user として計上
  • 24 時間休まず巡回
  • Singapore 集中 → クラウドホスト由来

人間のトラフィックではなく、自動化されたクラウド由来のスクレイピングが大量に発生している、というところまでは確定できる挙動でした。

初動: AWS 側の現状確認

CloudFront と WAF の構成を確認します:

aws cloudfront list-distributions \
  --query 'DistributionList.Items[].{Id:Id,Domain:DomainName,WebACL:WebACLId}' \
  --output json

共有 Web ACL が複数 distribution に適用されている構成で、Managed Rules と rate-limit による最低限の防御は稼働しています。WAF ログ (CloudWatch Logs) と CloudFront 標準アクセスログ (S3) も有効化されているため、攻撃の進行履歴を遡って分析できる状態です:

Rule 1: AWSManagedRulesCommonRuleSet
Rule 2: AWSManagedRulesKnownBadInputsRuleSet
Rule 3: AWSManagedRulesAmazonIpReputationList
Rule 4: rate-limit-per-ip
DefaultAction: Allow

ただし、この標準構成だけでは今回の分散スクレイピング型トラフィックは捕捉しきれていなかったので、ログを起点に攻撃の正体を分析していきます。

WAF ログで攻撃の正体を可視化

WAF ログの 1 件を見ると、JA3 fingerprint まで記録されているのが分かります:

{
  "action": "ALLOW",
  "terminatingRuleId": "Default_Action",
  "rateBasedRuleList": [
    {"rateBasedRuleName":"rate-limit-per-ip","limitKey":"IP",
     "limitValue":"<attacker-ip>"}
  ],
  "httpRequest": {
    "clientIp": "<attacker-ip>",
    "country": "SG",
    "uri": "/_nuxt/builds/meta/...",
    "host": "site-a.example"
  },
  "ja3Fingerprint": "<ja3-hash>",
  "ja4Fingerprint": "<ja4-hash>"
}

country: "SG" がデフォルトで付くこと、JA3/JA4 が標準で記録されることがポイントです。

CloudWatch Logs Insights で集計:

fields httpRequest.country as country, action
| stats count() as n by country, action
| sort n desc

過去 30 分の結果は SG が突出して多く、しかも全件 ALLOW でした。さらに SG のユニーク IP 数を集計すると数千 IP / 30 分。1 IP あたりは閾値の数% 程度しか来ておらず、極めて分散したスクレイピングであることがわかります。

SG 攻撃 IP の正体

Top IP のレンジを AWS 公式 IP ranges (https://ip-ranges.amazonaws.com/ip-ranges.json) で照合すると、AWS ap-southeast-1 の EC2 範囲に収まりました。攻撃者は AWS Singapore で多数 EC2 インスタンスを立てて分散スクレイピングしているようです。

対処1: rate-limit ではヒットしない

最初に確認すべきは、稼働中の rate-limit-per-ip がこの攻撃を捕捉できているかどうかです。WAF ログを集計したところ、1 IP あたりは閾値の数 % 以下しか来ておらず、rate-limit には引っかかっていませんでした。閾値を強気に下げる選択肢はありますが、SPARQL クライアントや企業 NAT 経由の正規利用に誤発火するリスクを考えると現実的ではなく、原理的な限界として受け入れて別アプローチを検討します。

対処2: IP レンジ Block

AWS SG のレンジを IPset に登録し、Block ルールを追加します:

aws wafv2 create-ip-set --name block-aws-sg-range --scope CLOUDFRONT \
  --ip-address-version IPV4 --addresses <aws-sg-cidr>

数分後の効果測定:

  • Block は出始めた
  • しかし SG ALLOW は依然として大量に通っている

Top IP を再分析すると、攻撃者は隣接する別の /16 にすでに pivot していました。これらを IPset に追加しても、また別の /16 が出てきます。連続する /16 をスライドしている挙動で、IP ベースのモグラ叩きでは追いつかないと判断しました。

対処3: Geo block で根本停止

サイトは日本語の文化アーカイブで、Singapore 在住の正規ユーザーは統計的にごく少数です。副作用が小さければ Geo block が最もクリーンな選択肢になります。

Rule: geo-block-sg
Priority: 1
Action: Block
Statement:
  GeoMatchStatement:
    CountryCodes: [SG]

伝搬完了後、SG からのアクセスは概ね遮断されました。正規 JP ユーザーへの影響は確認されず、モグラ叩きの状態は収束しています。

同時に IPset 経由の Block は使命を終えたので、Geo block と冗長になることから削除して構成を簡素化しました。

攻撃の正体を分析する

止血したので、攻撃プロファイルを腰を据えて分析します。CloudWatch Logs Insights で過去 60 分の Top URI を集計:

fields httpRequest.country as country, httpRequest.uri as uri
| filter country = "SG"
| stats count() as n by uri
| sort n desc
| limit 20

結果、SG からのアクセス上位は:

  • /sparql (Linked Data SPARQL endpoint)
  • /snorql/ (SPARQL UI)
  • /app/prx/...sparql (proxy 経由の他 SPARQL endpoint への中継)
  • 多言語版コンテンツの systematic 巡回

狙いは公開 SPARQL endpoint と Linked Data リソースに集中していました。

  • HTTP method: GET と HEAD のみ (純粋に読み取り)
  • JA3 fingerprint: 特定の単一ハッシュが支配的 (= 同一の自動化ツール)
  • Managed Rule (CommonRuleSet, KnownBadInputs) のマッチ: 0 件

つまり脆弱性を突いているわけではなく、公開されている SPARQL endpoint と Linked Data を systematic scraping している、という挙動です。これは AWS WAF の視点からすると「攻撃ではない、合法な公開リソースへの大量アクセス」とも言えるグレーゾーンですが、サーバ負荷とデータの大量持ち出しという意味で実害が出ます。

ピボット: US トラフィックの精査

対応後、ログを定期的に眺めていると、US からの Allowed Requests が JP の数十倍に達していることに気付きました。日本語コンテンツへのアクセス比率としては不自然なので、内訳を調査します:

  • US ALLOW が JP の数十倍
  • /sparql は依然多数のヒット
  • 同じ JA3 が出現 (SG 攻撃と完全一致)

攻撃ツールが IP を SG から US に変えて再来していると見られます。

ただし US は SG と質が違い、正規 bot が混在しています:

IP/Range正体
40.77.x.xBingbot (Microsoft)
52.167.x.xMicrosoft Azure (Bingbot)
66.249.x.xGooglebot
meta-externalagent UAMeta クローラ
Amazonbot UAAmazon クローラ
(PSINet/Cogent の単一 IP)異常な集中アクセス
(WebMeUp/BLEXBot)同上

正規 bot を巻き込まずに攻撃だけ止める設計が必要になります。

対処4: 個別 IP + JA3 fingerprint Block

個別 IP Block (アクセス集中の目立つ少数のみ)

アクセス集中が顕著な単一 IP だけを IPset に登録します:

aws wafv2 create-ip-set --name block-aggressive-scrapers \
  --scope CLOUDFRONT --ip-address-version IPV4 \
  --addresses <ip1>/32 <ip2>/32 <ip3>/32

JA3 fingerprint Block

JA3 は TLS Client Hello のフィンガープリント (TLS version / cipher suites / extensions / elliptic curves / EC point formats の MD5) です。User-Agent や IP を変えても、TLS スタックを変えない限り同じ JA3 が出ます。

SG 攻撃と US 攻撃で一致した JA3 を Block します:

{
  "Name": "block-attacker-ja3",
  "Priority": 6,
  "Statement": {
    "ByteMatchStatement": {
      "SearchString": "<ja3-hash>",
      "FieldToMatch": {
        "JA3Fingerprint": {"FallbackBehavior": "NO_MATCH"}
      },
      "TextTransformations": [{"Priority": 0, "Type": "NONE"}],
      "PositionalConstraint": "EXACTLY"
    }
  },
  "Action": {"Block": {}}
}

IP を変えても TLS スタックを変えない限り効き続ける、比較的安定したシグネチャ Block です。コストはルール 1 本分の月額のみで済みます。

対処5: UA Block ルール (LLM クローラー一掃)

AI 学習用クローラと SEO スクレイパーを一括で Block します。まず RegexPatternSet を作成します (case-insensitive で複数 UA を検出):

aws wafv2 create-regex-pattern-set --name bot-ua-patterns --scope CLOUDFRONT \
  --regular-expression-list \
    'RegexString=(?i)GPTBot' \
    'RegexString=(?i)CCBot' \
    'RegexString=(?i)ClaudeBot' \
    'RegexString=(?i)anthropic-ai' \
    'RegexString=(?i)Bytespider' \
    'RegexString=(?i)(PerplexityBot|OAI-SearchBot|ImagesiftBot|Diffbot|DataForSeoBot)' \
    'RegexString=(?i)(SemrushBot|AhrefsBot|MJ12bot|DotBot|BLEXBot)'

User-Agent ヘッダにこれらが含まれるリクエストを Block するルールを追加します。

ポイントは AI 学習 / SEO 解析用 bot に対象を限定している点です。Googlebot / Bingbot / Applebot (Siri 検索) は除外しているため SEO への影響は出ない設計にしました。Meta-ExternalAgentAmazonbot は二面性 (FB OGP / Alexa) があるので、意図的に除外しています。

これで Bot Control を入れずに、最小コストで declared bot を一掃できました。

robots.txt の配置 (アプリケーション層)

WAF と並行して、SPARQL endpoint サーバ (nginx) に robots.txt を配置しました。

# This is a SPARQL endpoint server, not intended for crawling.

# --- AI training crawlers: total denial ---
User-agent: GPTBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: Bytespider
Disallow: /
... (anthropic-ai, OAI-SearchBot, etc.)

# --- SEO scanners ---
User-agent: SemrushBot
Disallow: /
... (AhrefsBot, MJ12bot, DotBot, BLEXBot, DataForSeoBot)

# --- All other bots: avoid SPARQL endpoint and URI-deref paths ---
User-agent: *
Disallow: /sparql
Disallow: /snorql/
Disallow: /data/
Disallow: /entity/

robots.txt は強制力ゼロですが:

  • 善意の crawler は読んで撤退する → WAF まで来ない (= 課金されない)
  • robots.txt を無視する crawler は WAF UA Block で遮断
  • UA を偽装する crawler は JA3 / IPset / Geo block で対処

という三段構えの上層として位置付けています。

CloudWatch アラームによる再発検知

「鳴ったら対応する」運用にするため、3 つのアラーム + SNS Topic を設定:

AlarmMetricThreshold (例)
waf-blocked-attack-sources-spikeBlockedRequests (主要攻撃元国の合計)スパイク検知レベル
waf-sg-allowed-spikeAllowedRequests (Geo block 対象国)バイパス検知レベル
waf-non-jp-allowed-spikeAllowedRequests (非 JP 合計)pivot 検知レベル

通知先は SNS Topic → Email。

Metric Math の落とし穴

最初は SEARCH('{AWS/WAFV2,Rule,WebACL,Country} ...') で動的に集計しようとしましたが、CloudWatch アラームは SEARCH 式をサポートしていないようでした:

ValidationError: SEARCH is not supported on Metric Alarms.

回避策として、対象国を明示列挙して FILL(m_sg,0) + FILL(m_cn,0) + FILL(m_vn,0) + FILL(m_hk,0) の Metric Math を使用。FILL(m_xx, 0) は「データ欠損時に 0 扱い」なので、その国からのアクセスがほぼ無くてもアラーム計算が正しく行えます。

アラーム閾値とベースラインの関係

通知運用は閾値が意味を持つことが前提です。ベースラインが閾値ぎりぎりだと、新規異常を検知しにくくなります。

US トラフィック調査の途中で、非 JP Allow がベースラインで閾値の数割に達している状態になり、閾値を上げれば誤発火は減るが現状の攻撃が見えなくなる、というジレンマに直面しました。最終的には IP + JA3 + UA Block でベースラインを下げ、閾値はそのまま維持する判断としました。

rate-limit の閾値見直し

最後に rate-limit-per-ip の閾値を見直しました:

  • 元の閾値だと API 利用 (特に SPARQL クライアント) や企業 NAT 経由で誤発火しうる
  • メイン防御は Geo block / JA3 / IPset に移ったため、rate-limit は暴走 IP に対する最終安全網として緩めるのが合理的になります

閾値を 5 倍に引き上げ、人間と通常の API 利用では届かないが暴走には引っかかる水準に調整しました。SPARQL ヘビーユーザーや企業 NAT 経由の利用も保護されます。

CommonRuleSet オーバーライド構成の再点検

合わせて AWS Managed Rules の CommonRuleSet のオーバーライド構成も見直しました。一般 web 攻撃 (SQLi / XSS / LFI / RCE 等) を網羅的に検査する防御層で、サイトの性質に合わせて継続的にチューニングする必要があります。

オーバーライドの設計

CommonRuleSet には数十のサブルールが含まれていて、コンテンツの性質によって誤検知 (False Positive) が出やすいものがあります。WAF ログで実マッチを集計し、誤検知が出やすい個別ルールだけを Count に固定する方針としました。

Rule: AWSManagedRulesCommonRuleSet
OverrideAction: None  # ← グループ全体は Block
Statement:
  ManagedRuleGroupStatement:
    VendorName: AWS
    Name: AWSManagedRulesCommonRuleSet
    RuleActionOverrides:
      - Name: SizeRestrictions_BODY        # 8KB 超の POST body
        ActionToUse: Count
      - Name: NoUserAgent_HEADER           # UA 無しのバックエンド監視等
        ActionToUse: Count
      - Name: CrossSiteScripting_BODY      # 正規コンテンツ内 <script>
        ActionToUse: Count
      - Name: GenericLFI_QueryArguments    # URL に '..' を含む正規パス
        ActionToUse: Count
      - Name: EC2MetaDataSSRF_BODY         # API が IP 風文字列を扱う
        ActionToUse: Count

RuleActionOverrides を使うと、グループ全体を Block にしながら個別ルールだけ Count に固定できます。これで:

  • WAF ログでマッチが多かった UserAgent_BadBots_HEADER (declared SEO scraper) → Block (UA Block と二重防御)
  • RestrictedExtensions_URIPATH (.env.bak 等の探索) → Block
  • その他の SQL / XSS / LFI / RCE 系サブルール → Block
  • 5 つの誤検知リスク高ルール → Count (継続観測)

という配分になりました。今後は nonTerminatingMatchingRules で Count に固定したルールの状況を継続監視し、必要なら個別に Block へ昇格させるサイクルとなります。

翌日: CN への pivot

IP + JA3 + UA Block、rate-limit 見直し、CommonRuleSet 再点検まで終えて一段落…と思った翌日、ログを眺めていたら CN からのアクセスが急増していることに気付きました。SG/US の時よりさらに 3 倍ほどの総量です。

CN 攻撃のプロファイル

これまでとはやや性質が違う:

観測項目SG/US 時CN 時
ターゲット/sparql + 多言語版コンテンツNuxt フロント全体 + /sparql
URI 上位/sparql, /snorql/, /en/*/favicon.ico, /_nuxt/*.js (全 chunk), /sparql
IP 分布連続する /16 に集中 (AWS SG レンジ)/16 単位でも集中なし、広域分散
JA3単一の JA3 が支配的支配的な JA3 なし (多数のフィンガープリント)
推定挙動軽量 HTTP クライアント (curl/Python)ヘッドレスブラウザ駆動 (Puppeteer/Playwright で実 Chrome 起動)

Nuxt の JS チャンクを毎リクエストで全 DL している点と、JA3 が多様である点が大きく違います。これは次のように解釈できます:

  • ヘッドレスブラウザ駆動 = TLS スタックがブラウザネイティブ
  • ブラウザインスタンスを大量起動 = 実 Chrome の挙動を真似
  • = JA3 Block では効率が悪い

なぜ Geo block CN が正解か

候補と判断:

選択肢評価
Geo block CN◎ 即時遮断、副作用は CN 在住の正規ユーザーのみ
/sparql 限定 CN block○ 折衷案だが、Nuxt フロントも狙われている時点で限定意味薄い
Bot Control 導入○ JA3 多様性に対応可。ただしコスト高
IP / JA3 ベース追加✕ 分散ぶりから現実的でない

サイトは日本書籍 / 文化資料の DB で、CN からの正規ユーザーは統計的にごく少数です。SG の時と同じ論理で、Geo block CN が最適と判断しました。

実装

WebACL に geo-block-cn を Priority 2 で追加 (SG との並びを維持):

Rule: geo-block-cn
Priority: 2
Action: Block
Statement:
  GeoMatchStatement:
    CountryCodes: [CN]

伝搬完了後、CN からのアクセスは概ね遮断されました (BLOCK 99% 超 / ALLOW ほぼゼロ)。総トラフィック量も 6 倍以上削減され、SG 攻撃の時と同様に止まっています。

ピボット対応からの観察

  • 同じ手口が別の国から繰り返されるパターン (SG → US → CN と pivot)
  • JA3 fingerprint Block は攻撃ツールが固定の時に有効で、ヘッドレス実ブラウザ系には効きにくい
  • Geo block はツールの違いを問わず一律遮断できるため汎用性が高い
  • 「副作用が小さいなら Geo block」というしきい値判断は、SG / CN いずれも同じ結論になりました

最終構成

Web ACL shared-cloudfront-acl

PriRuleAction
1geo-block-sgBlock
2geo-block-cnBlock
5block-aggressive-scrapers (個別 IP)Block
6block-attacker-ja3Block
7block-bot-uas (RegexPatternSet)Block
10AWSManagedRulesCommonRuleSetBlock (誤検知が起こりやすい一部サブルールは Override で Count)
20AWSManagedRulesKnownBadInputsRuleSetBlock
30AWSManagedRulesAmazonIpReputationListBlock
100rate-limit-per-ipBlock

月額コストの考え方

AWS WAFv2 の料金体系 (公開情報):

  • Web ACL: $5.00 / 月 (基本料、ルール数に依存しない)
  • ルール / ルールグループ: $1.00 / 月 (各ルールごと)
  • リクエスト処理: $0.60 / 100 万リクエスト

これに WAF ログ ingestion ($0.50/GB) と CloudWatch アラーム / SNS が加算されます。

今回の構成は基本料が二桁ドル/月、リクエスト処理は規模に比例する従量です。Bot Control の導入も検討しましたが、Common Bot Control ($10/月 + $1/100万 reqs) は header / UA ベースが中心で JA3 多様性への対応は限定的、JA3 の多様性に踏み込むなら Targeted Bot Control ($10/100万 reqs) が必要となり料金は桁違いになります。現状は declared bot は UA で、unknown bot は JA3 で概ね捕まえられているため、見送りました。中長期で Bot Control が必要になるかどうかは観察次第です。

学び

1. GA4 は「不自然さ」の最初の入口

WAF ダッシュボードだけを見ていると「そういうもの」と感じてしまいがちですが、GA4 の PV / Session / User 比率を定期的に確認するだけでもボット汚染を検知できる場面は多いと感じました。今回は PV / Session ≒ 1.0 がわかりやすいシグナルでした。

2. ログ運用が攻撃分析の鍵

CloudFront 標準アクセスログ (S3) と WAF ログ (CloudWatch Logs) は、CloudWatch Logs Insights で過去 60 分・24 時間の集計が即座にできる粒度・保持期間で運用しておくと、いざという時に役立ちます。country / JA3 / URI / IP の多軸集計が走らないと、攻撃の正体特定や pivot 検知が後手に回りがちです。

3. IP レンジによる Block は分散攻撃に弱い

今回は連続する /16 に集中していたため一時的には効きましたが、residential proxy 経由の攻撃には原理的に追いつけません。IP ベースは:

  • 緊急止血用 (即効性) として使う
  • 永続防御には Geo block / JA3 / UA / Bot Control を使う

と棲み分けるのが現実的でした。IP リストを毎週手動更新する運用は維持コストが見合いにくいと感じます。

4. JA3 fingerprint は IP より安定したシグネチャ

JA3 (および JA4) は TLS Client Hello のフィンガープリントで、攻撃ツールを変えない限り IP / UA を変えても同じ JA3 になります。比較的安定した Block シグナルとして使えますし、WAF が JA3 / JA4 を標準で記録してくれるのが地味に便利でした。

5. Geo block の正当化は「副作用の小ささ」で計る

Geo block は強力ですが、副作用 (正規ユーザーの巻き込み) も大きく、サイトの性質と地理的ユーザー分布を踏まえて判断する必要があります。今回は日本語コンテンツに対する非日本からの大量アクセスがほぼ全量ボットだったため、副作用は許容範囲と判断しました。

6. アラーム閾値はベースラインで意味が決まる

「鳴ったら対応」運用は、ベースラインが閾値から十分に離れていてはじめて機能します。攻撃トラフィックが既にベースラインに溶け込んでいる場合は、まず Block でベースラインを下げてから閾値を設定するのが現実的でした。閾値を引き上げる方向で対応すると、見逃しのリスクが増えやすいと感じます。

7. SPARQL endpoint は境界防御の発想で作られていない

公開 SPARQL endpoint は export を前提とした仕様で、WAF 越しに守る前提で設計されているわけではありません。今回の対応を踏まえると、今後立ち上げる場合には以下を最低限のセットとして用意するのが妥当そうです:

  • Application 層 (nginx limit_req_zone) で /sparql の rate-limit
  • SPARQL サーバ側で query complexity / time / result-size limit
  • 認証付き bulk download 窓口を別ルートで提供
  • robots.txt + WAF UA Block を最初から設定

次に予定していること

  • 非 JP 圏 (DE / FR / IN 等) からの AllowedRequests スパイク用の追加アラーム
  • WAF 未適用 distribution の Web ACL アタッチ

攻撃と防御はイタチごっこの面がありますが、観測 (ログ) → 仮説 (集計) → 検証 (Block) → 監視 (アラーム) のサイクルを回す枠組みが整っていれば、次の波にも比較的早めに気付いて対処できます。現時点では同じ枠組みで後続のトラフィックも継続的に観察しています。


動画版(生成AIによる自動生成): この記事の内容をずんだもん×四国めたんの掛け合いで解説しています。自動生成のため、内容に誤りがある可能性があります。正確な情報は記事本文をご参照ください。