unmask

docs

JA4 取得、LB 設定、対応 distro、よくある質問。

JA4 fingerprint は、TLS handshake (ClientHello) から取り出すクライアントの「指紋」です。接続に使われる TLS ライブラリやクライアントの実装 (client stack) で決まる値なので、同じ stack を使うクライアントはすべて同じ値を共有し、IP / 国 / VPN / User-Agent を切り替えても変わりません。

ただし JA4 を取得できるかどうかは、「誰が TLS handshake を最初に受け取るか (TLS 終端の位置)」で決まります。このページでは代表的な構成パターンごとに可否を一覧します。

unmask の前段で LB や CDN が TLS を終端している場合に、クライアントの JA4 を受け取る設定 (GCP / Cloudflare / AWS など) は、このページの後半にあります。

結論:TLS 終端の位置で決まる

[Client] ──TLS handshake──▶ [ ここで ClientHello を見た者だけが JA4 を計算できる ]

JA4 を計算するには 生の ClientHello バイト列が必要です。TLS を復号した後の平文 HTTP しか見ていないプロセスは、後から JA4 を組み立てられません。

構成パターン別の JA4 可否

JA4 を計算するには 生の ClientHello バイト列が要るので、可否は TLS をどこで終端するか(= クライアントの ClientHello を最初に見る点)と unmask の置き方(nginx native module / gateway コンテナ)だけで決まります。後段のアプリは無関係です。

● = unmask インストールノード ★ = TLS 終端 (ここで ClientHello を見る)

構成 (クライアント → …) JA4 unmask 設定
① 前段に LB (unmask 同居) +後段アプリ — unmask が TLS 終端
★●LB nginx (native) → httpd可なし (プラグインが算出)
★●gateway コンテナ → httpd (任意の HTTP server)可なし (コンテナ内の module が算出)
② アプリ直 — unmask が TLS 終端
★●nginx (native)可なし
③ 前段 (CDN / LB) が TLS を終端し、クライアントの JA4 をヘッダーで転送 (下の補足参照) +後段アプリ
★CDN → ●LB nginx (native) → httpd可「信頼する LB / CDN」で選択
★CDN → ●gateway コンテナ → httpd (任意の HTTP server)可「信頼する LB / CDN」で選択
④ 前段 (CDN / LB) が TLS を終端し、クライアントの JA4 をヘッダーで転送 + unmask 直
★CDN → ●nginx (native)可「信頼する LB / CDN」で選択
  • native = nginx + unmask-plugin-nginx。C モジュールが ClientHello を直接解析して JA4 を算出 (最速・サブリクエスト無し・CentOS 6 等の古い distro でも動作)。サイトを nginx で配信していて JA4 を使うならこれ。
  • gateway コンテナ = unmask.sh/unmask イメージ。公式 nginx イメージ + unmask module と unmask daemon を 1 つのコンテナにまとめたもの。任意の HTTP server (Apache、Node、別の nginx など) の前段で TLS を終端し、native と同じ仕組みで JA4 を算出するので、後ろのサーバーは何も変えなくてよい。→ コンテナで動かす / インストールガイド。
  • Apache には unmask の module が無いので、Apache のサイトで JA4 を使うなら前段に gateway コンテナを置く (Apache 向けの推奨構成)。
  • ③④ には、クライアントの JA4 をリクエストヘッダーで転送する前段が要る。GCP HTTPS LB は custom request header (X-Client-JA4)、Cloudflare は自前の cf-ja4 ヘッダー (有効化が必要で、プランによる)、AWS CloudFront / ALB は AWS 側の構成で JA4 をヘッダーに載せられる場合に限る。そうしたヘッダーが無ければ、③④ でも JA4 は取れない。
  • ③④ では、どちらの置き方でも手順は同じで、管理画面の「信頼する LB / CDN」で前段 (CDN / LB) を選ぶだけ。unmask はその CDN / LB のアドレス範囲から届いた JA4 だけを採用し、それ以外から届いたものは使わない。ヘッダー名は preset 側が持っている (X-Client-JA4、Cloudflare は cf-ja4)。トグルも nginx 側の手作業の配線も不要。手順 → 下の LB / CDN 設定。

HTTP/3 (QUIC) — 先頭が q になる

JA4 の先頭 1 文字は ClientHello が届いたトランスポートを表します: t = TCP 上の TLS、q = QUIC。 nginx プラグインは HTTP/3 でも TCP と同じ仕組みで JA4 を算出します。 --with-http_v3_module 付きでビルドした nginx に listen … quic を設定すれば、QUIC の ClientHello も モジュールが既にフックしている TLS ライブラリの callback を通るため、追加設定は不要です。 nginx の HTTP/3 ビルドで使われる OpenSSL 3.5+ (native QUIC API) と quictls の 両系統で動作を確認済みです。

TCP  (HTTP/1.1, HTTP/2)  →  t13i3514h2_bfa337485184_3ad6d895dd8c
QUIC (HTTP/3)            →  q13i0306h3_55b375c5d22e_1cecd519fee8
  • 同じクライアントでも HTTP/3 では別の文字列になります (先頭文字・ALPN・hello の中身がすべて変わるため)。HTTP/3 を配信しているサイトで JA4 のカスタムルールを書くときは、両方の形を対象にしてください。
  • 同一訪問者の HTTP/2 ↔ HTTP/3 切替は、unmask の cookie rebinding が正常な遷移として扱います (fingerprint 不一致にはなりません)。
  • 前段の CDN / LB が HTTP/3 を終端して JA4 を転送する構成でも、転送値は q 始まりで届き、「信頼する LB / CDN」の gate をそのまま通ります。

JA4 が取れないとどうなる?

JA4 が空でも unmask は動作します。bot 判定は以下のシグナルで続行されます。

  • User-Agent 分類 — 600+ パターンのクローラー / bot UA リストで既知 bot を識別
  • 公式 IP 範囲 — Googlebot / Bingbot 等の公式範囲を通過リストに登録 (順位事故防止)。範囲を公開する vendor は UA でなく IP で検証し、なりすまし UA を弾く (範囲のないクローラーは UA で通過)
  • rate-limit — IP × パス単位の閾値超過で challenge に格上げ
  • honeypot — 登録した罠パス (運用していない CMS パス等) を踏んだ訪問者を BAN
  • behavioral CAPTCHA — mouseTrail / scroll / keyboard 等 5 軸の人間らしさスコア
JA4 が無い場合に bot 判定の精度が落ちるのは「正規ブラウザを装うステルス bot」の検出です。UA も IP も綺麗に偽装した bot は、TLS の癖 (JA4) でしか見破れません。逆に言えば JA4 さえ取れれば、IP の入れ替えや UA 偽装を貫通して同一ツールを追跡できます。

まとめ — どの構成を選ぶか

  • サイトを nginx で配信している → nginx + unmask-plugin-nginx (native mode)。nginx が TLS を終端していること
  • Apache などそれ以外の HTTP server → 前段に gateway コンテナを置く。コンテナが TLS を終端し、JA4 を自分で算出する
  • TLS を終端する前段 (CDN / LB) がある → そこからクライアントの JA4 をヘッダーで転送する → 下の LB / CDN 設定

インストール手順は インストールガイド を参照してください。

LB からクライアントの JA4 を取る

SSL 終端を Load Balancer (GCP / AWS / Cloudflare 等) で行っている場合、backend nginx にはクライアントの TLS handshake が届きません。届くのは平文の HTTP か LB 自身の TLS 接続なので、 そこで JA4 を計算しても空になるか、LB 自身の TLS fingerprint になってしまいます。実際のクライアントの JA4 を取るには、 LB 側でクライアントの TLS handshake から JA4 を抽出し、ヘッダーに詰めて backend へ転送する 設定が必要です。

以下で、主な LB / CDN での設定方法を説明します。unmask の native mode と gateway コンテナのどちらでも共通です。

1. 全体像

[Client] ──TLS──▶ [LB] ──HTTP + X-Client-JA4──▶ [nginx + unmask]
                  ↑                ↑
       ここで JA4 抽出        header から読む

LB / CDN 側の役割:

  • ① クライアントの TLS handshake から JA4 fingerprint を計算
  • ② backend へ転送するリクエストのヘッダーに値を入れる。慣例は X-Client-JA4 (Cloudflare は自前の cf-ja4 を使い、unmask の Cloudflare preset はそれをそのまま読む)
  • ③ クライアントが同じヘッダーを送ってきた場合は、LB 段で必ず上書き (偽装防止)

設定する箇所は 2 つ:

  • §2 CDN / LB 側:クライアントの JA4 をリクエストヘッダーに載せる (LB ごとに設定)
  • §3 unmask 側:「その LB を信頼する」と管理画面で選ぶ。既定は どの LB も信頼しない安全寄りの設定 なので、明示的に有効化する

2. CDN / LB 側の設定

使っている LB / CDN を選んで、クライアントの JA4 をリクエストヘッダーに載せる設定をする。このセクションは native mode と gateway コンテナのどちらにも当てはまる。

GCP HTTPS Load Balancer

GCP の HTTPS LB は custom request header 機能で {client_ja4_fingerprint} という template variable をヘッダーの値に埋め込めば、 LB が JA4 を計算してヘッダーに展開します。GCP の custom header 機能はクライアントの送信値を 必ず上書きするので、偽装の心配はありません。

手順 1: backend service を編集 (Console)

Google Cloud Console から:

  1. 左メニュー → Network Services → Load balancing
  2. 該当の LB をクリック → Edit
  3. Backend configuration → 該当の backend service → ✏️ 鉛筆アイコンで編集
  4. Advanced configurations 展開 → Custom request headers に + ADD HEADER
  5. Header に X-Client-JA4:{client_ja4_fingerprint} を入力
  6. Save → Update で完了
手順 2: gcloud CLI で同じことをする
# existing backend service の custom header を 1 行追加 (既存 header は維持されない上書きなので注意).
gcloud compute backend-services update YOUR_BACKEND_SERVICE \
    --global \
    --custom-request-header="X-Client-JA4:{client_ja4_fingerprint}"

※ --custom-request-header を複数回指定すると最後の 1 つだけが残る (既存とまとめたい 場合は全ヘッダーを一度に指定する)。確認は gcloud compute backend-services describe YOUR_BACKEND_SERVICE --global。

手順 3: unmask で信頼し、動作を確認

unmask の管理画面で、信頼する LB / CDN の GCP HTTPS Load Balancer を選び (§3)、§4 の方法で確認する。

偽装防止: GCP の --custom-request-header はクライアントの送信値を無条件で上書きするので、クライアントが自分で付けた X-Client-JA4 は LB で置き換わります。LB を通らずに nginx へ直接送られたものは、LB のアドレス範囲から来ていないため unmask は使いません。さらに、nginx を LB 経由 (35.191.0.0/16, 130.211.0.0/22, 35.227.0.0/16 等) のみ受け入れるファイアウォール (GCP VPC firewall rule) で多重に防御することを推奨します。

Cloudflare

Cloudflare は、訪問者の JA4 fingerprint を自前の cf-ja4 リクエストヘッダーでオリジンに送れます。 ただし zone でこのヘッダーを有効にした場合に限ります (手順 1。使えるかどうかは Cloudflare のプランによる)。 unmask は cf-ja4 をそのまま読むので、名前の変換も nginx への追記も不要です。

手順 1: Managed Transforms で cf-ja4 を有効化
  1. Cloudflare ダッシュボード → 該当の zone を選択
  2. Rules → Transform Rules → Managed Transforms
  3. 訪問者の JA4 を cf-ja4 として付加する managed request header を ON
  4. 以後、オリジンへのリクエストに cf-ja4 が付く
手順 2: unmask で Cloudflare preset を選ぶ

unmask の管理画面 設定 → ネットワーク → 信頼する LB / CDN で Cloudflare にチェックを入れて保存する (§3)。 以後 unmask は、Cloudflare のアドレス範囲から来たリクエストでだけ cf-ja4 を読み、それ以外のアドレスから来たものは使わない。 nginx の server block への追記は不要。

注意: cf-ja4 を使えるかどうかは Cloudflare のプランによります。最新のプラン要件は Cloudflare 公式ドキュメントで確認してください。
偽装防止: preset を選んだ時点で、Cloudflare のアドレス範囲以外から届いた cf-ja4 は使われません。多重防御として、nginx を Cloudflare IP (cloudflare.com/ips) からのみ受け入れるファイアウォール (クラウドのファイアウォール / security group / iptables) も入れてください。

AWS ALB / CloudFront

CloudFront や ALB の後ろでは、nginx に届くのは AWS 側の接続で、訪問者の接続ではありません。そのためクライアントの JA4 は リクエストヘッダーで届く必要があり、unmask はそのヘッダーを信頼するかどうかを nginx に接続してきたアドレスで決めます。 AWS 側の構成で訪問者の JA4 をヘッダーに載せられるかどうかは、AWS の現在の機能と設定次第なので、AWS のドキュメントで確認してください。 それぞれの選択肢での unmask 側の設定は次のとおりです。

選択肢 A: CloudFront または ALB が JA4 をヘッダーで届ける
  • CloudFront がオリジンに接続する構成: 信頼する LB / CDN で AWS CloudFront preset を選ぶ (§3)。 この preset は CloudFront がオリジンへ接続するアドレス範囲を信頼し、X-Client-JA4 ヘッダーを読むので、JA4 はこの名前で (クライアントの送信値を上書きして) オリジンに届く必要がある。別のヘッダー名で届く場合は、preset の代わりに、同じアドレス範囲と そのヘッダー名で custom LB の行を追加する。
  • ALB がオリジンに接続する構成 (ヘッダーを付けたのがどの層でも): ALB は自分の VPC 内のアドレスからインスタンスへ 接続するため、ALB 用の preset は無い。ALB のサブネットとヘッダー名で custom LB の行を追加する。 ALB が転送するヘッダーは ALB からのものとして信頼されるので、ヘッダーを付ける層以外が ALB に届かないようにしておく。
選択肢 B: インスタンスで TLS を終端

TLS を終端するロードバランサーを前段に置かなければ、nginx がクライアントの TLS handshake を直接見られる。EC2 インスタンス上で、 unmask module を入れた nginx か gateway コンテナを、サイトの証明書と一緒に動かす (gateway コンテナは Let's Encrypt から証明書を取得できる)。この場合 JA4 にヘッダーは要らない。ただし、 ALB の機能 (sticky sessions, WAF integration) は使えなくなる。

JA4 ヘッダーが無い場合: unmask は JA4 無しでも動作し、UA フィルタ・rate limit・honeypot・CAPTCHA はそのまま働きます。落ちるのは、実ブラウザを装うステルス bot の検出です (JA4 が取れないとどうなる?を参照)。

その他の LB / Reverse proxy

TLS を終端する任意の前段レイヤー

レシピは上記の LB と同じで、前段レイヤーで TLS を終端し、そこでクライアントの JA4 を 計算してリクエストヘッダー (慣例は X-Client-JA4) に詰めて backend へ転送する (クライアントの送信値は上書きする)。unmask がそのヘッダーを信頼するよう、 管理画面 設定 → ネットワーク の custom LB セクションに、 前段レイヤーの接続元 CIDR とヘッダー名を追加する。クライアントの TLS handshake から JA4 を出せる reverse proxy なら、同じ方式で unmask を動かせる。

自分で運用している nginx

前段が自分で運用している nginx なら、JA4 ヘッダーは要らない。unmask の native mode に切り替えるのが最も楽で (公式 module を提供)、module が ClientHello から JA4 を自分で算出する。前段を gateway コンテナにしても同じことができる。インストール手順は インストールガイドを参照。

3. unmask 側の設定

§2 で前段 (LB / CDN) が JA4 のヘッダーを送るようになったら、unmask 側は「どの送信元を信頼するか」を 1 箇所決めるだけ。native mode と gateway コンテナのどちらでも操作は同じ (管理画面の 設定 → ネットワーク タブ)。

  1. 「信頼する LB / CDN」で、使っている LB / CDN (GCP HTTPS LB / Cloudflare / AWS CloudFront 等) にチェックを入れる
  2. 一覧に無いもの (自社内部 LB / ALB 等) は custom LB で接続元 CIDR と JA4 ヘッダーを追加する

保存すると、unmask はその送信元から来た JA4 ヘッダーだけを採用し (ヘッダー名は preset 側が持っている。X-Client-JA4、Cloudflare は cf-ja4)、直アクセスの詐称ヘッダーは破棄される。あとは sudo nginx -s reload するだけ (gateway コンテナは自分で reload する)。nginx の server block への JA4 用の追記は不要。これで完了。

仕組み (運用者は意識不要): native は plugin が ClientHello から JA4 を算出し (前段が無くても動く)、信頼する LB のヘッダーが届いたときは生成済みの http.inc がそちらを採用する。gateway コンテナはコンテナ内で同じ仕組み。専用トグルは無い。

4. 動作確認 (admin ダッシュボード)

  1. reload 後、ブラウザのプライベートウィンドウで対象のサイトを開く (unmask の通過 cookie をまだ持っていない状態にするため)
  2. unmask の管理画面で bot ハントを開き、自分の IP の行を探す。JA4 列に、そのリクエストで unmask が使った fingerprint が出る (t で始まる。HTTP/3 なら q)。記録されるのは challenge を出した訪問で、既定では cookie を持たない初回訪問もこれに含まれる
  3. または unmask のホストで sudo unmask stats -kind ja4 -since 1h を実行すると、直近 1 時間に記録された fingerprint が一覧で出る

JA4 列の読み方:

  • ブラウザや端末ごとに fingerprint が違う → クライアントの JA4 が届いている ✓
  • - (空) → JA4 が unmask に届いていない。LB がヘッダーを送っていないか、§3 で LB / CDN を選んでいない
  • NOT_TRUSTED → 信頼するアドレス範囲の外から JA4 のヘッダーが届いた。選んでいる LB / CDN と、その CIDR を確認する
  • どのリクエストも同じ fingerprint → LB から nginx への TLS 接続の fingerprint で、クライアントのものではない。ヘッダーが使われていない

verdict 列や、統計ページの「JA4 verdict 分布」カードに出る (none) は、失敗の印ではありません。fingerprint がどの verdict ルールにも一致しなかったという意味で、普通のブラウザはここに入ります。JA4 自体が無いリクエストも含まれるので、判断は JA4 列で行ってください。

native mode でリクエスト単位に見る場合: http.inc は unmask という名前の log format を定義していて、unmask が使った JA4 とその理由を記録する (diag=ok。untrusted_lb_header = 信頼する範囲の外から JA4 のヘッダーが届いた。trusted_no_header = 信頼する LB がヘッダーを送ってこなかった)。既存の access_log 行と同じブロックに、しばらくの間だけ access log を追加する:

access_log /var/log/nginx/unmask-ja4.log unmask;   # 追加したら nginx を reload

sudo tail -f /var/log/nginx/unmask-ja4.log         # 各行に ... ja4=... v=... a=... diag=...