- CDNなしで HTTPプロトコル/IP帯域/クライアント信号/TCP特性/TLSフィンガープリント/コンテンツ圧縮を組み合わせ、実装や設定が不十分なボットの大半をブロックする方法をまとめている
- すべての方法は選択的に調整する必要があり、1〜3年分のアクセスログを先に分析しないと、VPNユーザー/検索エンジン/CDN/学校/図書館/特定言語のユーザーまで一緒にブロックしてしまう可能性がある
- HTTP/1.1クライアントとデータセンターのAS/CIDR帯域をブロックすると多くのボットを排除できるが、GoogleBotをはじめとする検索エンジンや正常なデータセンター利用者も除外される可能性がある
- Nginxのヘッダー検査とnftablesのTCPウィンドウサイズ/MSS/TTLフィルターは単純なクローラーやスキャナーを減らせるが、LTE/VPN/Windowsのような正常な環境で誤検知が発生する可能性がある
- 長期的には JA4 TLSフィンガープリント検出とBrotli専用レスポンスを代替案として提示しているが、完全な遮断は保証せず、収益を生むサービスでは使わないよう警告している
ブロック範囲と適用の前提
- ブロック対象を一部のボット/大半のボット/すべてのボットのどれにするかを先に決める必要がある
- ここで扱う方法は、高度な自動化ツール全体ではなく、実装や設定が不十分なボットの大半を比較的単純にブロックすることに重点を置いている
- 各方法ごとに、正常なユーザーと検索エンジンをブロックするリスクも併記している
- 2026年7月26日にHacker Newsで共有された後、ブロック機能の大半をブログから別のデモサイトへ移し、読者が方法を読んだ後に自分でアクセスを試みるパズル形式に変更することにした
- すべての設定は選択的に修正または省略でき、実際に適用する前には十分な調査とテストが必要
- 正常なユーザー/組織内システム/依存している外部サービスがブロックされる可能性があるため、適用責任はすべて運用者にある
- 収益を生む運用環境では使わないこと
方法1: HTTPプロトコルで区別する
- 正常なユーザーをブロックするリスクは低く、一部の検索エンジンをブロックするリスクは中程度
- 一般的なブラウザはHTTP/2.0を使う一方、多くのボットはHTTP/1.1を使うという違いを利用する
- GoogleBotはHTTP/1.1を使うと見られ、この方法でブロックされる
- BingとFacebookのクローラーはHTTP/2.0を使う
- HTTP/1.1でリンクタイトルや短いプレビューを取得するサービスもブロックされる可能性がある
- Opera Miniも対象から除外する
- Nginxで
$server_protocol が HTTP/2.0 でなければ、別ページへリダイレクトするか、200、403、444 を返すように構成する
if ($server_protocol != HTTP/2.0) {
return 403 'Upgrade your client';
}
444 を返すと、別途レスポンスを返さずに接続を切断できる
- Google検索からの流入をブロックしたときに生じる損失より、得られる利点が大きいかどうかは各組織が自分で判断する必要がある
方法2: データセンターIP帯域をブロックする
- 正常な家庭用/LTEユーザーをブロックするリスクは低いが、VPNユーザーは中程度で、データセンターで動作する検索エンジンはブロックリスクが高い
- 直近1〜2年のアクセスログから、次の信号を組み合わせて疑わしいリクエストを探す
- HTTPプロトコル
- User-Agent
Accept-Language
Sec-Fetch-Mode
Accept
- 疑わしいIPを BGP Tools または Hurricane Electric BGP Toolkit で調べ、所属ASと広告されているPrefixを確認する
- 提供されている ネットワークブラックホール一覧 にはCDNや検索エンジンの帯域も含まれる可能性があるため、選別して適用する必要がある
- 別リストに先立ち、
3/8、10/8、11/8、25/8、26/8、38/8、41/8、60/8、61/8、200/8、224/3 帯域をすでにブラックホール化した構成を使う
- ASのPrefixページをコピーした後、シェル関数でCIDRだけを抽出し、ソート/重複削除/マージする
- sum_cidr.pl を使い、Perlモジュール
Net::CIDR::Lite が必要
- 生成された結果は確認後、
/usr/local/etc/*.netset ファイルへ移動する
- サーバー起動時に各CIDRをブラックホールルートとして追加する
for CflIP in $(grep -E ^[1-9] /usr/local/etc/_cloudflare.netset); do
/sbin/ip route add blackhole "${CflIP}" 2>/dev/null
done
- 例ではCloudflare全体の帯域をブロックし、Workersなど経由のリクエストを受けないようにしている
- ファイアウォールのipsetルールより Linuxのブラックホールルーティングの方がCPU使用量が少ない という理由で、ルーティング方式を選んでいる
- 現在のサーバーが属するホスティング事業者の帯域もブロックできる
- DNS/設定/内部サービスで同じアドレス空間を使っていないことが必要
- 直接接続されたゲートウェイルートはブラックホールルートより優先される
方法3: 国/プロキシ/Tor/悪性IPをブロックする
- FireHOL Blocklists リポジトリのリストをダウンロードし、必要な帯域をブラックホールルートとして追加する
- リストファイルにはコメントが含まれるため、
grep -Ev '^#' で除去してから処理する必要がある
- 特に次のリストを推奨している
firehol_abusers_30d.netset
firehol_level2.netset
- 大きなリストは起動スクリプトの実行時間が長くなる可能性がある
- 実際のサーバーとファイアウォール構成で使った設定ファイルも提供している
方法4: HTTPクライアント信号を検査する
- User-Agentやヘッダーは偽装できるが、速度を優先する単純なボットはそれらを適切に偽装しないことが多い、という前提を使う
Curl、Wget リクエストにはプレーンテキストを返し、Bot、GPT、LLM、Spider が含まれるリクエストには 410 Gone を返す
if ($http_user_agent ~* Curl) { return 200 '\nGNU Terry Pratchett\n\n'; }
if ($http_user_agent ~* Wget) { return 200 '\nGNU Terry Pratchett\n\n'; }
if ($http_user_agent ~* Bot) { return 410 '1000101'; }
if ($http_user_agent ~* GPT) { return 410 '1000101'; }
if ($http_user_agent ~* LLM) { return 410 '1000101'; }
if ($http_user_agent ~* Spider) { return 410 '1000101'; }
- 観測されたUser-Agentの一部文字列を1つの長い正規表現で検査し、クローラー/スキャナー/収集ツールをブロックする
Go-http、Java、libwww、okhttp、urllib、python、nmap、zgrab、semrush、shodan、rss、scrap、crawler、headless、github、facebook、google、bing などに該当する部分文字列が含まれる
- 適用前に2〜3年分のUser-Agentを集計し、実際の正常なクライアントが正規表現に一致するか確認する必要がある
sort access-user-agents.txt | uniq -c | sort
- 一致したリクエストがHTTP/1.1なら、ボットである可能性が高いものとして扱うが、GoogleBotは例外として考慮する
- HTTP/2.0リクエストであれば、BGPツールでIPの所属を追加確認する
Sec-Fetch-Mode
Sec-Fetch-Mode をアクセスログに追加し、値が cors、no-cors、navigate のいずれかでなければブロックする
if ($http_sec_fetch_mode !~ (cors|no-cors|navigate)) {
return 410 '1000101';
}
Referer検査
- 他サイトからコンテンツを埋め込んだりスキャンしたりするリクエストを防ぐため、
Referer に特定の文字列があればブロックする
- 管理者ページ/検索エンジン/ソーシャルネットワーク/暗号資産/成人向けコンテンツ/スキャナー/WordPress関連文字列などを検査する
- Googleのルートページ
https://www.google.com/ をRefererだと主張する特定のボット種別も別途ブロックする
- 古いAndroidを装うリクエストで観測されたパターン
HTTPメソッド制限
- 通常のブラウザリクエストに必要な
GET と POST だけを許可し、他のメソッドはブロックする
if ($request_method !~ (^GET$|^POST)) {
return 410 '1000101';
}
- 実際のアプリケーションで
POST が必要なパスをさらに細かく制限したり、使わない場合は完全に除外したりできる
プロキシとブラウザらしさの検査
X-Forwarded-For ヘッダーがあればプロキシリクエストと判断してブロックする
- 学校や図書館のような正常な共有プロキシ環境もブロックされる可能性がある
- User-Agentに
Linux、BSD、Macintosh、Windows、Mozilla、WhatsApp のいずれも含まれていなければ、ブラウザらしくないリクエストと判断する
Accept-Language に en または es がなければブロックするルールも使う
- 一部のブラウザと、英語/スペイン語を使わない正常なユーザーをブロックする可能性が大きい
br または sy を含む言語設定を別途ブロックする任意ルールも提示している
センシティブなパスのスキャンをブロックする
- 次のファイルまたはパスをリクエストした場合、自動スキャンと見なしてブロックする
方法5: nftablesでTCPスキャナーをブロックする
- nftablesの
raw テーブル PREROUTING チェーンでTCP SYNパケットの特性を検査する
- 例のサーバーアドレス
172.238.221.88 を宛先として明示し、パケットロス時に発生しうる誤検知を減らす
- 80/443ポートに入ってくるSYNパケットのうち、次の条件をブロックする
- TCPウィンドウサイズが 12,288バイト未満
- MSSが 1,220〜1,460の範囲外
- 実際のクライアントはより大きなウィンドウサイズを使い、この範囲を外れたMSSは正常なクライアントである可能性が低い、という基準を使う
- MSSを正確に
1460 に制限するとより厳格になるが、LTEやVPNユーザーの大半をブロックする可能性がある
TTLベースの任意制限
- TCP SYNのTTLが
128 より大きい場合、LTEデバイスの大半をブロックできる
- TTLが
64 より大きい場合、Windowsシステムの大半もブロックされる
- デフォルトTTLの目安は次のように説明している
- Linux/Mac/BSD:
64
- Windows:
128
- LTE: これよりさらに大きい値
コネクション追跡の除外
- 80/443ポートを
notrack に指定し、conntrackテーブルにWebトラフィックを入れない
- この場合、filterテーブルでも送受信方向について ステートレスなルール を直接構成する必要がある
- 例ではクライアントの送信元ポート
1000-65535 とサーバーの80/443ポート間のトラフィックを許可している
方法6: 成人向けコンテンツとロボットヘッダー
add_header Rating 'RTA-5042-1996-1400-1577-RTA' always;
add_header adult 'porn, sex, politics, religion, philosophy' always;
add_header X-Robots-Tag "none,noindex,nofollow,nosnippet,noai" always;
- 一般的なボットはこうしたヘッダーを無視する可能性があるが、検索エンジンや成人向けコンテンツを避けるよう設計されたボットには影響する可能性がある
方法7: TLSフィンガープリント検出
- 前述の1〜6番の方法はいずれも粗いヒューリスティックであり、長期的には TLSフィンガープリント分析 の方が良い選択肢になりうる
- ボットがTLSフィンガープリントを通常のブラウザと同じに変更しないという条件で、JA4を活用できる
- まず Deploying JA4 のデプロイ方法を確認したうえで、FoxIO JA4 を適用するよう案内している
方法8: Brotli圧縮コンテンツだけを提供する
- サイトコンテンツを事前にBrotliで圧縮し、Webサーバーが圧縮済みファイルだけを返すように構成する
- 多くのボットがBrotli圧縮HTMLを解釈できない点を利用する
- 適用後、複数のボットがページリンクをそれ以上たどらなくなり、HTMLを実際にはパースできていないことを確認した
- Nginxでは静的Brotliファイルを常に提供するよう設定する
brotli_static always;
cat ./i.html | brotli --best -fncv > ./i.html.br
方法9: スキャナーに自己識別させる
1件のコメント
Hacker Newsのコメント
複数の公開Webサイトを運営し、他サイトをクロールしてツールに活用する立場としては、なぜ人々がボットをそこまで気にするのか気になる
キャッシュ付きのWordPressでも最安のVPSで毎秒約 1,000リクエスト を処理できるし、きちんと作られた静的サイトならその10倍も可能そう。ブログをLambdaのようなもので配信しているのか、それとも強迫観念や脆弱性対策、クロールが実サービスに影響していた時代の習慣なのか気になる
リポジトリはオープンソースなので意図的に公開している。今は簡単なCookieチェックで防御しており、通過できるボットは少数だが、検索エンジン露出を犠牲にするトレードオフになっている
フォーラム、画像掲示板、チャットサーバーなどに適用する方法を示そうとしているもので、すべての選択肢は調整または無効化できる。実運用前にはテストサーバーで検証すべきで、ただ笑って流しても構わない
DMZが塞がってローカルIMAPサーバーの確認に数秒余計にかかるのは大事ではないが、だからといってそれを好んだり、許し続ける理由にもならない
週末に、10〜20年間運用していたviewvc(CVS・Subversion)とhgweb(Mercurial)のWebインターフェースを終了した。住宅用プロキシIPから1日 270万件、平均毎秒30件のリクエストが来て、古いuWSGI/CGIプログラムや同じサーバー上の他サイトに負荷を与え、トラフィックもVPS上限の月1TBに近づいていたため
動的なVCS URLの組み合わせは数百万通りあり得るのでキャッシュ効果も不確かで、サーバーをさらに調整することに時間を使う価値もなく、結局は中央集権的インターネットへまた一歩近づく選択になった
“承認済み” のユーザーエージェント以外をすべて遮断すると、既存の ブラウザ独占 を助け、ディストピアを早めることになる。RMSが何十年も前から警告していたのもこういう問題
問題なら、トラフィック量とリクエスト頻度を基準に止めるべき。自分もこのサイトにアクセスできないが、それに合わせて行動するつもりはないし、DRMのように意志の強い相手はいずれ突破する
ただし、どんなブラウザでも使えるという主張とは別に、その場しのぎで生成したコードだったり、悪意あるサイトに対して十分に検証されていないブラウザには特に注意すべき。リーダーアプリも、侵入テストの専門家による広範なサードパーティコードレビューを受けていなければ、悪意あるサーバー に脆弱かもしれない
169.254.169.254を指す偽の cpanelサブドメイン を追加すると、初心者攻撃者に自分のホスティング事業者をポートスキャンさせることになり、検知または遮断され得るという発想が気に入っている発信元IPは世界中に散らばっていたが、実際のスキャンノイズは たった1人 から出ていたことになる
IPベースの遮断は注意が必要。IP帯域は時々再割り当てされるため、無関係な人を遮断してしまうことがあり、地域やデータセンターという理由で遮断していた帯域が住宅向けISPに移った例も何度も見た
HTTP/1.1の遮断も、古いブラウザを使う実際のユーザーをブロックしてしまう危険が大きい。さらに、クロスオリジンのリクエストで完全なURLを送らないブラウザもあるため、Google検索から流入した場合でもリファラがGoogleのルートページしか指さないことがあり、これをボットの偽装と決めつけて遮断すると、Google検索からの流入まで消える可能性がある
一方で、HTTP/1.1の遮断は合理的だと思う。ほぼすべてのブラウザがそれを超えるプロトコルをサポートしてから10年以上経っており、そのほど古いブラウザなら、すでに主流のWebの大半が壊れているので、個人サイト1つがさらに使えなくても例外ではなく日常だろう
古いブラウザやAPIツールを取りこぼしても、HTTP/1.1の遮断は維持する予定。古い金融システムの独占コードなら理解できるが、公開インターネットは自分のために更新されるべきだ
Googleはずっと前から遮断しているので、Googleから来たと主張するリクエストはすべて偽りだ。ブログを複数のランダムなドメインでローテーションして関連性とスナップショットを断ち、人々が自分の文章を見つける経路を制御しようとしている
そのおかげで無料の侵入テストも受けられ、防御策と処理パイプラインが堅牢だという結論を得た。生存圧力のため、ボットトラフィックの90%がVPNへ移動し、VPN終端点がクリスマスツリーのように光っている
使い捨てのアクセスIPフィードを提供することもできるが、利用者はきちんと検証を受け、用途も承認されなければならない。この過程自体が面白い
読めないなら、アーカイブ https://archive.ph/d3236 で見られる
ボットでもないのに、何かを読むためにiCloud Private Relayを切りたくはない。筆者の別の回答によれば、試験用サイトとしては良い実装だが、ほかのWeb管理者は可能ならこのやり方をそのまま全部導入しないでほしい
応答本文に410と
Sec-Fetch-Mode:文字列しか出ないのを見ると、自分をボットだと判定したようだ。読むものも見るものもなく、そのまま去るしかなく、現代のWebはひどい対応状況は https://caniuse.com/?search=sec-fetch-mode で、一部ヘッダは https://nochan.net/.env で確認できる
PR_END_OF_FILE_ERRORが発生したWebトラフィックの99%以上がボットやエージェントだと予想されるので、訪問者数表示を消そうか悩んでいる。数値に意味がなく、実際よりサイトがずっと賑わって見えるが、本物の人間が文章を読んだり本をダウンロードしたりできなくなるのではと心配で対応をためらっている
遮断が必要なら、一般に許可リストは拒否リストより効果的で、許可リストを適用できないなら、この方式も良い解決策ではないかもしれない
CloudflareやAnubisのようなツールは深刻なアクセシビリティ問題を起こしうるので、リクエスト頻度制限のほうを好む。アクセシビリティを損なわず、しかもすっきりしており、一時的な問題には短時間のIP遮断もうまく機能する
個人的にはfail2banでHTTPログを分析し、
robots.txtで禁止したURLやwp-login.phpのようなパスを要求したり、リクエスト頻度制限をあまりに頻繁に超えたりするIPをN時間遮断している。現在はGit Web UIでAnubisを試験中だfail2banだけでもかなり防げるが、最初の数か月は環境に合わせて細かく調整する必要がある
まず
failregex = ^ - \\S+ \\[\\] ".*?" 40[034]フィルタで検知し、その後より具体的な一覧に追加したが、今では約80個の正規表現であらゆるトラフィックを遮断しており、一般的な40[034]フィルタまで到達するリクエストは長いこと出ていないただし、ロードバランサやプロキシの背後では実IPを取得する方法が必要なので、fail2banも原文の方式もどちらも面倒になる
これらのコメントと自分の接続の試みを見ると、ボットだけでなく正常なトラフィックもすべて遮断しているように見える
日曜日は、人々がWebを探索するときに使う変わったブラウザやアプリを最も多く見かけ、平日は普通の主流ブラウザが多いので良いテストになる