Aller au contenu
ColliePWA

04/Documentation

Variantes de déploiement B–E

Points d'entrée autres que celui par défaut : un proxy avec gestion des identités, un reverse proxy sans Tailscale, un ingress hors hôte, plusieurs Collies sur un seul hôte (un par utilisateur, ou plusieurs instances pour un seul utilisateur), et le point d'entrée de secours d'une crew

Le pont écoute sur 127.0.0.1. Les déploiements diffèrent par leur point d'entrée et la vérification des identités. La Variante A (tailscale serve brut) se trouve dans le README. Les exigences de sécurité de docs/security.md s'appliquent à toutes les configurations.

Des appareils individuels peuvent aussi être autorisés sans proxy via association.

---

Variante B : proxy avec gestion des identités + autorisation par appareil

Collie lit un identifiant d'appareil opaque depuis COLLIE_DEVICE_HEADER et vérifie COLLIE_DEVICE_ALLOWLIST. Les identifiants sur liste autorisée reçoivent un accès en écriture ; les identifiants manquants ou non listés reçoivent un accès en lecture seule (les lectures, instantanés et listes de sessions restent accessibles ; les saisies dans le terminal, les téléversements et les actions sur les panneaux sont bloqués).

Configuration 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=1

Exigences pour le proxy :

  1. Authentifiez l'appareil (mTLS, SSO, forward-auth).
  2. Écrasez l'en-tête d'appareil sur chaque requête vers l'amont pour empêcher l'usurpation par le client.
  3. Transférez vers la boucle locale (127.0.0.1:$COLLIE_PORT).
  4. Transférez le Host public sans modification, ou listez l'origine publique dans COLLIE_ALLOWED_ORIGINS pour passer la vérification d'origine identique (same-origin).

Exemple de configuration Nginx :

location / {
    proxy_set_header X-Device-Id $device_id;
    proxy_set_header Host        $host;
    proxy_pass http://127.0.0.1:8787;
}

Vérifiez l'injection et l'écrasement de l'en-tête depuis un appareil externe :

$ 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 seconde vérification renvoie "device":"my-phone", le proxy effectue un ajout au lieu d'un écrasement.

Notes d'exploitation :

  • L'accès direct en boucle locale (http://127.0.0.1:$COLLIE_PORT) n'envoie aucun en-tête et s'effectue en lecture seule.
  • Pour exécuter des commandes localement avec curl, transmettez explicitement l'en-tête à la boucle locale : curl -H 'X-Device-Id: my-laptop' http://127.0.0.1:$COLLIE_PORT/api/...
  • Révoquez l'accès en supprimant l'identifiant de COLLIE_DEVICE_ALLOWLIST et en redémarrant (Standalone : bin/collie restart ; Herdr : herdr plugin action invoke restart --plugin herdr.collie).
  • N'activez pas COLLIE_DEVICE_HEADER sur un simple tailscale serve ; il n'écrase pas les en-têtes.
  • Si le proxy s'exécute sur un autre hôte, consultez Variante D.

---

Variante C : reverse proxy comme unique point d'entrée (sans Tailscale)

Utilisez cette option si vous travaillez hors de Tailscale ou derrière un proxy TLS/SSO dédié. COLLIE_SKIP_SERVE=1 empêche Collie de gérer tailscale serve.

Les quatre exigences de proxy issues de Variante B s'appliquent.

Exemple de configuration Caddy :

collie.example.com {
    reverse_proxy 127.0.0.1:8787 {
        header_up X-Device-Id {your_device_id}
        header_up Host {host}
    }
}

Configuration 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 (crew)
# COLLIE_PUBLIC_URL=https://collie.example.com

COLLIE_TRUSTED_USER n'a aucun effet sans Tailscale. Les en-têtes par appareil ou l'authentification propre au proxy servent de contrôle d'accès.

Routage de /auth/ et mise en cache

  • Transmettez /sw.js et index.html avec l'origine Cache-Control (no-cache). Ne bloquez pas les ressources statiques pour les clients non authentifiés, sinon les service workers ne pourront pas se mettre à jour.
  • Acheminez les flux d'authentification sous /auth/* (ou /cdn-cgi/access/ pour Cloudflare Access). Les service workers contournent le cache pour /auth/*.
  • Les redirections forward-auth sur les requêtes d'API sont traitées comme des erreurs 401 pour afficher l'interface de connexion. Les configurations Authentik doivent acheminer /auth/ vers /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 { ... }
    }
}

Vérifiez le point de terminaison et les règles de mise en cache :

$ 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 d'identité externe sur le tailnet

Utilisez cette méthode pour router le trafic via un nœud d'entrée Tailscale centralisé.

Un nœud d'entrée authentifie le téléphone à la périphérie du tailnet, et Collie lui-même ne répond que sur loopback.phoneingress nodeagent hostCollie127.0.0.1:8787httpsTLS + forward-auth,sets the device headerhttp, tailnet only —WireGuard already encrypts ittailscale serve, tailnet-onlyloopback
Un nœud d'entrée authentifie le téléphone à la périphérie du tailnet, et Collie lui-même ne répond que sur loopback.

Les exigences de Variante B s'appliquent, sauf que le proxy cible le point de terminaison HTTP Tailscale de l'hôte au lieu du loopback.

ACLs Tailscale (obligatoire)

Comme tailscale serve transmet les en-têtes du client sans modification, les ACLs Tailscale doivent restreindre l'accès direct aux ports au seul nœud d'entrée.

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"]

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

COLLIE_TRUSTED_USER correspond à l'identité du nœud d'entrée, et non à celle de l'utilisateur final qui l'utilise.

Vérification :

Depuis un pair du tailnet qui n'est pas le nœud d'entrée :

$ 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

Sur l'hôte de l'agent :

$ curl -s http://127.0.0.1:8787/api/snapshot | jq -c .device
{"enforced":true,"device":null,"authorized":false}

---

Variante E : tout autre maillage ou tunnel (NetBird, ZeroTier, Cloudflare Tunnel)

Pointez le tunnel/proxy vers 127.0.0.1:$COLLIE_PORT.

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

Règles :

  1. Appliquez les règles de proxy de Variante B.
  2. COLLIE_TRUSTED_USER est inactif sans Tailscale. Utilisez COLLIE_DEVICE_HEADER ou l'authentification du tunnel.
  3. Utilisez un nom d'hôte statique pour que le cache de la PWA et COLLIE_PUBLIC_HOSTS restent valides.

---

Plusieurs instances de Collie sur un même hôte

Pour héberger des instances indépendantes par utilisateur sur un système partagé (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

Vérification de l'URL du binaire autonome : bin/collie url. Vérification de l'URL du plugin Herdr : herdr plugin action invoke url --plugin herdr.collie.

Prérequis :

  • Utilisez des utilisateurs Unix distincts et des instances séparées de herdr/Collie.
  • Définissez toujours COLLIE_TRUSTED_USER pour restreindre l'accès par port.
  • Les liaisons Tailscale initiales nécessitent une configuration opérateur : exécutez bin/collie serve (ou l'équivalent Herdr) avec les privilèges d'opérateur.

COLLIE_SERVE_PORT définit le port d'entrée d'une instance. COLLIE_INSTANCE crée une instance isolée avec sa propre unité de service et sa configuration sur le même hôte (Plusieurs instances de Collie sur un même hôte).

---

Plusieurs instances de Collie sur un même hôte

Utilisez cette configuration pour exécuter un second Collie distinct sur la même machine. Cela permet par exemple d'exécuter une version stable aux côtés d'une copie de travail, ou d'exécuter un second multiplexeur (tmux/zellij) à côté de celui géré par défaut par Herdr. Chaque instance dispose d'un port dédié, d'une configuration, d'un répertoire d'état et d'une unité de service. Cela diffère de Plusieurs instances de Collie sur un même hôte, qui attribue une instance par utilisateur Unix. Ici, un seul utilisateur exécute plusieurs instances.

Créez la seconde instance

Définissez COLLIE_INSTANCE=<name> pour nommer l'instance. Le nom doit correspondre à [a-z0-9-]{1,16}. Une instance nommée nécessite également un COLLIE_PORT explicite. Le CLI s'arrête avec une erreur si ce port est manquant ; il ne déduit aucune valeur par défaut.

Créez ~/.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 et COLLIE_STATE_DIR peuvent tous résider dans ce fichier .env. L'environnement fusionné résout l'instance. HERDR_PLUGIN_CONFIG_DIR remplace le répertoire dans lequel le CLI recherche la configuration.

Exécuter une unité de service par instance

collie start écrit l'unité lorsque l'environnement définit l'instance. L'opérateur ne l'écrit pas à la main. Sur macOS, il écrit à la place un fichier plist launchd sous ~/Library/LaunchAgents/. L'unité définit COLLIE_PORT, COLLIE_INSTANCE, COLLIE_PLUGIN_ROOT, HERDR_PLUGIN_CONFIG_DIR et EnvironmentFile=-<config dir>/.env. --instance <name> sur ExecStart existe uniquement pour que deux instances partageant un même binaire puissent distinguer leurs ponts (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

Le CLI conserve un fichier gestionnaire tailscale serve par instance, de sorte que l'exécution de unserve sur une instance ne supprime pas le mappage d'une autre.

Cibler une instance nommée depuis le CLI

Chaque verbe du CLI (pair, devices, url, qr, crew …, logs, push-test, …) résout son instance cible à partir de l'environnement du processus. Définissez COLLIE_INSTANCE avant d'invoquer le verbe :

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

Sans COLLIE_INSTANCE, le CLI interroge Herdr. Herdr ne suit que le plugin sans suffixe, la commande s'exécute donc sur la première instance. Définissez toujours COLLIE_INSTANCE explicitement lors de l'exécution de plusieurs instances sur un même hôte pour éviter de modifier la mauvaise instance.

La règle de refus

Si COLLIE_INSTANCE est défini mais que herdr.collie-<name>/.env est manquant, le CLI se termine avec une erreur. Il ne bascule pas sur la configuration d'une autre instance.

L'appairage s'effectue par instance

collie pair écrit un code d'appairage dans le répertoire d'état de cette instance. Le QR code affiché encode l'URL propre à cette instance, car chaque instance possède son propre répertoire d'état et, via COLLIE_PUBLIC_URL ou son propre port, son propre point d'entrée. Ouvrez l'URL spécifique de cette instance sur le téléphone et saisissez le code sous Settings → Paired devices. Un code généré pour une instance ne peut appairer un appareil avec aucune autre instance (Associer un appareil).

---

L'accès de secours : chemin de basculement d'une équipe

Pour les déploiements crew. Configure un deputy préautorisé pour prendre le relais si le lead devient injoignable (ADR 0027, ADR 0028, CREW_PROTOCOL.md §18).

Paramètres du deputy et du lead :

CléValeur par défautDescription
COLLIE_STANDBY_PORT(non défini)Port pour l'écouteur standby. Laisser non défini désactive l'accès standby. Doit être identique sur le lead et le deputy.
COLLIE_STANDBY_HOST127.0.0.1Adresse de liaison (127.0.0.1 pour les proxys locaux, IP d'overlay pour les accès distants).
COLLIE_STANDBY_ARM_MSmax(30000, 2.5 × COLLIE_POLL_IDLE_MS)Durée de silence du lead requise avant armement.

Définissez COLLIE_STANDBY_PORT sur un port identique et inutilisé sur le lead et le deputy.

Le prérequis : un nom d'hôte, deux backends

Le lead et le deputy doivent être servis depuis la même origine pour partager l'enregistrement PWA et les identifiants des appareils. Un crew autonome sans ingress unifié récupère via bin/collie promote (ou Herdr : herdr plugin action invoke promote --plugin herdr.collie ; CREW_PROTOCOL §14.4).

Exemple de configuration Traefik :

http:
  routers:
    collie:
      rule: "Host(`collie.example.com`)"
      service: collie-crew
      tls: {}

  services:
    collie-crew:
      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

Réponses du health check : le lead renvoie 200 (non-200 lorsqu'il est déposé) ; le deputy renvoie 503 jusqu'à son armement, puis 200.

Configurez-le une seule fois, pendant que tout fonctionne correctement

Configuration autonome sur le lead :

bin/collie pair
bin/collie crew deputy nas
bin/collie crew status

Configuration Herdr sur le lead :

herdr plugin action invoke pair --plugin herdr.collie
herdr plugin action invoke crew --plugin herdr.collie deputy nas
herdr plugin action invoke crew --plugin herdr.collie status

Sur le deputy, configurez COLLIE_STANDBY_PORT=8788 et redémarrez. Assurez-vous que tous les pairs redémarrent pour charger le warrant du deputy. Vérifiez sur https://collie.example.com/standby.

⚠️ Le deputy doit être supervisé

Pendant le basculement, le deputy écrit l'état et quitte avec le code de sortie 75 (EX_TEMPFAIL) pour déclencher un redémarrage du processus (bridge/index.ts).

Assurez-vous que le superviseur redémarre sur les codes de sortie non nuls (systemd : Restart=always ou Restart=on-failure).

Jour de panne : le runbook

  1. Ouvrez https://collie.example.com.
  2. Lorsque le lead est défaillant, le proxy redirige vers /standby.
  3. Vérifiez le statut de veille du deputy.
  4. Sélectionnez Prendre le relais.
  5. Le deputy vérifie le consensus des pairs, applique le warrant et quitte avec 75.
  6. Le superviseur redémarre le processus ; la PWA se recharge en tant que nouveau lead.

Après récupération :

  • L'ancien lead se destitue lui-même lors de sa reconnexion (CREW_PROTOCOL.md §8.4).
  • Mettez à jour l'adresse de pair du nœud destitué : Standalone bin/collie crew set-address <member> <host:port> ou Herdr herdr plugin action invoke crew --plugin herdr.collie set-address <member> <host:port>.
  • Attribuez un nouveau deputy : Standalone bin/collie crew deputy <member> ou Herdr herdr plugin action invoke crew --plugin herdr.collie deputy <member>.
  • N'exécutez pas crew rotate tant que tous les membres ne se sont pas reconnectés.

Modifier cette page sur GitHub