ジャーナル

robots.txtを覆すCDN、AIクローラーを断る層の探し方

アムステルダムの自転車店サイトのうち到達できた79件で、19件がブラウザには200を返しながらAIクローラーを拒否し、多くは自分のrobots.txtと食い違っていました。CDN、Webサーバー、プラグインのどれが断っているかの見分け方と直し方。

前の記事では、robots.txtではGPTBotを歓迎しているのに、サーバーは403で断っている日本の観光系サイトを取り上げました。今回はその次の問いです。どの層が断っているのか、誰かが意図したのか、何を変えればいいのか。

その前に、この食い違いが日本のホスティングに特有の癖なのかを見るため、まったく別のサイト群で同じ確認をしました。特有の癖ではありませんでした。

先に、この確認でわからないこと

  • 対象は1つの都市の1つの業種で、到達できたのは79サイトです。下の24%を小規模事業者のサイト全般の割合として読まないでください。
  • 私のプローブは、クローラーのUser-Agentを普通の回線から送ったものです。本物のクローラーは各社が公開しているIPアドレスから来ます。User-Agentで判定するオリジン側のルール(下のnginx、Apache、LiteSpeedなどの14件)なら両者は同じ扱いになるので、その結果は本物のクローラーにも当てはまるはずです。
  • Cloudflareは両者を区別できます。AIボットの設定は検証済みのクローラーに加えて"a number of unverified bots that behave similarly"(似た振る舞いをする未検証のボットもいくつか)を対象にするので、私の真似への403は、Cloudflareがなりすましを止めただけで、本物のGPTBotは通っているのかもしれません。Cloudflareの5件については、ダッシュボードのSecurity Eventsかオリジンのアクセスログでしか確かめられません。
  • 200が返っても、AIの回答に何かが出る約束にはなりません。この確認で外せるのは、サイトが出てこない理由のうちの1つだけです。

アムステルダムの自転車店、2026-10-04の測定

出典は、2026-10-04にOverpass APIで取得した、アムステルダム市の境界内にありshop=bicycleとwebsiteのタグが付いたOpenStreetMapのオブジェクト全件です。オブジェクトは108件、ホスト名の重複を除くと92件。ほとんどは個人の修理店やレンタル店で、チェーン店がいくつかと、タグの付け方が少し妙なものが2、3件混じっています。手で選んだものはありません。

各ホームページに、ブラウザのUser-Agentで1回、公開されているGPTBot、ClaudeBot、PerplexityBotのUser-Agentでそれぞれ1回、普通の回線1本からリクエストしました。クローラーを拒否したサイトには数分後にもう一度リクエストし、同じクローラーが2回とも403だった場合だけを拒否と数えています。このルールで、レート制限や不安定なホスティングに見える429や503が数件と、タイムアウト1件を除外しました。

92ホストのうち13件はブラウザにも200を返しませんでした(ドメイン切れ、タイムアウト、サーバーエラー)。残りは79件です。その79件のうち19件が、ブラウザには200を返しながら、少なくとも1つのAIクローラーのUser-Agentに2回とも403を返しました。24%で、日本のサンプル(23件中6件)で見たおよそ4分の1に近い数字です。

  • 19件のうち15件では、robots.txtは拒否されたクローラーを許可しています。1件はrobots.txtでも拒否していて、こちらはrobots.txtとサーバーの判断が一致しています。残る3件はrobots.txtがありません。
  • ClaudeBotは19件すべてで、GPTBotは14件、PerplexityBotは5件で拒否されました。5件はClaudeBotだけを拒否しています。19件のうち12件は、GPTBotとして/robots.txtを取りに行っても拒否しました。
  • 作り物のUser-Agent(ExampleFetchBot/1.0)は19件中18件で200でした。これらのルールはAIクローラーを名指ししていて、「botという文字を含むものは全部弾く」汎用のフィルタではありません。
  • 拒否のうち5件はCloudflareのエッジから来ています(Cloudflare経由のサイトは21件)。残る14件はオリジンからで、8件がnginx、3件がApache、1件がLiteSpeed、1件がOpenRestyとして応答し、1件はServerヘッダーを返しません。

自分の回線のせいである可能性を外すため、19件のうち6件を、Vercel上に置いた別のチェッカー、つまり別のネットワークからも確認しました。6件とも同じステータスコードでした。

無関係な2つのサンプルが、2つの大陸で同じ形を示している。役に立つのはそこです。

何かを変える前に、層を特定する

リクエストを断れる場所は最大で4つあります。エッジのCDNやWAF、リバースプロキシ、Webサーバー、そしてアプリケーション(CMSのセキュリティプラグインなど)です。どれが答えたかは、たいていレスポンスに書いてあります。自分のサイトに対してこれを実行してください。

URL=https://example.com/
probe() {
  printf '%-9s ' "$1"
  curl -s -L -o /dev/null -D - -A "$2" "$URL" | tr -d '\r' | grep -i -E '^(HTTP/|server:|cf-mitigated:)' | paste -sd' ' -
}
probe browser   'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128 Safari/537.36'
probe GPTBot    'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.1; +https://openai.com/gptbot)'
probe ClaudeBot 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)'
probe made-up   'Mozilla/5.0 (compatible; ExampleFetchBot/1.0; +https://example.com/bot)'
URL=${URL}robots.txt probe robots-as-GPTBot 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.1; +https://openai.com/gptbot)'

サンプルのnginxのサイトの1つでは、こう出ます。

browser   HTTP/1.1 200 OK Server: nginx
GPTBot    HTTP/1.1 403 Forbidden Server: nginx
ClaudeBot HTTP/1.1 403 Forbidden Server: nginx
made-up   HTTP/1.1 200 OK Server: nginx
robots-as-GPTBot HTTP/1.1 403 Forbidden Server: nginx

読み方は次のとおりです。

403のときに見えるもの 断った層
Server: cloudflare、ページタイトルが"Attention Required! | Cloudflare"で"Sorry, you have been blocked"と表示 Cloudflareのブロック。WAFのカスタムルール、マネージドルール、またはAIボットの設定
Server: cloudflareに加えてcf-mitigated: challenge Cloudflareのチャレンジ(Bot Fight Mode、またはチャレンジを返すルール)。クローラーは解けないので、実質は拒否
Server: nginx、Apache、LiteSpeedで、小さな素っ気ないエラーページ Webサーバーか、その後ろの何か。/robots.txtまで拒否されるなら、ルールはパスごとの処理より前に動いています。典型はサーバー全体のUser-Agent判定か.htaccessのルール
サーバーは200だが、本文がCMSのエラー画面やログイン画面 アプリケーションのプラグイン

Serverヘッダーが示すのは応答した一番外側のサーバーで、判断したサーバーとは限りません。「nginx」のサイトの1つは、Apache標準の"You don’t have permission to access this resource"というページを返します。共有ホスティングによくある構成で、nginxが前にいてApacheが後ろにあり、ルールはApacheの設定か.htaccessにあります。

意図した拒否か、2つの層の食い違いか

AIクローラーを止めるのは筋の通った選択で、意図して止めている運営者もたくさんいます。確認したいのは、サイトが言っていることとやっていることが違っていないかです。たいていは次の3つの問いで決着します。

  1. robots.txtはこのクローラーについて何と言っているか。robots.txtが拒否していてサーバーも拒否しているなら、層同士は一致しています。19件中1件がこれでした。robots.txtが許可していてサーバーが拒否しているなら、誰かが1つ決めて、別の何かがその逆を決めています。
  2. 作り物のボットは通るか。ExampleFetchBotが200でGPTBotが403なら、ルールはAIクローラーを名指ししています。その一覧を誰か、または何かが書いたということです。作り物のボットも403なら、おそらくAIクローラーを想定していなかった汎用のボットフィルタです。
  3. 応答した層を管理しているのは誰か。共有ホスティングやサイトビルダーは独自のUser-Agentのブロックリストを持っていますし、セキュリティプラグインは更新のたびにリストを足します。サイトの持ち主がそのルールを一度も見ていないことはよくあります。ホスティング会社に聞くか、設定を検索してください。grep -rn -i 'gptbot\|claudebot' /etc/nginx/ .htaccess

サーバーを自分で管理しているなら、私の真似ではなく本物のクローラーについての答えが、アクセスログにあります。

grep -E 'GPTBot|ClaudeBot|PerplexityBot' /var/log/nginx/access.log \
  | awk '{ match($0, /GPTBot|ClaudeBot|PerplexityBot/); print substr($0, RSTART, RLENGTH), $9 }' \
  | sort | uniq -c

クローラーとステータスコードの組ごとに件数が出ます。標準のcombinedログ形式で、9番目のフィールドがステータスであることを前提にしています。

層ごとの直し方

まず何をしたいかを決めて、それをrobots.txtに書きます。クローラーが読むのはこの層で、誰かに聞かれたときに見せられるのもこの層だからです。3つとも通すなら、こうです。

User-agent: GPTBot
User-agent: ClaudeBot
User-agent: PerplexityBot
Allow: /

User-agent: *
Disallow: /wp-admin/

学習用クローラーは止めつつ、Perplexityの検索用クローラーからは見えるようにしたいなら、最初のグループをUser-agent: GPTBot、User-agent: ClaudeBot、Disallow: /に置き換えます。そのうえで、ほかのすべての層をこのファイルに合わせます。

Cloudflare

この拒否を生む設定は3つあり、振る舞いがそれぞれ違います。

  • Bot Fight Mode(Freeプラン、Security設定)。Cloudflareのドキュメントには、これは"cannot be bypassed or skipped using WAF custom rules or Page Rules"(WAFのカスタムルールやPage Rulesでは迂回もスキップもできない)とあります。Freeプランでは、オンのままチャレンジを受け入れるか、オフにするかのどちらかです。
  • Super Bot Fight Mode(Pro、Business、Enterprise)はスキップできます。アクションがSkipのWAFカスタムルールを追加し、スキップ対象にSuper Bot Fight Modeを選び、Cloudflareが検証済みのクローラーにだけ一致する式を使います。
(cf.verified_bot_category in {"AI Crawler" "AI Search" "AI Assistant"})

このルールは、ブロックするルールより上に置いてください。Cloudflareはこれらのボットを、User-Agentの文字列ではなくリクエストの送り元で検証するので、GPTBotのUser-Agentを真似ただけの相手に扉を開けることにはなりません。

  • AIボットのポリシー(Security設定の"Configure AI bot policies"。以前の"Block AI bots"のトグルを置き換えたもの)。Cloudflareのドキュメントによると、2026-09-15以降に追加された新しいドメインでは、広告を表示するページでTrainingとAgentのボットが既定でブロックされます。また"mixed-purpose crawlers that combine Search and Training"(検索と学習を兼ねるクローラー)は、学習をブロックするどの設定でもブロックされます。最近ドメインを追加してこの画面を一度も開いていないなら、確認してください。

なりすましを止めるルールを自分で書きたいなら、次の式が、GPTBotを名乗っているのに検証済みのボットから来ていないリクエストに一致します。

(http.user_agent contains "GPTBot" and not cf.client.bot)

どちらの式も、Cloudflareのオープンソースのルールパーサー(wirefilter)で確認しました。わざと壊した2つの版、閉じ括弧が欠けたものとandを打ち間違えたものは、どちらも構文エラーで落ちるので、この確認が不正な入力をちゃんと弾くこともわかっています。ただ、パーサーが確かめるのは、私が宣言したフィールドの型に対する構文までです。最終的な判断はダッシュボードの式エディタです。

nginx

うっかり版はたいていこんな形で、「悪いボットを止める」系のリストから貼り付けられたものです。

if ($http_user_agent ~* "(bot|crawl|spider)") { return 403; }

これをサーバーのレベルに置くと、/robots.txtも含めてすべてのクローラーを断ります。手元で動かしたところ、GPTBot、ClaudeBot、作り物のスクレイパーのどれも、/と/robots.txtの両方で403になりました。次の書き換えは、名前を挙げたAIクローラーを通し、それ以外には汎用のフィルタを残し、robots.txtは常に返します。

map $http_user_agent $block_ua {
    default                                            0;
    ~*(GPTBot|ClaudeBot|PerplexityBot|OAI-SearchBot)   0;
    ~*(bot|crawl|spider|scrape)                        1;
}

server {
    listen 80;
    root /var/www/html;

    location = /robots.txt { }

    location / {
        if ($block_ua) { return 403; }
    }
}

mapはhttpブロックに置きます。正規表現の項目は上から順に評価され、最初に一致したものが使われるので、許可の行は汎用の行より前に置く必要があります。nginx 1.28.0で試しました。nginx -tは通り、起動したインスタンスに対して、ブラウザ、GPTBot、ClaudeBotは200、SomeScraperBot/2.0は/で403、/robots.txtで200でした。defaultの行のセミコロンを消すとnginx -tが失敗するので、このテストがただ「はい」と言っているだけではないこともわかります。逆にクローラーを止めたいなら、それらを1の側に移し、robots.txtのlocationは残してください。止めると書いたファイルを、クローラーが読めるようにしておくためです。

共有ホスティング、Apache、プラグイン

自分で管理していないホスティングから403が来ているなら、まず.htaccessにクローラー名がないか探します(RewriteCond %{HTTP_USER_AGENT}の行と、その後ろの[F]付きのルール)。次はセキュリティプラグインのボットやファイアウォールの設定です。何も見つからなければ、ホスティング会社のサポートに、サーバーのレベルでAIクローラーをUser-Agentでブロックしているかを問い合わせてください。上の19件のうち12件はrobots.txtまで拒否していて、サイト自身のファイルが参照される前に動くルールがあることをうかがわせます。

ツールで確かめる場合

シェルを使わずに確かめたいなら、無料のAIクローラーチェックが、GPTBot、ClaudeBot、PerplexityBotについて、robots.txtとサーバーの応答を同じように比べます。入力した内容は保存しません。

1サイトを人の手で一通り見てほしい場合は、同じページから9,800円のミニ診断を申し込めます。確認するのはAIクローラーのアクセスと機械可読性で、優先順の改善リストをお渡しします。AIエンジンには質問を送らないので、AIの回答に出てくるかどうかはわかりません。