04/Documentation
デプロイバリアント B〜E
デフォルト以外のフロントドア: Identity-Aware Proxy、Tailscaleなしのリバースプロキシ、ホスト外のIngress、同一ホスト上の複数のCollie(ユーザーごとに1つ、または1ユーザーに複数インスタンス)、packのスタンバイドア
ブリッジは 127.0.0.1 にバインドします。デプロイ構成は、Ingress と ID 検証の方法によって異なります。バリアント A(プレーンな tailscale serve)は README に記載されています。docs/security.md のセキュリティ要件は、すべての構成に適用されます。
個々のデバイスは、プロキシを使用せず ペアリング 経由で認可することもできます。
---
バリアント B: Identity-Aware Proxy + デバイスごとの認可
Collie は COLLIE_DEVICE_HEADER から不透明なデバイス ID を読み取り、COLLIE_DEVICE_ALLOWLIST をチェックします。許可リストにある ID には書き込み権限が付与されます。ヘッダーがない場合やリスト外の ID は読み取り専用アクセスになります(読み取り、スナップショット、セッション一覧は利用可能ですが、ターミナル入力、アップロード、ペイン操作はブロックされます)。
Collie の設定(.env):
COLLIE_HOST=127.0.0.1 # keep loopback (default)
COLLIE_DEVICE_HEADER=X-Device-Id # the header your proxy injects
# ids allowed to drive agents; others → read-only
COLLIE_DEVICE_ALLOWLIST=my-phone,my-laptop
# only if the proxy does NOT forward the public Host
# COLLIE_ALLOWED_ORIGINS=https://collie.example.com
# REQUIRED unless the proxy forwards a Host Collie already knows: loopback, a
# discovered Tailscale host, or an allowed origin's host
# COLLIE_PUBLIC_HOSTS=collie.example.com
# opt out of Host validation entirely (re-opens DNS rebinding)
# COLLIE_ALLOW_ANY_HOST=1
# COLLIE_TRUSTED_USER still composes on top if your ingress also injects
# Tailscale-User-Login
# accept a request carrying no Tailscale-User-Login at all
# COLLIE_TRUSTED_USER_OPTIONAL=1プロキシの要件:
- デバイスを認証する(mTLS、SSO、forward-auth)。
- クライアントによる偽装を防ぐため、すべてのアップストリームリクエストで デバイスヘッダーを上書きする。
- ループバックにプロキシする(
127.0.0.1:$COLLIE_PORT)。 - 公開
Hostを変更せずに転送する を行うか、同一オリジンチェックを通過させるためにCOLLIE_ALLOWED_ORIGINSに公開オリジンを記載します。
Nginx の設定例:
location / {
proxy_set_header X-Device-Id $device_id;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:8787;
}外部デバイスからヘッダーの注入と上書きを検証する:
$ curl -s https://collie.example.com/api/snapshot | jq -c .device
{"enforced":true,"device":"my-laptop","authorized":true}
$ curl -s -H 'X-Device-Id: my-phone' https://collie.example.com/api/snapshot | jq -c .device
{"enforced":true,"device":"my-laptop","authorized":true}2 つ目のチェックで "device":"my-phone" が返される場合、プロキシは上書きではなく追記を行っています。
運用上の注意:
- ループバックへの直接アクセス(
http://127.0.0.1:$COLLIE_PORT)はヘッダーを送信しないため、読み取り専用になります。 - curl を使用してローカルでコマンドを実行するには、ヘッダーを明示的に指定してループバックに渡します:
curl -H 'X-Device-Id: my-laptop' http://127.0.0.1:$COLLIE_PORT/api/... - アクセスを取り消すには、
COLLIE_DEVICE_ALLOWLISTから ID を削除して再起動します(Standalone:bin/collie restart、Herdr:herdr plugin action invoke restart --plugin herdr.collie)。 - プレーンな
tailscale serveでCOLLIE_DEVICE_HEADERを有効にしないでください。ヘッダーは上書きされません。
---
バリアント C: リバースプロキシのみをフロントドアとする構成(Tailscale なし)
Tailscale の外部で実行する場合や、専用の TLS/SSO プロキシの背後で実行する場合に使用します。COLLIE_SKIP_SERVE=1 を指定すると、Collie は tailscale serve を管理しなくなります。
バリアント B の 4 つのプロキシ要件が適用されます。
Caddy の設定例:
collie.example.com {
reverse_proxy 127.0.0.1:8787 {
header_up X-Device-Id {your_device_id}
header_up Host {host}
}
}Collie の設定(.env):
# proxy is ingress; never run tailscale serve
COLLIE_SKIP_SERVE=1
# REQUIRED — Host validation fails closed, and `collie start` discovers no
# tailnet name here
COLLIE_PUBLIC_HOSTS=collie.example.com
# opt out of Host validation (re-opens DNS rebinding)
# COLLIE_ALLOW_ANY_HOST=1
# exact public origin for the same-origin gate
COLLIE_ALLOWED_ORIGINS=https://collie.example.com
COLLIE_DEVICE_HEADER=X-Device-Id # the header your proxy injects…
# …and the ids allowed to drive; others → read-only
COLLIE_DEVICE_ALLOWLIST=my-phone,my-laptop
# optional — status banner, `collie qr`, and the address a lead hands
# joining machines (pack)
# COLLIE_PUBLIC_URL=https://collie.example.comCOLLIE_TRUSTED_USER は Tailscale がないと機能しません。デバイスごとのヘッダー、またはプロキシ自体の認証をアクセスポータルとして使用します。
/auth/ のルーティングとキャッシュ
- オリジン
Cache-Control(no-cache) で/sw.jsとindex.htmlを渡します。未認証クライアントに対する静的アセットのブロックは避けてください。ブロックすると Service Worker が更新できなくなります。 - 認証フローは
/auth/*(または Cloudflare Access の場合は/cdn-cgi/access/) 配下にルーティングします。Service Worker は/auth/*のキャッシュをバイパスします。 - API リクエスト時のフォワード認証リダイレクトは、ログイン UI を表示するために 401 として扱われます。Authentik を使用する場合は
/auth/を/outpost.goauthentik.io/startにルーティングしてください。
collie.example.com {
handle /auth/* {
reverse_proxy 127.0.0.1:9091
}
handle {
forward_auth 127.0.0.1:9091 { ... }
reverse_proxy 127.0.0.1:8787 { ... }
}
}エンドポイントとキャッシュルールを確認します:
$ curl -s https://collie.example.com/api/snapshot | jq -c .device
{"enforced":true,"device":"my-phone","authorized":true}
$ curl -sI https://collie.example.com/sw.js | grep -i '^cache-control'
cache-control: no-cache---
バリアント D: tailnet 経由のホスト外 ID プロキシ
トラフィックを集約された Tailscale イングレスノード経由でルーティングする場合に使用します。
phone ──── https ────► ingress node TLS + forward-auth; SETS the device header
│
│ http, never leaves the tailnet (WireGuard encrypts it)
▼
host.your-tailnet.ts.net:8787 tailscale serve --http, tailnet-only
│
▼
127.0.0.1:8787 Collieバリアント B の要件が適用されますが、プロキシの接続先はループバックではなくホストの Tailscale HTTP エンドポイントになります。
Tailscale ACL (必須)
tailscale serve はクライアントヘッダーを変更せずに転送するため、Tailscale ACL を使用してポートへの直接アクセスをイングレスノードのみに制限する必要があります。
Tailscale / Headscale ≥ 0.29:
"grants": [
{ "src": ["tag:ingress"], "dst": ["tag:agent-host"], "ip": ["tcp:8787"] },
]Headscale ≤ 0.28:
acls:
- action: accept
src: ["ingress-node"]
dst: ["agent-host:8787"]
- action: accept
src: ["my-phone", "my-laptop"]
dst: ["agent-host:1-8786", "agent-host:8788-65535"]Collie の設定(.env):
# proxy terminates TLS; this hop is tailnet-internal
COLLIE_SERVE_MODE=http
COLLIE_HOST=127.0.0.1 # keep loopback (default)
# header your forward-auth injects — REQUIRED here
COLLIE_DEVICE_HEADER=X-Tailnet-Device
# ids allowed to drive; others + header-less → read-only
COLLIE_DEVICE_ALLOWLIST=my-phone,my-laptop
# REQUIRED — the Host the proxy forwards. COLLIE_TAILSCALE_HOSTS carries the bare
# tailnet name `collie start` found; a rewritten Host is yours to list.
# COLLIE_ALLOW_ANY_HOST=1 opts out.
COLLIE_PUBLIC_HOSTS=host:8787,host.your-tailnet.ts.net:8787
# the public origin the browser actually uses
COLLIE_ALLOWED_ORIGINS=https://collie.example.comCOLLIE_TRUSTED_USER は背後のエンドユーザーではなく、イングレスノードの ID に一致します。
検証:
イングレス以外の tailnet ピアから:
$ curl -s https://collie.example.com/api/snapshot | jq -c .device
{"enforced":true,"device":"my-phone","authorized":true}
$ curl -s --max-time 10 -H 'X-Tailnet-Device: my-phone' http://host.your-tailnet.ts.net:8787/api/snapshot
curl: (28) Connection timed outエージェントホスト上:
$ curl -s http://127.0.0.1:8787/api/snapshot | jq -c .device
{"enforced":true,"device":null,"authorized":false}---
バリアント E: その他のメッシュまたはトンネル(NetBird、ZeroTier、Cloudflare Tunnel)
トンネルまたはプロキシの接続先を 127.0.0.1:$COLLIE_PORT に設定します。
Collie の設定(.env):
COLLIE_SKIP_SERVE=1 # never run tailscale serve
# REQUIRED — exact public host; Host validation fails closed and finds no
# tailnet name here
COLLIE_PUBLIC_HOSTS=collie.example.com
# exact public origin for the same-origin gate
COLLIE_ALLOWED_ORIGINS=https://collie.example.comルール:
- バリアント B プロキシルールを適用します。
COLLIE_TRUSTED_USERは Tailscale がないと機能しません。COLLIE_DEVICE_HEADERまたはトンネルの認証を使用してください。- PWA のキャッシュと
COLLIE_PUBLIC_HOSTSが有効なまま維持されるよう、静的なホスト名を使用してください。
---
1 台のホストで複数の Collie を実行する
共有システム (ADR 0001) 上でユーザーごとに独立したインスタンスをホストする場合:
# ~/.config/herdr/plugins/config/herdr.collie/.env — one per Unix user
# this user's loopback bridge port — unique per user
COLLIE_PORT=8801
# this user's tailnet https listener — unique per user
COLLIE_SERVE_PORT=8443
COLLIE_TRUSTED_USER=dev-a@example.com # only this tailnet login may drive these agentsスタンドアロンバイナリ URL チェック: bin/collie url。Herdr プラグイン URL チェック: herdr plugin action invoke url --plugin herdr.collie。
要件:
- 異なる Unix ユーザーと個別の
herdr/Collie インスタンスを実行します。 - ポートごとにアクセスを制限するため、常に
COLLIE_TRUSTED_USERを設定してください。 - 初期 Tailscale バインドにはオペレーター権限での設定が必要です。オペレーター権限で
bin/collie serve(または同等の Herdr コマンド) を実行してください。
COLLIE_SERVE_PORT はインスタンスのエントリポートを設定します。COLLIE_INSTANCE は同じホスト上に専用のサービスユニットと設定を持つ分離されたインスタンスを作成します (単一ホスト上での複数 Collie インスタンスの実行)。
---
単一ホスト上での複数 Collie インスタンスの実行
同じマシン上で2つ目の独立した Collie を実行する場合はこの設定を使用します。例えば、安定版と作業用コピーを並行して実行する場合や、Herdr が管理するデフォルトの構成の横で2つ目のマルチプレクサー (tmux/zellij) を実行する場合などです。各インスタンスには専用のポート、設定、状態ディレクトリ、サービスユニットが割り当てられます。これは Unix ユーザーごとに1つのインスタンスを割り当てる 1 台のホストで複数の Collie を実行する とは異なり、1人のユーザーが複数のインスタンスを実行します。
2つ目のインスタンスを作成する
COLLIE_INSTANCE=<name> を設定してインスタンス名を指定します。名前は [a-z0-9-]{1,16} に一致する必要があります。名前付きインスタンスには明示的な COLLIE_PORT の指定も必要です。このポートが指定されていない場合、CLI はエラーを出して終了します。デフォルト値は推論されません。
~/.config/herdr/plugins/config/herdr.collie-<name>/.env を作成:
COLLIE_INSTANCE=<name> # required — [a-z0-9-], max 16 chars
COLLIE_PORT=8788 # required for a named instance — no default is inferred
# Unset, this defaults to ~/.local/state/collie, with no instance suffix
COLLIE_STATE_DIR=/home/you/.local/state/collie-<name>COLLIE_INSTANCE、COLLIE_PORT、COLLIE_STATE_DIR はすべてその .env ファイルに記述できます。マージされた環境変数からインスタンスが解決されます。HERDR_PLUGIN_CONFIG_DIR は CLI が設定を検索するディレクトリを上書きします。
インスタンスごとにサービスユニットを実行する
環境変数でインスタンスが設定されている場合、collie start がユニットを書き込みます。オペレーターが手動で作成する必要はありません。macOS では代わりに ~/Library/LaunchAgents/ の下に launchd plist を書き込みます。ユニットは COLLIE_PORT、COLLIE_INSTANCE、COLLIE_PLUGIN_ROOT、HERDR_PLUGIN_CONFIG_DIR、EnvironmentFile=-<config dir>/.env を設定します。ExecStart の --instance <name> は、単一のバイナリを共有する2つのインスタンスがそれぞれのブリッジを識別できるようにするためだけに存在します (cli/unit.ts systemdUnit()):
ExecStart=<root>/bin/collie _exec-bridge --instance <name>
Environment=COLLIE_PORT=8788
Environment=COLLIE_INSTANCE=<name>
Environment=COLLIE_PLUGIN_ROOT=<root>
Environment=HERDR_PLUGIN_CONFIG_DIR=<config dir>
EnvironmentFile=-<config dir>/.envCLI はインスタンスごとに tailscale serve ハンドラーファイルを保持するため、1つのインスタンスで unserve を実行しても別のインスタンスのマッピングが削除されることはありません。
CLI から名前付きインスタンスを対象にする
すべての CLI サブコマンド (pair、devices、url、qr、pack …、logs、push-test、…) は、プロセス環境変数から対象のインスタンスを解決します。サブコマンドを実行する前に COLLIE_INSTANCE を設定してください:
COLLIE_INSTANCE=next bin/collie pair
COLLIE_INSTANCE=next bin/collie devices listCOLLIE_INSTANCE を指定しない場合、CLI は Herdr に問い合わせます。Herdr はサフィックスのないプラグインのみを追跡するため、コマンドは最初のインスタンスに対して実行されます。誤ったインスタンスを変更しないよう、1台のホストで複数のインスタンスを実行する場合は常に明示的に COLLIE_INSTANCE を設定してください。
拒否ルール
COLLIE_INSTANCE が設定されているにもかかわらず herdr.collie-<name>/.env がない場合、CLI はエラーを出して終了します。他のインスタンスの設定にフォールバックすることはありません。
ペアリングはインスタンス単位
collie pairは、そのインスタンスの状態ディレクトリにペアリングコードを書き込みます。各インスタンスには独自の状態ディレクトリがあり、COLLIE_PUBLIC_URLまたは専用のポートを経由した独自の入り口があるため、表示されるQRコードにはそのインスタンス自体のURLがエンコードされています。スマートフォンでそのインスタンス固有のURLを開き、「Settings」→「Paired devices」でコードを入力します。あるインスタンス向けに生成されたコードを使用して、他のインスタンスにデバイスをペアリングすることはできません(デバイスをペアリングする)。
---
スタンバイ用の導線: pack のフェイルオーバーパス
pack デプロイ用。lead が到達不能になった場合に引き継ぐ事前承認済みの deputy を設定します (ADR 0027、ADR 0028、PACK_PROTOCOL.md §18)。
Deputy と lead の設定:
| キー | デフォルト | 説明 |
|---|---|---|
COLLIE_STANDBY_PORT | (未設定) | standbyリスナー用のポート。未設定の場合はstandby doorが無効化される。leadとdeputyで一致させる必要がある。 |
COLLIE_STANDBY_HOST | 127.0.0.1 | バインドアドレス(ローカルプロキシの場合は127.0.0.1、リモートの場合はオーバーレイIP)。 |
COLLIE_STANDBY_ARM_MS | max(30000, 2.5 × COLLIE_POLL_IDLE_MS) | arm状態に移行するまでに必要なleadの無応答時間。 |
leadとdeputyの両方で、COLLIE_STANDBY_PORTに同一の未使用ポートを設定する。
前提条件: 1つのホスト名と2つのバックエンド
PWAの登録とデバイスクレデンシャルを共有するため、leadとdeputyは同一オリジンから配信する必要がある。統合ingressのないスタンドアロンpackはbin/collie promote経由で復旧する(Herdrの場合: herdr plugin action invoke promote --plugin herdr.collie、PACK_PROTOCOL §14.4)。
Traefikの設定例:
http:
routers:
collie:
rule: "Host(`collie.example.com`)"
service: collie-pack
tls: {}
services:
collie-pack:
failover:
service: collie-lead
fallback: collie-deputy
collie-lead:
loadBalancer:
servers:
- url: "http://lead.internal:8787"
healthCheck:
path: /standby/health
interval: 5s
timeout: 2s
collie-deputy:
loadBalancer:
servers:
- url: "http://deputy.internal:8788" # COLLIE_STANDBY_PORT
healthCheck:
path: /standby/health
interval: 5s
timeout: 2sヘルスチェックの応答: leadは200を返す(deposed時は200以外)。deputyはarm状態になるまで503を返し、その後200を返す。
システムが正常なうちに一度だけ設定を行う
leadでのスタンドアロン設定:
bin/collie pair
bin/collie pack deputy nas
bin/collie pack statusleadでのHerdr設定:
herdr plugin action invoke pair --plugin herdr.collie
herdr plugin action invoke pack --plugin herdr.collie deputy nas
herdr plugin action invoke pack --plugin herdr.collie statusdeputyでCOLLIE_STANDBY_PORT=8788を設定して再起動する。deputy warrantを読み込むため、すべてのピアを再起動すること。https://collie.example.com/standbyで確認する。
⚠️ deputyはスーパーバイザーで管理する必要がある
テイクオーバー中、deputyは状態を書き出し、プロセスの再起動(bridge/index.ts)をトリガーするためにステータスコード75(EX_TEMPFAIL)で終了する。
非ゼロの終了コードでスーパーバイザーが再起動するように設定する(systemdの場合: Restart=alwaysまたはRestart=on-failure)。
障害発生時 — ランブック
https://collie.example.comを開く。- leadが異常な場合、プロキシは
/standbyにルーティングする。 - deputyのstandby状態を確認する。
- テイクオーバーを選択する。
- deputyはピアの合意を検証し、warrantを適用して
75で終了する。 - スーパーバイザーがプロセスを再起動し、PWAが新しいleadとしてリロードされる。
復旧後:
- deposedノードのピアアドレスを更新する: スタンドアロンは
bin/collie pack set-address <member> <host:port>、Herdrはherdr plugin action invoke pack --plugin herdr.collie set-address <member> <host:port>。 - 新しいdeputyを割り当てる: スタンドアロンは
bin/collie pack deputy <member>、Herdrはherdr plugin action invoke pack --plugin herdr.collie deputy <member>。 - すべてのメンバーが再接続するまで
pack rotateを実行しないこと。