# ADR-0004 — Déploiement Phase 1 LAN clair + Phase 2 Coolify wildcard TLS - Statut : Accepté - Date : 2026-04-30 - Auteur : FoundingEngineer - Relecture : CEO - Source plan : [BARAAA-14 §5.1 + §6.2 + §6.3 + §6.4](/BARAAA/issues/BARAAA-14#document-plan) - Issue ADR : [BARAAA-18](/BARAAA/issues/BARAAA-18) ## Décision AgentHub se déploie en deux topologies distinctes, **versionnées dans le même repo et la même image Docker**. Phase 1 (MVP) tourne sur le **serveur Ubuntu LTS du founder, branché sur le LAN Barodine, en HTTP/WS clair sur le port 3000**, sans Traefik ni TLS, exposé au LAN uniquement. Phase 2 (cible, non déployée Phase 1) tourne **derrière Coolify + Traefik sur `agenthub.barodine.net`, avec le wildcard `*.barodine.net` pré-provisionné côté Coolify**, sans émission ACME au premier deploy. Deux fichiers `compose.lan.yml` et `compose.coolify.yml` cohabitent dans le repo ; seul le wrapper réseau, les origines CORS et l'éventuelle exposition de port changent. Le bootstrap hôte Phase 1 passe par un script `scripts/bootstrap.sh` idempotent (Docker Engine + compose v2 + user `agenthub` + `ufw` + `unattended-upgrades`). L'**activation** Phase 2 est explicitement hors-scope MVP et trackée via un item Plane séparé (suggestion `AGNHUB-15`), à créer après la livraison Phase 1 et la démo founder LAN. ## Pourquoi deux topologies dans le même ADR Pour deux raisons concrètes. **Réversibilité préservée** : la même image, le même `Dockerfile`, le même schéma BDD, les mêmes variables d'env (à `ALLOWED_ORIGINS` près) tournent dans les deux modes. Le passage Phase 1 → Phase 2 est un changement d'orchestration, pas un changement d'archi — figer ça dans un seul ADR évite qu'un ADR-0005 redéfinisse l'archi alors que c'est juste l'enveloppe qui bouge. **Coût de retour minimisé** : si la Phase 2 ne se fait jamais (changement de stratégie produit, fournisseur Coolify abandonné), on jette `compose.coolify.yml` et l'ADR §B sans toucher au code applicatif. Si on saute Phase 1 demain (peu probable mais possible si le founder dispose d'un VPS internet immédiat), on jette `compose.lan.yml` et le bootstrap LAN. ## Section A — Phase 1 LAN clair (déployée MVP) ### A.1 Hôte cible - **Serveur Ubuntu LTS founder** (Ubuntu 22.04 ou 24.04 LTS, à jour) — fourni par Barodine sur le LAN ([comment fe3df1aa](/BARAAA/issues/BARAAA-14#comment-fe3df1aa-7ff8-47a8-b674-3f1057adc762)). - **Specs minimales** : 2 vCPU / 4 Go RAM / 40 Go SSD. - **Réseau** : IP fixe LAN ou hostname mDNS (`agenthub.local` via Avahi), à confirmer ops Barodine au moment du J10. Pas bloquant : par défaut on prend l'IP fixe, l'option mDNS reste documentée dans le runbook. - **Accès** : SSH avec sudo, clés uniquement (pas de mot de passe root). - **Sortie internet uniquement** (push backups chiffrés vers Scaleway). **Aucun port forward entrant**. ### A.2 `compose.lan.yml` Fichier versionné à la racine du repo `agenthub`, Phase 1 = `docker compose -f compose.lan.yml up -d`. Caractéristiques : - Service `app` publie **`3000:3000` sur le LAN** (clé `ports:`), pas de labels Traefik. - `ALLOWED_ORIGINS` whitelist explicite des origines LAN connues (ex. `http://192.168.1.42:3000,http://agenthub.local:3000`). **Refus de `*` et de toute origine non listée**. - Postgres 16 et Redis 7 co-localisés en réseau Docker interne, **non exposés** hors compose. - Image identique à Phase 2 : `registry.barodine.net/agenthub:`. - Secrets via fichier `.env` chargé par compose (mode 600, owner `agenthub`). ### A.3 Bootstrap hôte — `scripts/bootstrap.sh` idempotent Livré J10 dans le repo. Étapes (chacune vérifie l'état avant d'agir) : 1. `apt-get update && apt-get upgrade -y`. 2. Installer + activer `unattended-upgrades` (`dpkg-reconfigure -plow unattended-upgrades`). 3. Créer le user de service `agenthub` (UID 1001, sans login interactif). `usermod -aG docker agenthub` après l'install Docker. 4. Installer **Docker Engine** + **docker compose plugin v2** depuis le repo officiel Docker (méthode `get.docker.com` ou repo apt — **pas le paquet Ubuntu `docker.io`** qui retarde de plusieurs versions). 5. `systemctl enable --now docker`. 6. Préparer `/opt/agenthub` (owner `agenthub:agenthub`, mode 750). 7. Pull repo `agenthub` depuis Forgejo (ou unpack tarball release). 8. Charger `.env` (mode 600, owner `agenthub`). 9. `docker compose -f compose.lan.yml pull && docker compose -f compose.lan.yml up -d`. 10. Smoke local : `curl -fsS http://127.0.0.1:3000/healthz` doit renvoyer 200. Le script est **rejouable** sans effet de bord. Procédure de rollback (`docker compose down`, restore dump) couverte par `docs/RUNBOOK-lan.md` (livrable J10). ### A.4 Sécurité hôte (Phase 1) - `ufw` activé : `default deny`, `allow 22/tcp from `, `allow 3000/tcp from `. Sous-réseau exact à confirmer avec le founder côté ops. - Pas de mot de passe SSH root, clés uniquement. - `unattended-upgrades` couvre les patches de sécurité Ubuntu. - Postgres et Redis jamais exposés hors du réseau Docker. ### A.5 TLS, HSTS, CORS - **TLS : aucun**. HTTP/WS clair sur 3000. - **HSTS : désactivé** Phase 1 (Fastify + helmet, option `hsts: false`). HTTP clair côté LAN, forcer le browser à HTTPS sur un host LAN sans cert produit du faux négatif. - **CSP** : `default-src 'self'`, X-Frame-Options DENY, Referrer-Policy `strict-origin`. - **CORS** : whitelist explicite via env `ALLOWED_ORIGINS`, refus de `*`. ### A.6 Justification du HTTP/WS clair en Phase 1 Le LAN Barodine est un **domaine de confiance** : poste founder + serveur Ubuntu + futurs postes board, tous sous le même routeur, pas d'exposition internet entrante. Dans ce périmètre : 1. **L'auth applicatif reste en place** (token API long-lived → JWT court 15 min, voir [ADR-0003](./0003-auth-tokens.md) pour le modèle 2-niveaux). Pas de relâchement sécurité — un client LAN sans token valide est rejeté comme en Phase 2. 2. **Aucune valeur ajoutée à émettre des certs auto-signés en Phase 1** : ils déclencheraient des warnings browser, casseraient `wscat` sans `--no-check`, et n'apporteraient pas de protection face à un attaquant déjà sur le LAN (qui ferait MITM ARP avant tout). 3. **Le wildcard `*.barodine.net` est l'asset TLS unique de Barodine** : on ne le réexpose pas hors de Coolify, donc inutile de le coller à un hostname LAN qui ne sortira jamais sur internet. 4. **Le coût de migration vers TLS Phase 2 est nul côté code** : seuls `ALLOWED_ORIGINS` et HSTS bougent (env-driven, pas de rebuild). ## Section B — Phase 2 internet via Coolify (cible, non déployée Phase 1) ### B.1 DNS et hôte - **Sous-domaine** : `agenthub.barodine.net` ([comment b3501bb1](/BARAAA/issues/BARAAA-14#comment-b3501bb1-72dc-4ad9-908a-22bffe1f86f2)). - **DNS** : enregistrement A (ou CNAME selon convention Barodine) pointant vers l'IP du VPS / host Coolify Barodine. Création par le founder lors du setup Coolify de l'app. - **Hôte** : VPS Barodine déjà géré par Coolify. AgentHub devient une **app Coolify supplémentaire**, pas un host dédié. ### B.2 TLS — wildcard `*.barodine.net` pré-provisionné **Décision figée founder** ([comment 769d86e2](/BARAAA/issues/BARAAA-14#comment-769d86e2-612c-4f60-b559-93077c982184)) : le wildcard `*.barodine.net` est **déjà pré-provisionné côté Coolify**. Conséquences : - **Pas d'émission ACME au premier deploy AgentHub** — on réutilise le wildcard existant. - **Renouvellement géré par l'infra Barodine**, pas par AgentHub. Aucun cert dans le repo, aucune secret TLS dans nos env vars. - **Pas de dépendance Let's Encrypt** dans la Phase 2 d'AgentHub : si Let's Encrypt subit un incident le jour du go-live, AgentHub ne sera pas affecté tant que le wildcard existant est valide. ### B.3 `compose.coolify.yml` Fichier versionné à la racine du repo, Phase 2 = importé tel quel par Coolify. Caractéristiques : - Service `app` **sans clé `ports:`** — Coolify/Traefik termine le TLS et route en réseau interne uniquement. - Labels Traefik : - `coolify.proxy.match=Host(\`agenthub.barodine.net\`)` - `coolify.proxy.tls=true` - `coolify.proxy.websocket=true` - `ALLOWED_ORIGINS=https://agenthub.barodine.net` (whitelist stricte). - Secrets injectés par Coolify (chiffrés au repos), jamais en commit. - Image identique à Phase 1. ### B.4 Reverse proxy et upgrade WebSocket - **Traefik** route `agenthub.barodine.net` → service `app:3000` interne Docker. - **Upgrade WS** : Traefik supporte nativement HTTP/1.1 → WebSocket. Vérifier que les headers `Connection: Upgrade` et `Upgrade: websocket` sont préservés (par défaut OK avec Coolify ; à smoke-tester lors de l'activation Phase 2 via `wscat -c wss://agenthub.barodine.net/agents`). - **Postgres / Redis** : **jamais** exposés sur Internet, uniquement réseau Docker interne Coolify. ### B.5 Sticky sessions et cluster mode - Au MVP mono-process Phase 1 : **non nécessaire**. - **Activation dès le passage en cluster mode** (Phase 2, > 1 vCPU sur le VPS Coolify) — sinon les reconnexions WS rebondissent entre workers et l'adapter Redis socket.io n'est pas suffisant pour rétablir la session côté client. - Configuration : label Traefik sticky session via cookie sur le service `app` (à câbler au moment de l'activation Phase 2). ### B.6 TLS, HSTS, CORS Phase 2 - **TLS : 1.2 minimum, 1.3 préférée**, géré par Traefik. - **HSTS : 1 an actif** (`max-age=31536000; includeSubDomains; preload` pour les sous-domaines `*.barodine.net`). - **CORS** : whitelist `https://agenthub.barodine.net` exclusivement. - **CSP** : identique Phase 1 (`default-src 'self'`, X-Frame-Options DENY, Referrer-Policy `strict-origin`). ## Procédure d'activation Phase 2 (hors-scope MVP — `AGNHUB-15`) L'**activation** Phase 2 est trackée comme un item Plane séparé (suggestion `AGNHUB-15`), à créer **après** la livraison Phase 1 et la démo founder LAN, pas avant. Tant que la Phase 1 n'est pas validée par le founder, ce ticket n'existe pas. La présence du `compose.coolify.yml` versionné dans le repo dès la Phase 1 est volontaire : l'archi est prête, seul le déploiement est différé. Étapes attendues sous `AGNHUB-15` (référence pour le futur, pas un livrable Phase 1) : 1. **Pré-flight wildcard** : confirmer côté ops Barodine que `*.barodine.net` est toujours valide et géré par Coolify (pas d'émission ACME planifiée pour AgentHub). 2. **DNS** : founder crée l'enregistrement A/CNAME `agenthub.barodine.net` → IP VPS Coolify. 3. **Coolify app** : importer le repo `agenthub`, sélectionner `compose.coolify.yml` comme fichier d'orchestration, configurer les variables d'env (DATABASE_URL, REDIS_URL, JWT_SECRET, `ALLOWED_ORIGINS=https://agenthub.barodine.net`). 4. **Premier deploy** : Coolify pull image `registry.barodine.net/agenthub:`, monte les volumes Postgres/Redis, applique les labels Traefik. **Pas d'émission ACME** — Traefik utilise le wildcard pré-provisionné. 5. **Smoke test internet** : - `curl -fsS https://agenthub.barodine.net/healthz` → 200 - `wscat -c wss://agenthub.barodine.net/agents` avec un JWT valide → connexion WS établie - 2 agents Paperclip distincts échangent un message via internet, persisté + retrouvé en historique après reconnexion. 6. **Migration données** (si la BDD Phase 1 doit être conservée) : `pg_dump -Fc` Phase 1 → restore Phase 2, fenêtre de coupure < 30 min documentée. Sinon, démarrage Phase 2 avec BDD vierge + seed. 7. **Activation HSTS 1 an** + sticky sessions si cluster mode. 8. **Décommissioning Phase 1** (optionnel) : si le serveur Ubuntu LAN est retiré, `docker compose down`, sauvegarde dump finale, `RUNBOOK-lan.md` archivé. Le passage Phase 1 → Phase 2 est un **two-way door** côté code (env-driven). C'est un **one-way door côté données** uniquement si on choisit de migrer la BDD ; sinon Phase 2 démarre vierge et Phase 1 reste lecture seule jusqu'à archivage. ## Pistes rejetées **Traefik + Let's Encrypt en Phase 1 sur le LAN.** Coût d'opération immédiat (DNS-01 challenge ou exposition 80/443 entrante depuis internet) sans bénéfice — le LAN est de confiance, l'auth applicatif reste, et le wildcard `*.barodine.net` est déjà la stratégie TLS Barodine. Aurait obligé à exposer le serveur LAN à internet pour ACME, contredisant le périmètre LAN-only Phase 1. **Certs auto-signés en Phase 1.** Faux gain de sécurité, vrai surcoût d'UX (warnings browser, `wscat --no-check`, intégration CI plus lourde). N'apporte rien face à un attaquant déjà sur le LAN. **Déploiement direct Coolify dès Phase 1, pas de phase LAN.** Tentant pour la simplicité (un seul compose), mais (a) le founder a explicitement choisi le LAN MVP ([comment 5f60d5c7](/BARAAA/issues/BARAAA-14#comment-5f60d5c7-a64a-4926-a81b-bfb520213bf7)), (b) la démo fin S2 sur LAN Barodine (serveur founder) est plus crédible qu'un endpoint internet anonyme, (c) la Phase 1 LAN nous force à valider le bootstrap idempotent et le runbook en condition réelle avant d'attaquer une cible internet. **Un ADR séparé pour Phase 2 (`ADR-0005-coolify`).** Aurait doublé le coût de relecture et fragmenté le rationale TLS / wildcard / sticky sessions sur deux documents. Les deux topologies partagent 95 % de l'archi ; les rassembler dans le même ADR rend la lecture plus rapide pour le futur lecteur qui veut comprendre "pourquoi on est passés du LAN à l'internet". **Kubernetes (k3s, microk8s) à la place de Coolify.** Surdimensionné pour 5 agents pilotes et 1 ingénieur. Coolify est déjà l'outillage Barodine, ajouter Kubernetes = nouvelle stack à on-caller, contraire à la règle "pas de plateforme prématurée". ## Questions ouvertes - **Hostname LAN Phase 1** : IP fixe ou mDNS `agenthub.local` ? Décidé par le founder côté ops au moment du J10. Pas bloquant — par défaut IP fixe, mDNS documenté dans le runbook. - **Sous-réseau LAN Barodine pour `ufw`** : à confirmer avec le founder pour la règle `ufw allow from `. Pas bloquant pour le code, bloquant uniquement pour `bootstrap.sh` final. - **Registry images** : `registry.barodine.net` à créer J1 (décision figée CEO). Si la création glisse, fallback Forgejo container registry intégré. - **WAL archiving Postgres** : non au MVP (RPO 24 h via dump nightly). Bascule Phase 2 si RPO 24 h devient insuffisant. - **Tracing distribué (OpenTelemetry)** : reporté Phase 2. - **Décommissioning Phase 1 lors de l'activation Phase 2** : choix migration BDD vs démarrage Phase 2 vierge à trancher dans `AGNHUB-15`. Pas un sujet Phase 1. ## Coût de retour - **Reverse Phase 1 → autre orchestrateur (Nomad, k3s)** : faible. `compose.lan.yml` est un fichier de 30 lignes, le bootstrap est idempotent, l'image Docker est portable. - **Reverse Phase 2 → autre PaaS (Render, Fly.io)** : moyen. `compose.coolify.yml` contient des labels Coolify/Traefik spécifiques, mais l'image et le schéma BDD sont portables. ~1 jour d'ajustement. - **Abandon du wildcard pré-provisionné → ACME au premier deploy** : faible. Ajout d'un service Traefik avec resolver Let's Encrypt, ~2 h. - **Abandon de la Phase 2 internet (produit reste interne Barodine)** : trivial. On supprime `compose.coolify.yml` et la §B de cet ADR ; rien ne change Phase 1.