Korzen
Bot Protection
このドキュメントの目次

ファーストパーティモード

タグ、ビーコン、チャレンジページを自社のドメインから配信します。SDK、Cloudflare Worker、またはその両方で、Korzen をブロックするネットワークの訪問者にも届きます。

このモードは任意です。DNS フィルター、広告ブロッカー、または Korzen のホストをブロックするネットワークは、タグ、そのビーコン、チャレンジページを止めてしまいます。そうした訪問者もサーバーが送る情報だけで判定されて通過しますが、タグのトークンを必要とするルートでは先へ進めません。ファーストパーティモードは、この 3 つを自社ドメインのパス(別のパスを指定しない限り /kz)から配信し、訪問者のアドレスを添えて Bot Protection へ転送します。サイトそのものをブロックせずに、そのサイトのパスだけをブロックすることはできません。

パスを配信する方法は 2 つあり、両方を使うこともできます。

方法有効にするもの/kz を配信するもの
SDK のみNode では firstParty: true、Go では FirstParty: true自社のサーバー
Worker のみKORZEN_FIRST_PARTY = "1" と /kz/* のルート自社の Cloudflare Worker
Worker と SDK両方を同じパスでWorker。チャレンジされた訪問者はサーバーが Worker へ送ります

どの方法でも、ページは Korzen のホストではなく自社のパスからタグを読み込みます。タグは自身の src からアドレスを取るため、タグが送るリクエストもすべて /kz へ向かいます。

<script src="/kz/tag.js" data-site-key="YOUR_SITE_KEY" async></script>

このモードはすべての SDK にあります。Node(とその Worker)、Go、Python、PHP、Java、.NET で、オプションはそれぞれの README に記載しています。フレームワークガイドをもとに書いたクライアントにはフォワーダーがありません。その場合は SDK を使うか、Cloudflare の背後であれば、どの言語のオリジンにもこのモードを提供できる Worker だけを使ってください。

SDK のみ

自社のサーバーがパスを配信します。すでに Bot Protection に問い合わせているミドルウェアにオプションを渡し、同じオプションでパス専用のハンドラーを用意してください。クライアントは API キーを指定して createClient で作成したものでなければなりません。フォワーダーはリクエストごとにこのキーを Korzen へ送ります。

// Express, with korzen.js as the Express guide sets it up
const options = { client: korzen, ip: (_get, req) => req.ip, firstParty: true };

app.use("/kz", protect(options)); // before any body parser: it answers /kz/... itself
app.use(express.urlencoded({ extended: false }));
app.post("/signup", protect(options), signup);

// Next.js: app/kz/[...path]/route.js serves the path
const kz = withKorzen(() => new Response(null, { status: 404 }), { client, ip: ipFrom.vercel, firstParty: true });
export { kz as GET, kz as POST };
o := korzen.Options{IP: korzen.RemoteIP, FirstParty: true}

mux.Handle("/kz/", bot.Protect(http.NotFoundHandler(), o)) // the path: Protect answers it itself
mux.Handle("POST /signup", bot.Protect(http.HandlerFunc(signup), o))
  • 有効にするもの。protect または withKorzen に firstParty: true(true のときだけ有効)、Go では Options.FirstParty です。そのうえで、ページのタグの src を /kz/tag.js にします。訪問者のアドレスは判定と同じ方法で読み取ります。ip オプション、Go では Options.IP です。
  • 訪問者が書き込めないアドレス。Korzen はフォワーダーがあなたのキーとともに送るアドレスを信頼するので、ip オプションは訪問者が設定できないアドレスを読み取る必要があります。ipFrom.vercel は Vercel 上でのみ、ipFrom.cloudflare(Go では korzen.CloudflareIP)はサーバーが Cloudflare からのトラフィックしか受け付けない場合のみ、自前のロードバランサーや nginx の背後では ipFrom.lastForwarded を使います。訪問者が書き込めるヘッダーを使うと、Korzen が判定するアドレスをスクリプトが選べてしまいます。
  • 訪問者にとって変わること。目に見える変化はありません。ブラウザはタグを読み込み、ビーコンを自社のドメインへ送ります。そのため、Korzen をブロックするネットワークの訪問者もタグの証拠をもとに判定され、トークンを必要とするアクションも行えます。パス配下のリクエストは判定されず、自社のハンドラーにも届きません。それ以外のパスはこれまでどおり判定されます。
  • チャレンジページ。チャレンジされたページの読み込みは、Korzen のドメインではなく自社ドメインの /kz/challenge?c=…&r=… へ送られ、サーバーがページ、そのファイル、送信を転送します。課題は同じで、訪問者は元のページへ戻ります。
  • Fail-open。Bot Protection が遅い、停止している、またはサーバーから届かない場合、パス配下の各リクエストは空のボディで 503 を返し、タグはそのシグナルを捨てます。ページやほかのルートは何も待ちません。Fail-open
  • 動作の確認。curl -I https://shop.example/kz/tag.js が 200 を返し、ブラウザのネットワークパネルでタグのリクエストが自社のドメインへ向かっていることを確認できます。動作の確認

Worker のみ

Cloudflare Worker がパスを配信します。サーバーは変更不要で、オリジンの言語も問いません。Cloudflare Workers のとおりに Worker を設定してから、モードを有効にし、パスのルートを追加してください。

# wrangler.toml
routes = [
  { pattern = "shop.example/kz/*", zone_name = "shop.example" },
  { pattern = "shop.example/signup*", zone_name = "shop.example" },
]

[vars]
KORZEN_URL = "https://korzen.dev"
KORZEN_FIRST_PARTY = "1"
# KORZEN_FIRST_PARTY_PATH = "/kz"   # the path, /kz unless set
  • 有効にするもの。KORZEN_FIRST_PARTY を 1 または true に設定し(0 と false 以外の値では無効のままで、Worker のログに警告が出ます)、shop.example/kz/* のルートを追加し、ページのタグの src を /kz/tag.js にします。訪問者のアドレスは Cloudflare が設定する CF-Connecting-IP です。
  • 訪問者にとって変わること。目に見える変化はありません。タグとビーコンは自社のドメインへ送られ、Cloudflare のエッジで応答されます。オリジンがそれらのリクエストを見ることはありません。Worker のルートはこれまでどおり判定されます。
  • チャレンジページ。Worker がチャレンジしたページの読み込みは、自社ドメインの /kz/challenge へ送られ、Worker がそのページを配信します。
  • Fail-open。SDK と同じです。そのリクエストだけが空のボディで 503 になり、ほかのリクエストはすべてオリジンへ進みます。Fail-open
  • 動作の確認。下記のとおりです。さらに npx wrangler tail には /kz 配下のリクエストが表示され、オリジンのアクセスログには 1 件も表示されません。動作の確認

Worker と SDK

サイトの前段に Worker があり、アプリケーションでも SDK を使っている場合(アプリケーションだけが知る context のため)は、両方で同じパスを指定してモードを有効にします。

  • 有効にするもの。上記のとおり /kz/* のルートを持つ Worker と、その背後の SDK の firstParty: true(Go では FirstParty: true)です。/kz/* には Worker 自身が応答するため、SDK がそれらのリクエストを見ることはありません。
  • 訪問者にとって変わること。Worker のみの場合と同じです。タグ、ビーコン、チャレンジページは自社のドメインから、エッジで応答されます。
  • チャレンジページ。SDK の firstParty は、SDK がチャレンジした訪問者を Korzen のホストではなく /kz/challenge へ送り、Worker がそのページを配信します。これがないと、SDK がチャレンジした訪問者は Korzen のホストへ送られ、ネットワークによってはブロックされます。
  • Fail-open。パスを配信する側がそれぞれ単独で打ち切り、503 を返します。どちらも相手を待たせません。Worker のルートに /kz/* が含まれない場合や Worker を外した場合は、SDK が自らパスを配信します。その場合、Cloudflare の背後では ip: ipFrom.cloudflare(Go では korzen.CloudflareIP)を指定してください。
  • 動作の確認。下記のとおりです。npx wrangler tail には /kz 配下のリクエストが表示され、サーバーのログには 1 件も表示されません。

パスが配信するもの

フォワーダー(SDK または Worker)は次のものだけに応答します。

自社のドメイン内容
GET /kz/tag.jsタグ。Korzen のキャッシュヘッダー付き
GET /kz/n、POST /kz/sタグの nonce とビーコン(ボディは最大 64 KiB)
GET /kz/pingフォワーダー自身が 204 で応答し、転送しません
GET と POST /kz/challenge、GET /kz/challenge/waitチャレンジページとその送信(最大 16 KiB)
GET /kz/static/…チャレンジページのスクリプト、スタイル、フォント

/kz 配下のそれ以外のパスは 404、既知のパスでもメソッドが違えば 405 になり、どちらも Bot Protection へは届きません。そのため、このパスがオープンプロキシになることはありません。フォワーダーが Korzen へ送るのは、短いリストにあるブラウザ自身のヘッダー(User-Agent、Accept、Accept-Language、Content-Type、Origin、Sec-Fetch-*、クライアントヒント、If-None-Match)、bearer としての API キー、訪問者のアドレスです。Cookie、ブラウザの Authorization、X-Forwarded-* は決して送りません。戻ってくる応答は Korzen のキャッシュヘッダーとセキュリティヘッダーを保ち、Set-Cookie は決して含みません。

Korzen が訪問者のアドレスを受け入れるのは、これらのルートへのリクエストが自社の API キーを持つ場合だけで、ビーコンでは自社のサイトキーの場合だけです。それ以外は 403 で拒否されます。キーがブラウザに届くことはありません。

タグの _kz Cookie とストレージはこれまでどおり自社のドメインに残り、Korzen が受け取るデータも同じです。ブラウザが Korzen に直接接続したときだけ読める証拠の一部は、フォワーダー経由では得られません。タグのほかの証拠は届き、欠けているものが訪問者に不利に働くことはありません。

別のパス

firstPartyPath(Go では Options.FirstPartyPath、Worker では KORZEN_FIRST_PARTY_PATH)で別のパスを指定できます。たとえばブロックリストが /kz を覚えたときです。パスは / で始まり、/ で終わらず、A-Z a-z 0-9 . _ ~ - のセグメントで最大 64 文字です。読み取れないパスは一度だけログに記録され、モードは無効のままです。タグの src、SDK、Worker で同じパスを使ってください。

Fail-open

このパスがページやほかのルートを待たせることはありません。

  • 転送されるリクエストはそれぞれ単独で打ち切られます。GET は 1 秒、POST は 2.5 秒です。タイムアウト、ネットワークエラー、4 MiB を超える応答、フォワーダーの障害は、空のボディと Cache-Control: no-store の 503 になります。リダイレクトはたどりません。Bot Protection 自身の 4xx と 5xx はそのまま返ります。
  • 訪問者のアドレスがない場合(ip オプションが何も読み取れない場合)、nonce、ビーコン、チャレンジは 503 を返します。タグとチャレンジページのファイルは引き続き配信されます。
  • そのときタグは、Korzen のホストが停止しているときと同じように、そのシグナルを捨てます。タグが報告しなかったことがボットの証拠になることはありません。訪問者はサーバーが送る情報で判定されます。

動作の確認

  • curl -I https://shop.example/kz/tag.js が 200 を、curl -I https://shop.example/kz/ping が 204 を返します。
  • タグのあるページでブラウザの開発者ツールを開くと、ネットワークパネルに自社ドメインの /kz/tag.js、/kz/n、/kz/ping、/kz/s が 200 または 204 で表示され、Korzen のホストへのリクエストはありません。
  • 保護されたアクションを一度通してから、セットアップページの「連携を確認する」を押してください。「その呼び出しに含まれるタグのトークン」が「検出済み」になります。判定はこれまでどおりダッシュボードの判定ページに表示されます。
  • Content-Security-Policy を使っている場合は、Korzen のホストを外せます。タグには script-src 'self'; connect-src 'self'; worker-src blob: で足ります。タグ

よくある間違い

  • タグをまだ Korzen のホストから読み込んでいる。モードはパスを配信しますが、ブラウザをそこへ向かわせるのはタグの src だけです。
  • Express でフォワーダーより前にボディパーサーがある。ビーコンのボディがフォワーダーに渡る前に読まれてしまいます。どのパーサーよりも前に、またはパスだけにマウントしてください。
  • KORZEN_FIRST_PARTY を設定した Worker に /kz/* のルートがない。リクエストはオリジンに届き、SDK がそこでパスを配信していなければ 404 になります。
  • パスが 2 つある。Worker と SDK で別のパスを使うと、SDK はチャレンジした訪問者を Worker が配信しないパスへ送ってしまいます。
  • /v1/decide を直接呼び出している。その challenge.url は常に Korzen のホストです。訪問者を自社のパスへ送るのは SDK と Worker だけです。