跳转至正文
ColliePWA

04/Documentation

部署变体 B–E

默认之外的其他入口:身份感知代理、无 Tailscale 的反向代理、主机外 Ingress、单台主机上的多个 Collie(每位用户一个,或单用户多个实例)以及 pack 的备用入口

Bridge 绑定到 127.0.0.1。不同部署方式的区别在于入口接入和身份验证。方案 A(基础 tailscale serve)请见 README。docs/security.md 中的安全要求适用于所有部署形式。

单台设备也可以不通过代理直接经由 配对 完成授权。

---

方案 B:身份感知代理 + 单设备授权

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

代理要求:

  1. 验证设备身份(mTLS、SSO、forward-auth)。
  2. 在每个发往上游的请求中 覆盖设备请求头,以防止客户端伪造。
  3. 代理到 loopback127.0.0.1:$COLLIE_PORT)。
  4. 原样转发公网 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}

如果第二次检查返回 "device":"my-phone",说明代理是在追加而不是覆盖请求头。

运维说明:

  • 直接访问 loopback(http://127.0.0.1:$COLLIE_PORT)不会发送请求头,权限为只读。
  • 若要在本地通过 curl 执行命令,需要显式向 loopback 传递请求头: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;它不会覆盖请求头。
  • 如果代理运行在另一台主机上,请参阅 变体 D

---

方案 C:反向代理作为唯一入口(不使用 Tailscale)

在 Tailscale 之外或在专用 TLS/SSO 代理后运行时使用此配置。COLLIE_SKIP_SERVE=1 可防止 Collie 管理 tailscale serve

变体 B 中的四项代理要求同样适用。

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.com

没有 Tailscale 时 COLLIE_TRUSTED_USER 不生效。按设备请求头或代理自身的认证将作为访问入口控制。

路由 /auth/ 和缓存

  • 透传 /sw.jsindex.html 并携带源 Cache-Controlno-cache)。不要对未认证客户端拦截静态资源,否则 service worker 无法更新。
  • 将认证流程路由到 /auth/* 下(Cloudflare Access 则路由到 /cdn-cgi/access/)。Service worker 会对 /auth/* 绕过缓存。
  • API 请求上的 Forward-auth 重定向会被视为 401,以显示登录 UI。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 接入的主机外身份代理

通过集中式 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.com

COLLIE_TRUSTED_USER 匹配的是入口节点的身份,而不是其背后的最终用户。

验证:

从非入口 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

在 agent 主机上:

$ 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

规则:

  1. 应用 变体 B 代理规则。
  2. 没有 Tailscale 时 COLLIE_TRUSTED_USER 处于停用状态。请使用 COLLIE_DEVICE_HEADER 或隧道自身的认证。
  3. 使用静态主机名,以保证 PWA 缓存和 COLLIE_PUBLIC_HOSTS 保持有效。

---

单主机运行多个 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 实例

使用此配置在同一台机器上运行第二个独立的 Collie。例如,在工作副本旁运行稳定版本,或在 Herdr 管理的默认多路复用器旁运行第二个多路复用器(tmux/zellij)。每个实例都会获得专用的端口、配置、状态目录和服务单元。这与 单主机运行多个 Collie 不同,后者为每个 Unix 用户分配一个实例。在这里,单个用户即可运行多个实例。

创建第二个实例

设置 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_INSTANCECOLLIE_PORTCOLLIE_STATE_DIR 都可以位于该 .env 文件中。合并后的环境会解析出该实例。HERDR_PLUGIN_CONFIG_DIR 会覆盖 CLI 搜索配置的目录。

为每个实例运行一个服务单元

当环境变量设置了实例时,collie start 会写入该单元文件。操作员无需手动编写。在 macOS 上,它会改为在 ~/Library/LaunchAgents/ 下写入 launchd plist。该单元会设置 COLLIE_PORTCOLLIE_INSTANCECOLLIE_PLUGIN_ROOTHERDR_PLUGIN_CONFIG_DIREnvironmentFile=-<config dir>/.envExecStart 上的 --instance <name> 存在的唯一目的是让共享同一个二进制文件的两个实例能够区分它们各自的桥接(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>/.env

CLI 为每个实例维护一个 tailscale serve 处理程序文件,因此在一个实例上运行 unserve 不会删除另一个实例的映射。

从 CLI 指定命名实例

每个 CLI 动词(pairdevicesurlqrpack …logspush-test、…)都会从进程环境中解析其目标实例。在调用动词之前设置 COLLIE_INSTANCE

COLLIE_INSTANCE=next bin/collie pair
COLLIE_INSTANCE=next bin/collie devices list

如果没有 COLLIE_INSTANCE,CLI 会查询 Herdr。Herdr 仅跟踪无后缀的插件,因此该命令会针对第一个实例执行。在同一台主机上运行多个实例时,务必显式设置 COLLIE_INSTANCE,以避免修改错误的实例。

拒绝规则

如果设置了 COLLIE_INSTANCE 但缺少 herdr.collie-<name>/.env,CLI 将报错退出。它不会回退到另一个实例的配置。

配对基于单个实例

collie pair 会将配对码写入该实例的状态目录中。它输出的二维码编码了该实例自有的 URL,因为每个实例都有独立的状态目录,并通过 COLLIE_PUBLIC_URL 或各自的端口拥有独立的入口。在手机上打开该实例对应的专属 URL,并在“Settings → Paired devices”下输入配对码。为一个实例生成的配对码无法用于将设备配对到任何其他实例(配对设备)。

---

备用入口:pack 的故障转移路径

适用于 pack 部署。配置一个预授权的 deputy,以便在 lead 无法访问时接管(ADR 0027ADR 0028PACK_PROTOCOL.md §18)。

Deputy 和 lead 设置:

默认值作用
COLLIE_STANDBY_PORT(未设置)备用监听器的端口。未设置时禁用备用入口。lead 和 deputy 上的端口必须一致。
COLLIE_STANDBY_HOST127.0.0.1绑定地址(本地代理为 127.0.0.1,远程为覆盖网络 IP)。
COLLIE_STANDBY_ARM_MSmax(30000, 2.5 × COLLIE_POLL_IDLE_MS)激活前要求的 lead 静默时长。

在 lead 和 deputy 上将 COLLIE_STANDBY_PORT 设置为相同的未占用端口。

前置条件:一个主机名,两个后端

lead 和 deputy 必须由同一源提供服务,以共享 PWA 注册和设备凭据。没有统一入口的独立 pack 通过 bin/collie promote 恢复(或 Herdr:herdr plugin action invoke promote --plugin herdr.colliePACK_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(被废黜时返回非 200);deputy 在激活前返回 503,激活后返回 200

在一切正常时完成一次性配置

lead 上的独立配置:

bin/collie pair
bin/collie pack deputy nas
bin/collie pack status

lead 上的 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 status

在 deputy 上配置 COLLIE_STANDBY_PORT=8788 并重启。确保所有 peer 重启以加载 deputy warrant。在 https://collie.example.com/standby 验证。

⚠️ deputy 必须处于受控管理下

接管期间,deputy 写入状态并以状态码 75EX_TEMPFAIL)退出,以触发进程重启(bridge/index.ts)。

确保进程管理器在非零退出码时重启(systemd:Restart=alwaysRestart=on-failure)。

故障场景:应急预案

  1. 打开 https://collie.example.com
  2. 当 lead 处于不健康状态时,代理路由至 /standby
  3. 检查 deputy 备用状态。
  4. 选择 接管
  5. deputy 验证 peer 共识,应用 warrant,并退出 75
  6. 进程管理器重启进程;PWA 重新加载为新 lead。

恢复后:

  • 原 lead 在重新连接后自我废黜(PACK_PROTOCOL.md §8.4)。
  • 更新被废黜节点的 peer 地址:独立 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

在 GitHub 上编辑本页