結論:TLS 終端の位置で決まる
[Client] ──TLS handshake──▶ [ ここで ClientHello を見た者だけが JA4 を計算できる ]
JA4 を計算するには 生の ClientHello バイト列が必要です。TLS を復号した後の平文 HTTP しか見ていないプロセスは、後から JA4 を組み立てられません。
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 など) は、このページの後半にあります。
[Client] ──TLS handshake──▶ [ ここで ClientHello を見た者だけが JA4 を計算できる ]
JA4 を計算するには 生の ClientHello バイト列が必要です。TLS を復号した後の平文 HTTP しか見ていないプロセスは、後から 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」で選択 |
unmask-plugin-nginx。C モジュールが ClientHello を直接解析して JA4 を算出 (最速・サブリクエスト無し・CentOS 6 等の古い distro でも動作)。サイトを nginx で配信していて JA4 を使うならこれ。unmask.sh/unmask イメージ。公式 nginx イメージ + unmask module と unmask daemon を 1 つのコンテナにまとめたもの。任意の HTTP server (Apache、Node、別の nginx など) の前段で TLS を終端し、native と同じ仕組みで JA4 を算出するので、後ろのサーバーは何も変えなくてよい。→ コンテナで動かす / インストールガイド。X-Client-JA4)、Cloudflare は自前の cf-ja4 ヘッダー (有効化が必要で、プランによる)、AWS CloudFront / ALB は AWS 側の構成で JA4 をヘッダーに載せられる場合に限る。そうしたヘッダーが無ければ、③④ でも JA4 は取れない。X-Client-JA4、Cloudflare は cf-ja4)。トグルも nginx 側の手作業の配線も不要。手順 → 下の LB / CDN 設定。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
q 始まりで届き、「信頼する LB / CDN」の gate をそのまま通ります。JA4 が空でも unmask は動作します。bot 判定は以下のシグナルで続行されます。
unmask-plugin-nginx (native mode)。nginx が TLS を終端していることインストール手順は インストールガイド を参照してください。
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 コンテナのどちらでも共通です。
[Client] ──TLS──▶ [LB] ──HTTP + X-Client-JA4──▶ [nginx + unmask]
↑ ↑
ここで JA4 抽出 header から読む
LB / CDN 側の役割:
X-Client-JA4 (Cloudflare は自前の cf-ja4 を使い、unmask の Cloudflare preset はそれをそのまま読む)設定する箇所は 2 つ:
使っている LB / CDN を選んで、クライアントの JA4 をリクエストヘッダーに載せる設定をする。このセクションは native mode と gateway コンテナのどちらにも当てはまる。
GCP の HTTPS LB は custom request header 機能で
{client_ja4_fingerprint} という template variable をヘッダーの値に埋め込めば、
LB が JA4 を計算してヘッダーに展開します。GCP の custom header 機能はクライアントの送信値を
必ず上書きするので、偽装の心配はありません。
Google Cloud Console から:
+ ADD HEADERX-Client-JA4:{client_ja4_fingerprint} を入力# 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。
unmask の管理画面で、信頼する LB / CDN の GCP HTTPS Load Balancer を選び (§3)、§4 の方法で確認する。
--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 は、訪問者の JA4 fingerprint を自前の cf-ja4 リクエストヘッダーでオリジンに送れます。
ただし zone でこのヘッダーを有効にした場合に限ります (手順 1。使えるかどうかは Cloudflare のプランによる)。
unmask は cf-ja4 をそのまま読むので、名前の変換も nginx への追記も不要です。
cf-ja4 として付加する managed request header を ONcf-ja4 が付く
unmask の管理画面 設定 → ネットワーク → 信頼する LB / CDN で Cloudflare にチェックを入れて保存する (§3)。
以後 unmask は、Cloudflare のアドレス範囲から来たリクエストでだけ cf-ja4 を読み、それ以外のアドレスから来たものは使わない。
nginx の server block への追記は不要。
cf-ja4 を使えるかどうかは Cloudflare のプランによります。最新のプラン要件は Cloudflare 公式ドキュメントで確認してください。
cf-ja4 は使われません。多重防御として、nginx を Cloudflare IP (cloudflare.com/ips) からのみ受け入れるファイアウォール (クラウドのファイアウォール / security group / iptables) も入れてください。
CloudFront や ALB の後ろでは、nginx に届くのは AWS 側の接続で、訪問者の接続ではありません。そのためクライアントの JA4 は リクエストヘッダーで届く必要があり、unmask はそのヘッダーを信頼するかどうかを nginx に接続してきたアドレスで決めます。 AWS 側の構成で訪問者の JA4 をヘッダーに載せられるかどうかは、AWS の現在の機能と設定次第なので、AWS のドキュメントで確認してください。 それぞれの選択肢での unmask 側の設定は次のとおりです。
X-Client-JA4 ヘッダーを読むので、JA4 はこの名前で
(クライアントの送信値を上書きして) オリジンに届く必要がある。別のヘッダー名で届く場合は、preset の代わりに、同じアドレス範囲と
そのヘッダー名で custom LB の行を追加する。TLS を終端するロードバランサーを前段に置かなければ、nginx がクライアントの TLS handshake を直接見られる。EC2 インスタンス上で、 unmask module を入れた nginx か gateway コンテナを、サイトの証明書と一緒に動かす (gateway コンテナは Let's Encrypt から証明書を取得できる)。この場合 JA4 にヘッダーは要らない。ただし、 ALB の機能 (sticky sessions, WAF integration) は使えなくなる。
レシピは上記の LB と同じで、前段レイヤーで TLS を終端し、そこでクライアントの JA4 を
計算してリクエストヘッダー (慣例は X-Client-JA4) に詰めて backend へ転送する
(クライアントの送信値は上書きする)。unmask がそのヘッダーを信頼するよう、
管理画面 設定 → ネットワーク の custom LB セクションに、
前段レイヤーの接続元 CIDR とヘッダー名を追加する。クライアントの TLS handshake から JA4 を出せる reverse
proxy なら、同じ方式で unmask を動かせる。
前段が自分で運用している nginx なら、JA4 ヘッダーは要らない。unmask の native mode に切り替えるのが最も楽で (公式 module を提供)、module が ClientHello から JA4 を自分で算出する。前段を gateway コンテナにしても同じことができる。インストール手順は インストールガイドを参照。
§2 で前段 (LB / CDN) が JA4 のヘッダーを送るようになったら、unmask 側は「どの送信元を信頼するか」を 1 箇所決めるだけ。native mode と gateway コンテナのどちらでも操作は同じ (管理画面の 設定 → ネットワーク タブ)。
保存すると、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 コンテナはコンテナ内で同じ仕組み。専用トグルは無い。
t で始まる。HTTP/3 なら q)。記録されるのは challenge を出した訪問で、既定では cookie を持たない初回訪問もこれに含まれるsudo unmask stats -kind ja4 -since 1h を実行すると、直近 1 時間に記録された fingerprint が一覧で出るJA4 列の読み方:
- (空) → JA4 が unmask に届いていない。LB がヘッダーを送っていないか、§3 で LB / CDN を選んでいないNOT_TRUSTED → 信頼するアドレス範囲の外から JA4 のヘッダーが届いた。選んでいる LB / CDN と、その CIDR を確認する
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=...