unmask

docs

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

コンテナで動かす

リリースごとにイメージを 1 つ公開しています: unmask.sh/unmask — 公式 nginx イメージ + unmask module(その nginx バージョンに合わせてビルド)と unmask daemon を 1 つの entrypoint が束ねたものです。ホストへのインストールと同じ構成が 1 コンテナに 入っています — nginx が TLS を終端して判定し、daemon が検証と状態を持つ — そして任意の HTTP サーバーの前段に JA4 対応の gateway として置けます。アプリを nginx で配信していない環境で unmask を使う方法がこれです。

1. クイックスタート(docker compose)

リポジトリの docker-compose.example.yml(各リリースに添付されるので、取得した copy はそのリリースのイメージと一致します) は、サービス 1 つと仮の upstream だけです。用意するものはありません:

curl -fsSL -o docker-compose.example.yml https://unmask.sh/dl/docker/docker-compose.yml
docker compose -f docker-compose.example.yml up -d
docker compose -f docker-compose.example.yml logs unmask | grep "setup token"

compose を使わない場合は、同じコンテナを手で起動します。転送先 (upstream) を設定するまで、gateway は訪問者に短い案内ページを返します。 転送先は管理画面で設定するか (後述)、初回起動時に -e UNMASK_UPSTREAM=http://<転送先サーバー>:<ポート> で渡します。このコンテナから届くアドレスを指定してください。名前を解決できないホストを指定すると nginx が起動しません。

docker run -d --name unmask --restart unless-stopped -p 80:80 -p 443:443 \
  --add-host host.docker.internal:host-gateway \
  -v unmask-config:/etc/unmask -v unmask-data:/var/lib/unmask -v unmask-acme:/var/cache/nginx/unmask-acme \
  unmask.sh/unmask:latest
docker logs unmask | grep "setup token"

https://localhost/unmask/admin/ を開きます。初回起動は admin が生成した自己署名証明書を配信するのでブラウザが一度警告します (いったん受け入れてください)。管理画面もサイトと同じ保護の内側にあるので、次に短い security check が表示され、通過するとインストール ウィザードが続きます。ログのセットアップトークンを貼り、SQLite(既定)か自分の DB を選び、管理者ユーザーを作れば完了。 あとは 設定 → Gateway で、転送先 (upstream)・応答するホスト名・本番の証明書(Let's Encrypt / 貼り付け / mount したファイル)を設定します。

volume は必ず残してください。 /etc/unmask には描画済みの nginx include と、通過 cookie に署名する secret が入っています。 これを付けずにコンテナを動かすと、再起動のたびに secret が変わり、訪問者全員がログアウトして再チャレンジになります。 /var/lib/unmask はデータベース、/var/cache/nginx/unmask-acme は Let's Encrypt を使う場合のアカウントと証明書です。

管理画面で変更した設定は /etc/unmask に描画され、同じコンテナ内の nginx がファイルの変化を数秒以内に検知して自分で reload します。 事前に nginx -t を通すので、解釈できない描画があればログに残してスキップし、稼働中の設定はそのまま保ちます。手で実行するものはありません。 自分のタイミングで reload したい場合は UNMASK_AUTORELOAD=0 で watcher を止められます。

1 コンテナに 2 プロセス: entrypoint が daemon を起動して応答を待ち、次に nginx を起動して、両方を見守ります。daemon が落ちれば 少し待って再起動し、その間も nginx は配信を続けます。リクエストは challenge なしで proxy され(fail-open)、断られるのは BAN 済みのクライアントだけです。nginx が落ちればコンテナごと終了し、restart policy が全体を立て直します。docker stop は nginx を graceful に止めます。

2. gateway モード: 任意の HTTP サーバーの前段に

コンテナが :443 で TLS を終端し(クライアントの ClientHello を直接見るので JA4 が取れます)、 チャレンジ判定を行い、通過したリクエストを upstream へ proxy します。アプリ側には何もインストールしません。 配信しているものが Node でも Rails でも Go でも Apache でも別の nginx でも、静的ファイルサーバーでも構いません。

gateway の設定は管理画面の 設定 → Gateway にあります: 転送先 (upstream)(Docker ホスト上のサーバーなら http://host.docker.internal:8080、別ホストなら http://10.0.0.5:8080、同じ compose 内のコンテナなら http://app:3000)、応答するホスト名、証明書。前段ロードバランサーの信頼するプロキシは 設定 → Network → 信頼する LB / CDN (GCP / Cloudflare などの preset あり)で、JA4 header を信頼する相手と同じ集合です。転送先が未設定の間、gateway はその旨の案内ページを返します。同じホストで動いているサーバーを転送先にする場合は、:80/:443 を gateway に譲って自分のサーバーを 8080 へ移します (Apache は Listen 8080、nginx は listen 8080;)。コンテナから届くアドレスに bind する必要があるので 127.0.0.1 は不可、8080 は firewall で外から閉じておきます。compose ファイルには host.docker.internal がホストを指す設定が最初から入っています。下の環境変数は初回起動時にタブへ 流し込むだけです(UNMASK_UPSTREAM / UNMASK_TRUSTED_PROXIES は、タブが空の間のフォールバックとしても効きます)。

変数意味既定
UNMASK_GATEWAYgateway モード。このイメージでは既定で ON。0 にし、UNMASK_UPSTREAM を設定しないでおくと(設定すると gateway モードに戻ります)、module を読み込んだだけの素の nginx(+ daemon)になり、自分の conf.d が効きます。3 章を参照。1
UNMASK_UPSTREAM通過したリクエストの転送先。例 http://app:3000。初回起動時にタブの初期値になり、タブが空の間はフォールバックとして効きます。未設定 = タブで設定
UNMASK_SERVER_NAMEgateway が応答するホスト名(初回起動時に Gateway タブへ流し込まれます)。_ = すべて、それ以外はスペース区切りのリストで、流し込まれる証明書のドメインにもなります。_(すべて)
UNMASK_ACME_EMAIL自動 HTTPS の初期値。UNMASK_SERVER_NAME の証明書を Let's Encrypt から取得し、nginx 自身が更新します。未設定 = 自己署名(証明書ファイルをマウントしていればそのファイル)
UNMASK_ACME_DIRECTORYACME directory URL。検証中は staging を指定。Let's Encrypt 本番
UNMASK_TLS_CERTコンテナ内の証明書チェーン(PEM)。ACME を使わない場合。/etc/unmask/tls/fullchain.pem
UNMASK_TLS_KEYコンテナ内の秘密鍵(PEM)。ACME を使わない場合。/etc/unmask/tls/privkey.pem
UNMASK_TLS_MODE前段のロードバランサーが TLS を終端して :80 に転送する構成なら none(「待ち受け: http」のみを seed。:443 を開かず証明書不要)。ほか files / acme / upload / selfsigned。自動: メールがあれば acme、証明書をマウントしていれば files、どちらもなければ自己署名
UNMASK_TRUSTED_PROXIESgateway の前段にあるロードバランサーのアドレス範囲(空白区切り)。訪問者のアドレスをその X-Forwarded-For から取ります。初回起動時に 設定 → ネットワーク → 信頼する LB / CDN へ流し込み、その一覧が空の間は代わりに効きます。未設定 = 接続元がそのまま訪問者

ホスト名と証明書

どちらも管理画面の 設定 → Gateway で、互いに独立した 2 つの問いとして設定します。どのホスト名に応答するか: すべて(Host をそのまま upstream に渡す。upstream 側が複数サイトを持つ場合向け)か、指定したリストか — リストにない Host は TLS ハンドシェイクの段階で拒否します。どの証明書を持つか: 証明書ごとに 1 エントリで、入手元と「どのドメインの証明書か」を 持ちます。nginx は SNI で証明書を選び、1 枚目が既定としてどの証明書のドメインにも一致しないホスト名に配信されます(リストのホスト名に 証明書が無ければタブが警告します)。つまり 1 つの gateway で複数サイト・複数証明書を扱え、ホスト名を変えても証明書には触れません。 各証明書には :443 がその最初のドメインに実際に配信している証明書(発行者・有効期限・名前の一致)も表示されます。 証明書の入手元:

:80 は https へリダイレクトします。ロードバランサーの health check は user agent(GoogleHC、ELB-HealthChecker、kube-probe など。追加は 設定 → ネットワーク → HTTP → HTTPS リダイレクト)でリダイレクトを免れます。 ロードバランサーの範囲を 設定 → ネットワーク → 信頼する LB / CDN に登録すると(初回起動時なら UNMASK_TRUSTED_PROXIES でも可)、BAN・rate limit・geo ルールがロードバランサーではなく訪問者を見るようになります。upstream には Host、X-Real-IP、X-Forwarded-For、 X-Forwarded-Proto: https が渡り、WebSocket の upgrade も通します。

gateway は body サイズの上限を設けず、1 時間までのレスポンスを許容します。アップロードやストリームの制限は upstream 側の設定がそのまま効き、 gateway が勝手に決めた既定値で切られることはありません。

すでに nginx を運用している場合、その前段に置く

動きます。二段 proxy の一般的な注意が 2 つあるだけです。後段の nginx には gateway のアドレスから素の HTTP が届くので、 (1) 実クライアントの所在を教えてください — set_real_ip_from に gateway のアドレス、real_ip_header X-Forwarded-For — でないとログ・rate limit・geo 判定がすべて gateway を見ます。(2) 後段が自前で http→https リダイレクトをしているなら、受け取ったスキームではなく $http_x_forwarded_proto で判定してください。そうしないと両者がリクエストを弾き合ってループします。 あるいは TLS を後段に残したまま転送先にその https リスナーを指定する手もあります。gateway は SNI を送るので vhost の選択も効きます。 クライアント証明書(mTLS)は TLS が gateway で終わるため通せません。

その nginx を変更できるなら、module-only の方が向いています。そちらに module を足せば二段目の proxy 自体が要りません。 gateway は、それができないときのためのものです。

gateway モードでは unmask がリクエスト経路に入ります(module-only のインストールでは入りません)。nginx は公式イメージそのままの stock バイナリで、 server.inc の daemon 停止時 fail-open も同じく効きます。daemon が止まっている間(entrypoint が再起動します)は、拒否ではなくチャレンジなしで proxy します。

3. module-only: 自分の nginx イメージで

UNMASK_GATEWAY=0 にして UNMASK_UPSTREAM を設定しないでおくと、コンテナは module を読み込んだだけの素の nginx になります(daemon は同じコンテナで動き続けます)。 自分の conf.d を /etc/nginx/conf.d に mount し、ホストと同じ include を書きます:

# http スコープ(conf.d のファイルはすべて http スコープ)
include /etc/unmask/http.inc;
include /etc/unmask/upstream.conf;
include /etc/unmask/forward-auth-lbtrust.conf;

server {
    listen 443 ssl;
    ...
    include /etc/unmask/server.inc;
    location / {
        include /etc/unmask/protect.inc;
        proxy_pass http://app:3000;
    }
}

既存の nginx イメージに module だけ足す構成は、このイメージではなくホスト install の形です: Dockerfile の module ステージが .so の作り方 (nginx のバージョンと一致し、--with-compat でビルド。公式の nginx:<version> イメージはこの条件を満たします)で、 daemon はパッケージでホストに入れ、描画された include を自分のイメージに mount します。

4. Kubernetes と他のリバースプロキシ

JA4 は TLS handshake から読むので、TLS を終端する場所にしか存在しません。ingress controller、ほかのリバースプロキシ、クラウドの LB の裏では コンテナはクライアントの ClientHello を見られないため、前段が JA4 を転送しない限り unmask は JA4 無しで動きます (転送できる前段は LB / CDN 設定にまとめています)。 行動 CAPTCHA、PoW、IP、UA の各軸はそのまま効き、fingerprint の軸だけが使えなくなります。

edge を自分で握れるなら、gateway イメージを edge にしてください。小さなクラスタなら LoadBalancer Service を直接向ければ JA4 の軸を保てます。 ingress-nginx との統合はありません — あの controller はサードパーティの module を読み込みません。

5. イメージのタグ

イメージはリリース workflow がタグの commit からビルドして push します。CI は push のたびにビルドするので、 --with-compat で読み込めなくなる module の変更は出荷前に検出されます。