本記事は、一定期間にわたり実施したスクレイピング(Bot)対策とセキュリティに関する一連のアーキテクチャ改修をまとめ、一つのストーリーとして再構成したものである。
具体的なエンドポイント、キー名、閾値、IPセグメント、シークレットキーなどの運用上の詳細はすべてマスキングし、設計のトレードオフにのみ焦点を当てる。
導入:コンテンツサイトのBot対策は何を防ぐべきか
私たちのサイトは多言語コンテンツプラットフォームであり、記事、フォーラム、マルチメディアリソース、各種専門データベースなどを網羅している。この種のサイトの価値はすべて公開されている読み取り可能なコンテンツにあるため、必然的に様々なクローラーの標的となる。検索エンジンの正規クローラー(歓迎)や、ソーシャルプレビュー用の取得(無害)がある一方で、AI学習用のクローラー(歓迎しない)や、コンテンツの大量不正コピー、APIエンドポイントの探索、ログインのブルートフォース攻撃を目的とした悪意のあるトラフィック(重点防御対象)も存在する。
Bot対策における最大の罠は、「防御」が容易に「正規ユーザーの阻害」へとスリップしてしまうことだ。過激すぎるルールは、防げるクローラーが限定的であるにもかかわらず、多くの生身のユーザーを誤検知(False Positive)で巻き込んでしまう。しかも、正規ユーザーの巻き添えはサイレントに起こる。彼らはクレームを入れることなく、ただ二度と訪問しなくなるだけだ。したがって、今回のリファクタリングの真の主軸は「いかに厳しくブロックするか」ではなく、**「大量スクレイピングを防ぎつつ、正規ユーザーの誤検知を限りなくゼロに近づけるか」**である。
1. 初期バージョンの問題点:振る舞い推論ベースのブロックと運用上のブラックホール
初期のBot対策は、**「振る舞い推論ベースのブロック」**というアプローチを取っていた。匿名の訪問者の行動をスコアリングし、「直帰(開いてすぐ閉じる、スクロールなし、ゼロインタラクション)」であれば特定のポイントを減点し、ダウンストリームで429(Too Many Requests)制限を受けた場合は別のポイントを減点。閾値に達すると段階的にアクセスをブロック(Ban)するというものだった。
このロジックの直感は間違っていなかったが、2つの致命的な問題があった。
- 誤検知の範囲が広く、説明責任が果たせない。 「直帰」というシグナル自体がノイズだらけである。人間がリンクを間違えて戻る、プリフェッチ、CGNAT下の同一出口IPによる連帯責任など、すべてが疑わしい行動としてカウントされてしまう。累積でブロックされると、誤検知されたユーザーにはAPIエラーしか見えず、何が起きたのか全く分からない。
- 運用上のブラックホールである。 ブロックの記録はキャッシュのカウンターに散在しており、管理者は「現在誰を、なぜブロックしているのか」を把握できず、誤検知されたユーザーを手動でホワイトリスト化する便利な画面もなかった。
そのため、最初の一歩はBot対策を「単なるキャッシュカウンターの山」から「運用可能なシステム」に変えることであった。
- ブロックのクールダウン状態をバックエンド間で共有するストレージに落とし込み、管理画面とパブリックサイトのバックエンドが同一のストレージを読み書きするようにし、クロスサービスの呼び出しをゼロにした。
- 「疑わしい検知」と「アクセス行動」の永続化ログを新設し、管理画面から検索、ドリルダウン、タイムラインの確認を可能にした。
- 管理画面に手動ブロック / 手動解除 / 履歴のドリルダウンの3つのアクション、およびIPごとに集計した「レートリミット統計」ページを追加。これにより、「現在誰がブロックされているか」が初めて可視化された。
このステップで防御力が上がったわけではないが、その後のすべての変更が検証可能、ロールバック可能、説明可能になった。これが今回の再構築を進めるための大前提である。
2. コアとなる方針転換:疑わしきは罰せず(Fail-open)
真の転換点は、システム全体の設計哲学を**「過検知の許容(Aggressive Blocking)」から「疑わしきは罰せず(Benefit of the Doubt / Fail-open)」**へと反転させたことだ。
新しい原則を一言で言えば、**「行動シグナルは『レピュテーション(信用スコア)』の調整にのみ使用し、単独でブロックの根拠にはしない。ブロックは決定的な証拠(Hard Evidence)にのみ基づく」**というものだ。
この背景には、全く独立した2つのラインを完全に切り離すという思想がある。
2.1. レピュテーションライン(ソフトレートリミット):疑わしいが不確実な対象
匿名IPにはそれぞれ「レピュテーションスコア」があり、中央値の初期値から始まり、加点・減点され、上限と下限がある。
- 加点(チャンスを十分に与える):ページ上での実際のインタラクション、意味のある滞在、スクロール読了などがあれば加点される。特に「実際のクリック」は信頼できるチャネルを経由して返され、人間の信用スコアの主な源泉となる。
- 減点(極めて抑制的に行う):「直帰(ゼロインタラクション)」という弱いシグナルでのみ減点され、その減点幅は加点幅よりもはるかに小さい。
スコアが特定の閾値を下回った場合、ブロックするのではなく、そのIPに段階的な「ソフトレートリミット」ウィンドウを適用する。スコアが低いほど制限時間は長くなり、数十分から数日に及ぶ。しかし、それは常に「レート制限」であり「ブロック」ではない。 ウィンドウの期限が切れると、レピュテーションスコアは自動的に初期値に「遅延リセット」される。一度ペナルティ期間を終えれば、再びクリーンな状態に戻るのだ。
このラインの精神は、**「すべての匿名訪問者をデフォルトで人間として扱う」**ことだ。生身の人間であれば、正常なブラウジング行動によってスコアはすぐに安全圏まで回復する。「開いてすぐ離脱する純粋な機械的アクセス」のみが、ゆっくりとソフトレートリミットに落ちていく。そして制限を受けたとしても、それは回復可能であり、説明可能である。
2.2. 証拠ライン(ハードバン):決定的な証拠のみ
真のブロック(ハードバン)は、決定的な証拠のみに留保される。自動化ブラウザの特有の挙動、ハニーポット(隠しトラップ)への接触、データセンターやクラウドASNからのブラウザレンダリングを伴わないアクセス、機密エンドポイントの探索、レートリミットのハード上限突破、そして管理者による手動ブロックである。このラインはレピュテーションスコアを一切考慮しない。スコアが高くても、決定的な証拠を踏めば即座にブロックされる。
ハードバンも同様に段階的なクールダウンであり、期限が切れれば自動的に回復し、永久BANは絶対に行わない。
この2つのラインは独立しており、互いに干渉しない。「疑わしきは罰せず + 2つの独立ライン」という構造が、システム全体の骨格である。
3. 多層防御の全体像
上記の骨格の上に、互いを補完し合う多層的な防御を実装した。各層は前の層の死角をカバーしており、どの層を単独で取り出しても不完全である。マスキング後の概要は以下の通りだ。
- エッジ層(CDN経由のオリジンアクセス強制): 本番環境ではすべてのトラフィックがCDNエッジを経由することを強制し、オリジンサーバーへの直接接続は拒否する。これにより、身元を隠してエッジのルールを迂回しようとする直接接続のクローラーを最前線で防ぎ、実IPに基づくロジックに信頼できるIPソースを提供する。(ここでの落とし穴:一律強制後、サーバー自身の内部SSRなども遠回りして遅延が発生したため、信頼できる内部読み取りのみを個別に許可して修正した。セキュリティ強化に伴う「身内の誤検知」は最もよくある暗礁である。)
- UAリスト層(ホットリロード可能なリスト): 最外層での迅速な判定。確認済みの悪意ある/AI学習用クローラーのUAは直接拒否し、確認済みの信頼できるクローラー(正引き/逆引きDNSで双方向確認済み)は直接許可する。
- レピュテーション + 証拠層: 前述の2つの独立したラインであり、リクエスト経路の主体である。
- ハニーポット層(スクリプト実行型クローラー対策): 人間には見えず、「DOMを解析してリンクをたどる/フォームを送信する」クローラーだけが触れる隠しトラップをページに埋め込む。リクエストがトラップ由来のトークンを持ち帰った場合、それは確度の高いクローラーである。
- オフラインログの突合層(スクリプト非実行型クローラー対策): 前述の層はクライアントでのスクリプト実行に依存しているが、HTTPで純粋にHTML本文のみを取得するクローラーには盲点となる。そこで、フロントのアクセスログとサイト内の実際の行動ログを定期的に突き合わせるオフラインバッチ処理を追加した。「大量のページを取得しているが、フロントエンドリソースを一切ロードせず、人間の行動シグナルも全く発生しない」IPを抽出し、データセンターIPには低い閾値を適用し、純粋なHTMLクレイピングはページレベルのブロックリストに落とし込む。
- 機密エンドポイントの探索ブロック: ログインなど、特定のメソッド(POSTなど)でしかアクセスされないエンドポイントを予期しない方法で探索するアクセスがあった場合、スキャナーと断定し、即座に長めの固定クールダウンを課す。
4. ハードコーディングから運用可能な仕組みへ
初期は、ホワイトリストとブラックリストがコード内に定数としてハードコーディングされていた。クローラーを追加するたびにコード修正とデプロイが必要だったが、これでは全く追いつかない。
そこで、リスト全体をデータベースに移行し、管理画面を整備した。
- 管理画面での保守とホットリロード: 管理者がリストを更新すると、パブリック側のバックエンドがバージョン番号のポーリングを通じて検知し、秒単位でホットリロードする。デプロイ不要・再起動不要で対策を追加可能になった。ストレージ障害時は既存のリストを維持するFail-openを適用し、システムの一時的な揺らぎで防御がゼロになるのを防ぐ。
- 3種類のセマンティクスの明示的区別: リストは「許可 / ブロック」だけでなく、中間に**「コンテンツのみ許可」**というカテゴリを設けた。これは、クラウド上で動作し専用の逆引きDNSを持たないが、公開コンテンツの読み取りは許可したいクローラー(一部のトラフィック誘導型AIクローラーやソーシャルプレビューなど)を対象とする。これらは公開ページは読めるが、API層でのレートリミット免除は一切受けられない。
- セルフミスを防ぐ強力なバリデーション: 許可ルールのドメインサフィックスにはAWSなどのパブリック汎用ドメインの入力を禁止し、UA識別子も "chrome" などの汎用文字列を禁止する。これにより、運用担当者のヒューマンエラーによる大事故を防ぐ。
5. 優先順位の哲学:手動 > 自動、許可 > ブロック
リストとブロック条件が増えると、ルール同士が競合する。テスト環境での検証を通じ、2つの**普遍的な優先順位(Invariants)**を整理した。
- 手動ブロック > 自動免除: 信頼できるクローラーリストの優先順位は高いが、管理者による手動ブロックだけは上書きできない。これに関連し、書き込み側の脆弱性も塞いだ。テストにおいて、管理者が手動でブロックしたIPがその後自動メカニズムをトリガーし、手動ブロックの期間が自動ブロックの短い期間に密かに上書きされる事象が再現された。すべての自動書き込みの前に「すでに手動ブロックされているか」をチェックするように修正した。
- 許可 > ブロック(部分文字列による誤検知の防止): ブラックリストはUAの部分文字列マッチングで機能するが、広範すぎる文字列が混入すると、本来許可すべきクローラーを誤ってブロックする可能性がある。そのため、**「先に許可リストをチェックし、次にブロックリストをチェックする」**順序に変更した。許可リストにヒットすれば直ちにブロック対象外とする。
共通する精神は、**「明示的な人間の意図は暗黙の自動ルールよりも優先される。そしてユーザー体験に影響を与える『許可』の判断においては保守的であるべき」**ということだ。
6. 制限を受けたユーザーに「理由」を知らせる
Bot対策の最もユーザーフレンドリーでない点は、人間に対してサイレントであることだ。誤検知されたユーザーはAPIエラーを見るだけで、何の説明も受けない。
そこで、システム通知機能を実装した。訪問者がソフトレートリミットに入った場合、サイト内のメニュー下部に「なぜ制限を受けているのか、およそどれくらいで回復するのか」を明記したバナー通知を自動的に表示する。
これにより、Bot対策が「ブラックボックスの罰則」から「説明可能でフィードバックのあるメカニズム」へと変わった。誤検知されてもきちんと説明を受けたユーザーと、無言でアクセス遮断されたユーザーとでは、雲泥の差がある。
7. データ保護:段階的な表示制限とペイウォール
Bot対策は「大量の不正コピーを防ぐ」ことだが、より根本的なアプローチは、**「価値のあるデータをアカウント層の背後に隠す」**ことだ。匿名IPのシグナルは弱い一方で、アカウントは強力なシグナル(追跡可能、レート制限可能)となる。
- 段階的な表示制限(機密フィールドのゲート): 一部のコアフィールド(深い関連情報、コア指標データなど)は、未ログインの訪問者に対して直接マスキングし、ログインを誘導する。大量にスクレイピングされても、取得されるのはパブリックな情報だけになる。
- ペイウォールによる登録誘導: 高頻度でアクセスされる価値の高い詳細ページに対しては、匿名訪問者に1日あたりの低い無料クオータを設定する。上限を超えた場合、乱暴にエラーを返すのではなく、会員登録へ誘導する。
これらは、技術的な「拒否」をプロダクト的な「誘導」に置き換え、匿名のトラフィックをアカウントシステムに穏やかに収束させるアプローチである。
8. 管理ドメインの収束:IPゲートからクレデンシャルゲートへ
管理画面の初期のアクセス制御はIPセグメントに基づくゲートコントロールであった。しかし、管理者の出口IPは変わるし、IP自体が推測や偽装可能である。
そこで、管理ドメインのゲートコントロール全体を**「統一されたクレデンシャルゲート」**に置き換えた。「有効なクレデンシャル(Cookie)を持っているか」の1点のみで判定され、送信元IPは関係ない。
アクセス制御の判断基準が**「どこから来たか」から「誰であるか」に変わった**ことで、より安定し、「ゼロトラスト」の直感にも合致するようになった。
9. 誠実な境界線
いかなる防御にも限界がある。万能を装うよりも、限界を明確に記すことのほうが重要だ。
- 捕捉できるもの: ページスクリプトを実行するクローラー。純粋なHTTPで本文だけを大量に取得するクローラー(オフライン突合で対応)。
- 捕捉できないもの: 自サイト専用にチューニングされ、トラップを識別してスキップできる標的型クローラー。これに対するクライアント側の防御は無効であり、原理的な限界である。
- フォールバック原則(Fail-open): 依存するストレージが利用不能になった場合、経路全体を一律で**許可(Fail-open)する。システムの一時的な揺らぎによって、本物のユーザーを締め出してはならない。「1つのクローラーを見逃したとしても、1人のユーザーを誤検知してはならない」**というのがデフォルトの姿勢である。
10. エンジニアリング手法:テスト可能なアーキテクチャ
Bot対策のコードは、正しさを目視で判断できないという厄介な特性がある。免除の順序を1行変えただけで、本物のユーザーを一斉にブロックしてしまう可能性がある。
そこで、ブラックボックス・リグレッションテスト環境を並行して構築した。各経路(ソフト制限、ハードバン、通知、優先順位など)をモジュール化されたテストスイートに分割した。前述の優先順位のバグも、このテスト環境で再現・特定された。
「システムの正しさを直感で検証できない場合、それを自動的かつ集中的に検証できる仕組みを作らなければならない。」
終わりに
振り返ってみると、この一連の改修で本当に変わったのは「アルゴリズム」ではなく、**「Bot対策に向き合うスタンス」**である。
- **「過検知の許容」から「疑わしきは罰せず」**へ: デフォルトですべての匿名訪問者を人間として扱う。
- **「キャッシュカウンターの山」から「運用可能、説明可能、テスト可能」**なシステムへ。
- **「技術的な拒否」から「プロダクト的な誘導」**へ: ログイン誘導やバナー通知を用いて、冷たい拒否を穏やかなアカウント収束へと変える。
- **「どこから来たか」から「誰であるか」**へ: アクセス制御を変動するIPから、明示的なクレデンシャルへ移行する。
Bot対策の究極の難点は「防ぐこと」ではない。十分に過激なルールを設定すれば防ぐことはできる。本当に難しいのは、クローラーを防ぐと同時に、一人たりとも生身のユーザーを不快にさせないことである。今回の決定のほとんどは、この境界線上での度重なるトレードオフの結果である。