# Résilience DSH-VPS face aux coupures NAS + fix tailscale-vps-ssh-relay + leçon bifrost-proxy **Date** : 24/08/2026 **Contexte** : coupure STEG (canicule) ayant coupé le NAS pendant plusieurs heures, DSH-VPS (VPS Contabo, indépendant électriquement) s'est retrouvé hors service car il dépendait entièrement de Bifrost (NAS) pour parler aux modèles. ## 1. Route directe OpenCode pour DSH (indépendante du NAS) **Problème** : DSH ne parlait qu'à Bifrost (`http://100.86.197.88:3086/v1`), lui-même hébergé sur le NAS. Toute coupure NAS = DSH inopérant, même si le VPS tourne. **Solution** : ajout d'un second provider dans `settings.yaml` de DSH (volume `dsh_vps_home`, fichier `/home/dsh-agent/.dsh/settings.yaml`), en plus de `bifrost` (pas en remplacement) : ```yaml llm-pi-ai: providers: bifrost: # ... inchangé, reste le défaut (agent-default-model) ... opencode-direct: displayName: OpenCode Zen (direct, sans NAS) apiKeyEnv: OPENCODE_DIRECT_API_KEY api: openai-completions baseURL: https://opencode.ai/zen/go/v1 compat: thinkingFormat: deepseek models: - id: deepseek-v4-flash contextWindow: 131072 maxTokens: 16384 - id: deepseek-v4-pro contextWindow: 131072 maxTokens: 16384 - id: mimo-v2.5 contextWindow: 1048576 maxTokens: 131000 - id: ox-alpha-free contextWindow: 1048576 maxTokens: 131072 ``` Clé (`OPENCODE_DIRECT_API_KEY`) = **même clé** que celle utilisée par Bifrost pour son provider `opencode` (`BIFROST_OPENCODE_KEY` dans `/mnt/docker/hermes-platform/.env` sur le NAS). Même compte, même forfait — juste un deuxième chemin d'accès. Ajoutée en dur dans `.credentials.yaml` du conteneur DSH et dans `docker-compose.yml` (`services.dsh-vps.environment`). **⚠️ Conséquence à documenter** : cette clé existe maintenant sur DEUX hôtes (NAS + VPS). En cas de rotation future, penser aux deux. **Décision produit (Nabil, 24/08/2026) — OpenCode plutôt qu'OpenRouter pour un modèle gratuit-temporaire** : Bifrost a aussi un provider `openrouter` avec accès large (`"models": ["*"]`). Pour un modèle comme `stealth/ox-alpha` (gratuit maintenant, potentiellement payant demain), la route OpenRouter a été **écartée délibérément** : c'est du pay-as-you-go sans plafond — si le modèle bascule en payant, la facturation continue silencieusement jusqu'à ce qu'on s'en aperçoive. La route OpenCode Zen Go tourne sur un forfait plafonné ($10/mois) : le pire cas est soit une consommation du forfait existant, soit un arrêt net du service — jamais une dérive de facturation. **Règle générale à retenir** : pour tout futur modèle gratuit-temporaire/stealth, préférer systématiquement une route à coût plafonné (forfait) à une route pay-as-you-go pure, même si cette dernière est théoriquement plus permissive. **Validation** : testé via `dsh --profile headless --patch ` (pas juste `curl` brut vers l'API OpenCode — un `curl` réussi ne prouve pas que le runtime DSH lui-même sait utiliser la config). 3/3 sur `mimo-v2.5` et `ox-alpha-free` après quelques échecs initiaux dus à la flakiness intrinsèque de l'endpoint OpenCode Zen "go" (gratuit/beta) — pas un bug de config. **Piège à éviter** : ne pas conclure à un bug de config sur un seul test raté sur cet endpoint ; retester plusieurs fois avant de creuser plus loin. ## 2. Leçon bifrost-proxy — faux positif à ne pas reproduire **Ce qui a été (à tort) diagnostiqué comme une régression** : tout appel vers `http://100.86.197.88:3086/v1/...` (bifrost-proxy, exposition Tailscale de Bifrost pour le VPS) avec un header `x-bf-vk: ` renvoyait `401 virtual key is required`, alors que la même clé fonctionnait parfaitement en LAN direct (`172.17.0.1:3085` ou `192.168.100.33:3085`). **Cause réelle (pas une panne)** : `bifrost-proxy` est un nginx (openresty) qui **ne transmet pas** le header `x-bf-vk` reçu du client. Sa conf (`/usr/local/openresty/nginx/conf/nginx.conf` dans le conteneur `bifrost-proxy`) le reconstruit lui-même : ```nginx map $http_authorization $bearer_vk { ~*^Bearer\s+(.+)$ $1; default ""; } map $http_x_api_key $resolved_vk { "" $bearer_vk; default $http_x_api_key; } ... proxy_set_header x-bf-vk $resolved_vk; ``` Donc ce endpoint accepte l'auth via `Authorization: Bearer ` OU `X-Api-Key: ` — **jamais** `x-bf-vk` directement en entrée (ce header n'existe qu'en interne, entre `bifrost-proxy` et `bifrost:8080`, et sur le endpoint LAN direct qui lui l'accepte nativement). Tester avec `x-bf-vk` sur le endpoint Tailscale envoie une VK vide côté proxy → 401 qui ressemble à une vraie panne mais n'en est pas une. **Règle à retenir** : pour tester `bifrost-proxy` (port 3086, Tailscale) → `-H "Authorization: Bearer sk-bf-..."`. Pour tester Bifrost en LAN direct (port 3085) → `-H "x-bf-vk: sk-bf-..."`. Deux endpoints, deux conventions différentes — ne pas les confondre au prochain incident. ## 3. Fix tailscale-vps-ssh-relay (NAS) **Symptôme** : conteneur `tailscale-vps-ssh-relay` refusait de démarrer après un reboot NAS : `cannot join network of a non running container: ` (`network_mode: container:`, cible = `tailscale-nyora-bridge`, pas encore démarrée — simple course au démarrage, résolu par un `docker start` une fois la cible up). **Vrai problème sous-jacent, découvert en creusant la fiabilité** : `tailscale-nyora-bridge` tourne en **Tailscale mode userspace** — pas de `/dev/net/tun`, pas d'interface `tailscale0`/`tun0` dans son netns (confirmé via `docker exec tailscale-nyora-bridge ip a`). Conséquence : toute connexion TCP directe du noyau vers une IP Tailscale (`100.64.0.0/10`) depuis ce netns **time-out systématiquement** — le trafic doit obligatoirement passer par le proxy SOCKS5 local que Tailscale expose en mode userspace, sur `127.0.0.1:1055`. L'ancien relais (`while true; do nc -lk -p 22223 -e nc ; sleep 1; done`, BusyBox nc) cumulait deux problèmes : tentative de connexion TCP directe (donc timeout garanti, indépendamment du reste) ET un `-lk` dont le support par BusyBox nc est douteux (races de bind/`TIME_WAIT` → trous d'écoute intermittents, `nc: bind: Address in use` en boucle). **Correctif pérenne déployé** : service dédié `/volume1/docker/tailscale-vps-ssh-relay/` : ```dockerfile FROM alpine:latest RUN apk add --no-cache socat ENTRYPOINT ["socat", "TCP-LISTEN:22223,fork,reuseaddr", "SOCKS5-CONNECT:127.0.0.1::,socksport=1055"] ``` ```yaml services: tailscale-vps-ssh-relay: build: . network_mode: "container:tailscale-nyora-bridge" restart: unless-stopped ``` **Validation** : 20/20 (bannière SSH via IP Tailscale) + 20/20 (bannière via IP interne Docker) + 5/5 sessions SSH authentifiées réelles côté Gemini ; 10/10 rebanniere confirmés indépendamment par Claude en méthode différente (Python socket direct plutôt que curl). Convergence forte, accepté. **Règle générale à retenir** : pour tout futur relais/proxy TCP sortant vers une IP Tailscale depuis un conteneur — vérifier d'abord son mode Tailscale (`docker exec ip a` : présence ou non de `/dev/net/tun` et d'une interface `tailscale0`/`tun0`). En mode userspace (le cas courant pour un conteneur sans `--cap-add=NET_ADMIN --device=/dev/net/tun`), la connexion sortante doit systématiquement passer par `SOCKS5-CONNECT:127.0.0.1::,socksport=1055` (typiquement via `socat`), jamais une connexion TCP directe — celle-ci time-out à coup sûr. ## Fichiers de référence - `brief-gemini-fix-bifrost-proxy-ox-alpha-dsh-20260824.md` (historique complet de la décision OpenCode vs OpenRouter, v1→v3) - `brief-gemini-fix-ssh-relay-20260824.md`