04/Documentation
Variantes de despliegue B–E
Puertas de entrada alternativas a la predeterminada: un proxy con reconocimiento de identidad, un proxy inverso sin Tailscale, un ingress externo al host, varios Collies en un mismo host (uno por usuario o varias instancias para un solo usuario) y la puerta de reserva de un pack
El puente se vincula a 127.0.0.1. Los despliegues varían según el ingreso y la verificación de identidad. Variante A (tailscale serve sin cifrar) se encuentra en el README. Los requisitos de seguridad de docs/security.md se aplican a todas las configuraciones.
Los dispositivos individuales también se pueden autorizar sin proxy mediante emparejamiento.
---
Variante B: proxy consciente de la identidad + autorización por dispositivo
Collie lee un ID de dispositivo opaco desde COLLIE_DEVICE_HEADER y comprueba COLLIE_DEVICE_ALLOWLIST. Los ID en la lista de permitidos reciben acceso de escritura; los ID ausentes o no listados reciben acceso de solo lectura (las lecturas, instantáneas y listas de sesiones permanecen abiertas; la entrada de terminal, las cargas y las acciones de paneles se bloquean).
Configuración de 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=1Requisitos del proxy:
- Autenticar el dispositivo (mTLS, SSO, forward-auth).
- Sobrescribir el encabezado del dispositivo en cada solicitud ascendente para evitar la suplantación de clientes.
- Hacer proxy a loopback (
127.0.0.1:$COLLIE_PORT). - Reenviar el
Hostpúblico sin modificaciones, o listar el origen público enCOLLIE_ALLOWED_ORIGINSpara superar la comprobación de mismo origen.
Ejemplo de configuración de Nginx:
location / {
proxy_set_header X-Device-Id $device_id;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:8787;
}Verifique la inyección y sobrescritura de encabezados desde un dispositivo externo:
$ 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}Si la segunda comprobación devuelve "device":"my-phone", el proxy está añadiendo en lugar de sobrescribir.
Notas operativas:
- El acceso directo por loopback (
http://127.0.0.1:$COLLIE_PORT) no envía encabezados y es de solo lectura. - Para ejecutar comandos localmente mediante curl, pase el encabezado explícitamente a loopback:
curl -H 'X-Device-Id: my-laptop' http://127.0.0.1:$COLLIE_PORT/api/... - Revoque el acceso eliminando el ID de
COLLIE_DEVICE_ALLOWLISTy reiniciando (Standalone:bin/collie restart; Herdr:herdr plugin action invoke restart --plugin herdr.collie). - No active
COLLIE_DEVICE_HEADERentailscale servebásico; no sobrescribe encabezados.
---
Variante C: proxy inverso como única puerta de entrada (sin Tailscale)
Utilice esta opción al ejecutar fuera de Tailscale o detrás de un proxy TLS/SSO dedicado. COLLIE_SKIP_SERVE=1 evita que Collie administre tailscale serve.
Se aplican los cuatro requisitos de proxy de Variante B.
Configuración de ejemplo de Caddy:
collie.example.com {
reverse_proxy 127.0.0.1:8787 {
header_up X-Device-Id {your_device_id}
header_up Host {host}
}
}Configuración de 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 no tiene efecto sin Tailscale. Los encabezados por dispositivo o la propia autenticación del proxy actúan como control de acceso.
Enrutamiento de /auth/ y almacenamiento en caché
- Pase
/sw.jsyindex.htmlcon el origenCache-Control(no-cache). No bloquee los recursos estáticos para clientes no autenticados, o los service workers no podrán actualizarse. - Enrute los flujos de autenticación bajo
/auth/*(o/cdn-cgi/access/para Cloudflare Access). Los service workers omiten el almacenamiento en caché para/auth/*. - Las redirecciones de forward-auth en solicitudes a la API se tratan como 401 para mostrar la interfaz de inicio de sesión. Las configuraciones con Authentik deben enrutar
/auth/a/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 { ... }
}
}Verifique el endpoint y las reglas de almacenamiento en caché:
$ 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---
Variante D: proxy de identidad fuera del host a través de la tailnet
Utilice esta opción al enrutar tráfico a través de un nodo de entrada centralizado de 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 CollieSe aplican los requisitos de Variante B, excepto que el proxy apunta al endpoint HTTP de Tailscale del host en lugar de loopback.
ACL de Tailscale (obligatorio)
Dado que tailscale serve reenvía los encabezados del cliente sin modificaciones, las ACL de Tailscale deben restringir el acceso directo al puerto únicamente al nodo de entrada.
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"]Configuración de 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 coincide con la identidad del nodo de entrada, no con la del usuario final detrás de él.
Verificación:
Desde un par de tailnet que no sea de entrada:
$ 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 outEn el host del agente:
$ curl -s http://127.0.0.1:8787/api/snapshot | jq -c .device
{"enforced":true,"device":null,"authorized":false}---
Variante E: cualquier otra red en malla o túnel (NetBird, ZeroTier, Cloudflare Tunnel)
Apunte el túnel/proxy a 127.0.0.1:$COLLIE_PORT.
Configuración de 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.comReglas:
COLLIE_TRUSTED_USERestá inactivo sin Tailscale. UtiliceCOLLIE_DEVICE_HEADERo la autenticación del túnel.- Utilice un nombre de host estático para que el almacenamiento en caché de la PWA y
COLLIE_PUBLIC_HOSTSsigan siendo válidos.
---
Varios Collie en un mismo host
Para alojar instancias independientes por usuario en un sistema compartido (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 agentsVerificación de URL de binario independiente: bin/collie url. Verificación de URL de plugin de Herdr: herdr plugin action invoke url --plugin herdr.collie.
Requisitos:
- Ejecutar usuarios de Unix distintos e instancias separadas de
herdr/Collie. - Definir siempre
COLLIE_TRUSTED_USERpara restringir el acceso por puerto. - Las vinculaciones iniciales de Tailscale requieren configuración de operador: ejecute
bin/collie serve(o el equivalente de Herdr) con privilegios de operador.
COLLIE_SERVE_PORT define el puerto de entrada para una instancia. COLLIE_INSTANCE crea una instancia aislada con su propia unidad de servicio y configuración en el mismo host (Múltiples instancias de Collie en un mismo host).
---
Múltiples instancias de Collie en un mismo host
Use esta configuración para ejecutar un segundo Collie independiente en la misma máquina. Los ejemplos incluyen ejecutar una versión estable junto a una copia de trabajo, o ejecutar un segundo multiplexor (tmux/zellij) junto al valor predeterminado administrado por Herdr. Cada instancia obtiene un puerto dedicado, configuración, directorio de estado y unidad de servicio. Esto difiere de Varios Collie en un mismo host, que asigna una instancia por usuario de Unix. En este caso, un solo usuario ejecuta múltiples instancias.
Crear la segunda instancia
Defina COLLIE_INSTANCE=<name> para nombrar la instancia. El nombre debe coincidir con [a-z0-9-]{1,16}. Una instancia con nombre también requiere un COLLIE_PORT explícito. La CLI finaliza con un error si falta este puerto; no infiere ningún valor predeterminado.
Crear ~/.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 y COLLIE_STATE_DIR pueden residir en ese archivo .env. El entorno fusionado resuelve la instancia. HERDR_PLUGIN_CONFIG_DIR anula el directorio donde la CLI busca la configuración.
Ejecutar una unidad de servicio por instancia
collie start escribe la unidad cuando el entorno define la instancia. El operador no la escribe manualmente. En macOS escribe un plist de launchd en ~/Library/LaunchAgents/ en su lugar. La unidad define COLLIE_PORT, COLLIE_INSTANCE, COLLIE_PLUGIN_ROOT, HERDR_PLUGIN_CONFIG_DIR y EnvironmentFile=-<config dir>/.env. --instance <name> en ExecStart existe únicamente para que dos instancias que comparten un binario puedan distinguir sus bridges (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>/.envLa CLI mantiene un archivo de controlador tailscale serve por instancia, por lo que ejecutar unserve en una instancia no elimina la asignación de otra.
Apuntar a una instancia con nombre desde la CLI
Cada verbo de la CLI (pair, devices, url, qr, pack …, logs, push-test, …) resuelve su instancia de destino a partir del entorno del proceso. Defina COLLIE_INSTANCE antes de invocar el verbo:
COLLIE_INSTANCE=next bin/collie pair
COLLIE_INSTANCE=next bin/collie devices listSin COLLIE_INSTANCE, la CLI consulta a Herdr. Herdr solo rastrea el plugin sin sufijo, por lo que el comando se ejecuta en la primera instancia. Defina siempre COLLIE_INSTANCE explícitamente al ejecutar múltiples instancias en un host para evitar modificar la instancia incorrecta.
La regla de rechazo
Si COLLIE_INSTANCE está definido pero falta herdr.collie-<name>/.env, la CLI finaliza con un error. No recurre a la configuración de otra instancia.
El emparejamiento es por instancia
collie pair escribe un código de emparejamiento en el directorio de estado de esa instancia. El QR que imprime codifica la URL propia de esa instancia, ya que cada instancia tiene su propio directorio de estado y, mediante COLLIE_PUBLIC_URL o su propio puerto, su propia puerta de acceso. Abra la URL específica de esa instancia en el teléfono e introduzca el código en Settings → Paired devices. Un código generado para una instancia no puede emparejar un dispositivo con ninguna otra instancia (Emparejar un dispositivo).
---
La puerta de reserva: la ruta de conmutación por error de un pack
Para despliegues de pack. Configura un deputy preautorizado para asumir el control si el líder queda inaccesible (ADR 0027, ADR 0028, PACK_PROTOCOL.md §18).
Configuración de deputy y líder:
| Clave | Predeterminado | Qué hace |
|---|---|---|
COLLIE_STANDBY_PORT | (no definido) | Puerto para el listener en standby. Si no se define, se desactiva la puerta de standby. Debe coincidir en el lead y en el deputy. |
COLLIE_STANDBY_HOST | 127.0.0.1 | Dirección de enlace (127.0.0.1 para proxys locales, IP de overlay para remotos). |
COLLIE_STANDBY_ARM_MS | max(30000, 2.5 × COLLIE_POLL_IDLE_MS) | Duración del silencio del lead requerida antes del armado. |
Defina COLLIE_STANDBY_PORT con un puerto idéntico y sin usar tanto en el lead como en el deputy.
Requisito previo: un hostname, dos backends
El lead y el deputy deben servirse desde el mismo origen para compartir el registro de la PWA y las credenciales del dispositivo. Los packs independientes sin un ingress unificado se recuperan mediante bin/collie promote (o Herdr: herdr plugin action invoke promote --plugin herdr.collie; PACK_PROTOCOL §14.4).
Configuración de ejemplo para 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: 2sRespuestas del health check: el lead devuelve 200 (distinto de 200 cuando está depuesto); el deputy devuelve 503 hasta que se arma, y luego 200.
Configúrelo una vez, mientras todo funcione correctamente
Configuración independiente en el lead:
bin/collie pair
bin/collie pack deputy nas
bin/collie pack statusConfiguración con Herdr en el lead:
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 statusEn el deputy, configure COLLIE_STANDBY_PORT=8788 y reinicie. Asegúrese de reiniciar todos los peers para cargar el deputy warrant. Verifique en https://collie.example.com/standby.
⚠️ El deputy debe estar supervisado
Durante la toma de control, el deputy guarda el estado y finaliza con el código de salida 75 (EX_TEMPFAIL) para activar el reinicio del proceso (bridge/index.ts).
Asegúrese de que el supervisor se reinicie con códigos de salida distintos de cero (systemd: Restart=always o Restart=on-failure).
En caso de fallo: el runbook
- Abra
https://collie.example.com. - Cuando el lead no responde correctamente, el proxy enruta hacia
/standby. - Revise el estado de standby del deputy.
- Seleccione Tomar el control.
- El deputy verifica el consenso de los peers, aplica el warrant y finaliza con
75. - El supervisor reinicia el proceso; la PWA se recarga como el nuevo lead.
Tras la recuperación:
- Actualice la dirección del peer del nodo depuesto: independiente
bin/collie pack set-address <member> <host:port>o Herdrherdr plugin action invoke pack --plugin herdr.collie set-address <member> <host:port>. - Asigne un nuevo deputy: independiente
bin/collie pack deputy <member>o Herdrherdr plugin action invoke pack --plugin herdr.collie deputy <member>. - No ejecute
pack rotatehasta que todos los miembros se hayan reconectado.