mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-28 01:53:40 +02:00
capitalisation: triage 95_a_capitaliser + création domaine infra
Triage des 27 propositions du buffer de capitalisation (skill capitalisation-triage), avec vérification des doublons contre la base. Intégré dans knowledge/ (23 entrées): - backend: redis (compensation incrBy non-atomique), nestjs (injection cassée sous tsx watch; guard write mode dégradé), async (test rollback pipeline multi-fichiers), contracts (idempotence POST), auth (disclosure comptes soft-deleted), prisma (index partial soft-delete), llm-providers (nouveau: OAuth vs API key, prompt caching). - frontend: tests (garde-fous parking Later), navigation (fichiers non-route sous src/app Expo Router), general (type client vs payload backend), state (fallback catch-all mapping DB→UI). - workflow: story-tracking (statut BMAD vs narratif obsolète). - product: general (nouveau: doc feature store sans UI). - infra: NOUVEAU DOMAINE (traefik, tailscale, docker, docker-networking, reverse-proxy-paths, sidecar tailscale) + 00_INDEX.md. Autres: - 90_debug_et_postmortem.md: post-mortem réseau Docker partagé hors compose. - Rejeté 3 doublons (types enum contracts, getter PrismaService, $transaction). - Buffer 95_a_capitaliser.md purgé et restauré à son état initial. - _projects.conf: MAJ statuts epics + ajout app-rl799.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Infra — Patterns validés — Index
|
||||
|
||||
Patterns d'infrastructure (Docker, reverse proxy, Tailscale, homelab NUC) testés et validés en conditions réelles.
|
||||
|
||||
Avant toute proposition infra, identifie le fichier dont le nom et la description matchent le domaine traité, puis lis-le.
|
||||
|
||||
---
|
||||
|
||||
| Fichier | Domaine | Entrées clés |
|
||||
|---------|---------|--------------|
|
||||
| `docker-networking.md` | Bridges Docker, communication container ↔ hôte | Bridges isolés / service hôte injoignable, conteneuriser plutôt que firewall, `network_mode: host`, anti-pattern `host-gateway` |
|
||||
| `reverse-proxy-paths.md` | Servir une app sous un sous-chemin (Traefik) | App consciente du préfixe (stripPrefix ou non), chemins relatifs, app racine, conflits de path multi-apps |
|
||||
| `tailscale.md` | Sidecar Tailscale pour app incompatible subpath | Quand l'utiliser, architecture, squelette compose, `serve.json`, init obligatoire, coûts |
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Infra — Patterns validés : Docker networking
|
||||
domain: infra
|
||||
bucket: patterns
|
||||
tags: [docker, networking, bridge, traefik, host-gateway, firewall]
|
||||
applies_to: [architecture, implementation, debug]
|
||||
severity: medium
|
||||
validated_on: 2026-06-25
|
||||
source_projects: [_Assistant_Cuisine]
|
||||
---
|
||||
|
||||
# Infra — Patterns validés : Docker networking
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-bridge-isole-service-hote"></a>
|
||||
## Bridges Docker isolés — un container ne peut pas joindre un service hôte par défaut
|
||||
|
||||
### Constat
|
||||
|
||||
Un container connecté à un bridge custom (ex. `traefik-net` en `172.21.0.0/16`) ne peut **pas** atteindre par défaut :
|
||||
|
||||
- l'IP hôte côté `docker0` (`172.17.0.1`)
|
||||
- l'IP hôte sur d'autres interfaces (ex. IP Tailscale `100.x.x.x`)
|
||||
- `host.docker.internal` (qui résout vers `172.17.0.1`, donc même blocage)
|
||||
|
||||
Même quand l'hôte écoute bien sur `0.0.0.0:PORT`, l'appel depuis le container part en timeout. Cause : sous Docker 29 + iptables/nftables strict, les bridges sont isolés entre eux et le forward bridge → host est bloqué par défaut.
|
||||
|
||||
**Conséquence** : faire pointer un router Traefik vers `host.docker.internal:PORT` pour atteindre un service systemd hôte ne fonctionne pas en bridge mode.
|
||||
|
||||
### Bonnes pratiques / solutions (par ordre de propreté)
|
||||
|
||||
**Solution 1 (préférée) — Conteneuriser le service hôte**
|
||||
|
||||
Mettre le service dans un container connecté au même bridge (`traefik-net`). La communication passe par le DNS Docker interne (`servicename:port`). Aucune complexité réseau, fonctionne nativement. C'est presque toujours possible (ex. code-server via l'image `lscr.io/linuxserver/code-server`).
|
||||
|
||||
**Solution 2 — Traefik en `network_mode: host`**
|
||||
|
||||
Traefik abandonne son bridge custom et écoute directement sur les interfaces hôte. Il atteint tous les services hôte trivialement, MAIS perd l'accès aux containers via bridge : il faut alors leur publier des ports hôte, ce qui défait l'intérêt du reverse proxy interne. Réservé aux setups simples où Traefik ne sert que des services hôte.
|
||||
|
||||
**Solution 3 — Ouvrir manuellement le firewall**
|
||||
|
||||
```bash
|
||||
sudo iptables -I DOCKER-USER -s 172.21.0.0/16 -d 172.21.0.1 -p tcp --dport <PORT> -j ACCEPT
|
||||
```
|
||||
|
||||
Fragile : règle volatile, à refaire au reboot ou à l'upgrade Docker. Si on prend ce chemin, documenter et provisionner via une unit systemd.
|
||||
|
||||
### Anti-pattern : `extra_hosts: host.docker.internal:host-gateway`
|
||||
|
||||
Cette ligne déclare seulement un alias DNS dans le `/etc/hosts` du container ; elle ne crée **aucune route réseau**. Si le firewall Docker bloque, l'alias ne sert à rien : le container résout `host.docker.internal` mais le SYN reste bloqué.
|
||||
|
||||
**Règle** : avant de partir sur la Solution 2 ou 3, toujours vérifier qu'on ne peut pas conteneuriser le service. C'est presque toujours possible et toujours plus propre.
|
||||
|
||||
- Contexte technique : Docker 29 / Traefik / NUC — _Assistant_Cuisine 04-05-2026
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Infra — Patterns validés : Reverse proxy & sous-chemins
|
||||
domain: infra
|
||||
bucket: patterns
|
||||
tags: [traefik, reverse-proxy, pathprefix, stripprefix, subpath, spa]
|
||||
applies_to: [architecture, implementation, debug]
|
||||
severity: medium
|
||||
validated_on: 2026-06-25
|
||||
source_projects: [_Assistant_Cuisine]
|
||||
---
|
||||
|
||||
# Infra — Patterns validés : Reverse proxy & sous-chemins
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-app-sous-chemin-reverse-proxy"></a>
|
||||
## Servir une app sous un sous-chemin via reverse proxy
|
||||
|
||||
Le pattern qui fonctionne dépend de la façon dont l'app gère ses URLs internes (assets, API, redirects). Trois cas.
|
||||
|
||||
### Cas 1 — App nativement consciente du préfixe
|
||||
|
||||
L'app prend une option de base path et émet ses URLs internes préfixées. Côté Traefik, lire la doc de l'app pour savoir si un `stripPrefix` est requis — il n'y a **pas de règle universelle** :
|
||||
|
||||
- **App qui reçoit le path complet et sait quoi en faire** (ex. Cooklang `cook server --url-prefix /cuisine`) : `PathPrefix(/cuisine)` **sans** `stripPrefix`.
|
||||
- **App qui veut recevoir `/` mais émet ses assets sous le préfixe** (ex. Portainer `--base-url=/portainer`) : `PathPrefix(/portainer)` **avec** `stripPrefix /portainer`.
|
||||
|
||||
### Cas 2 — App qui sert uniquement des chemins relatifs
|
||||
|
||||
Si l'app utilise des URLs relatives partout (`<a href="./xxx">`, `<script src="./xxx">`, redirects relatifs aussi), elle fonctionne sous n'importe quel sous-chemin avec un simple `stripPrefix` côté Traefik. L'app ne sait pas où elle est, mais le navigateur résout correctement les URLs relatives (cas observé sur code-server).
|
||||
|
||||
⚠️ Vérifier qu'**aucune URL absolue** ne traîne dans le HTML/JS. Un `href="/static/..."` (au lieu de `href="./static/..."` ou `href="static/..."`) casse le rendu.
|
||||
|
||||
### Cas 3 — App qui veut être à la racine
|
||||
|
||||
Beaucoup de SPA modernes (Homepage, Vite/Next statiques) hardcodent `href="/_next/..."` etc. Le path prefix casse alors les assets. Solutions :
|
||||
|
||||
- Sous-domaine dédié (`homepage.example.com`) plutôt que sous-chemin.
|
||||
- Sinon, plugin de réécriture de body (ex. `traefik/plugin-rewritebody`) qui patche le HTML à la volée.
|
||||
- Si l'app est incompatible subpath ET doit rester derrière Tailscale : sidecar Tailscale dédié (voir `knowledge/infra/patterns/tailscale.md`).
|
||||
|
||||
**Règle** : tester en priorité avec un simple `PathPrefix + stripPrefix`. Si les assets cassent, lire la doc de l'app pour une option de base path. Si rien d'officiel, basculer sur sous-domaine (ou sidecar Tailscale).
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-conflit-path-multi-apps"></a>
|
||||
## Conflit de path entre apps multiples — anticipation
|
||||
|
||||
Quand plusieurs apps cohabitent sur le même domaine :
|
||||
|
||||
- **Avant** d'ajouter une app, vérifier que son préfixe ne va pas matcher des routes d'une autre app — voir le cas `/api` dans `knowledge/infra/risques/traefik.md`.
|
||||
- Préférer des préfixes longs et distinctifs (`/cuisine`, `/portainer`, `/code`) plutôt que génériques (`/app`, `/admin`, `/dashboard`).
|
||||
- Documenter dans le compose Traefik le mapping path → service pour garder une vue d'ensemble.
|
||||
|
||||
- Contexte technique : Traefik v3 / NUC — _Assistant_Cuisine 04-05-2026
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: Infra — Patterns validés : Sidecar Tailscale
|
||||
domain: infra
|
||||
bucket: patterns
|
||||
tags: [tailscale, sidecar, docker, subpath, magicdns, tls, spa]
|
||||
applies_to: [architecture, implementation]
|
||||
severity: medium
|
||||
validated_on: 2026-06-25
|
||||
source_projects: [apps/stirling-pdf]
|
||||
---
|
||||
|
||||
# Infra — Patterns validés : Sidecar Tailscale
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-sidecar-tailscale-subpath"></a>
|
||||
## Sidecar Tailscale pour app incompatible avec subpath
|
||||
|
||||
Quand une app refuse d'être servie sous un sous-chemin (assets et fetch API hardcodés sur `/`) et qu'on veut la garder derrière Tailscale, on lui attache un **sidecar `tailscale/tailscale`** qui rejoint le tailnet avec son propre hostname et termine le TLS via un cert MagicDNS.
|
||||
|
||||
### Quand l'utiliser
|
||||
|
||||
- L'app expose une SPA Vite/Next/CRA avec assets hardcodés sur `/`.
|
||||
- L'app fait des fetch API en chemin absolu (`/api/...`) sans respecter un base path.
|
||||
- Patcher l'upstream ou injecter un `<base href>` est non trivial / fragile.
|
||||
- **Critère de décision** : si la première page charge mais que les assets JS retournent 404 derrière Traefik, c'est ce cas (cf. bug upstream Stirling-PDF #5072).
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
tailnet
|
||||
│
|
||||
▼ https://<app>.<tailnet>.ts.net (cert MagicDNS auto)
|
||||
[container <app>-ts (sidecar tailscale)]
|
||||
│ network: <app>-internal (bridge dédié)
|
||||
▼
|
||||
[container <app>]:<port>
|
||||
```
|
||||
|
||||
- Le sidecar **rejoint le tailnet** comme un nœud à part (compte dans le quota devices).
|
||||
- Cert TLS délivré automatiquement par `tailscale serve` via `serve.json`.
|
||||
- L'app reste servie sur `/` côté interne ; **Traefik n'est pas dans la boucle**.
|
||||
- Réseau Docker dédié, jamais branché sur `traefik-net`.
|
||||
|
||||
### Squelette docker-compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
myapp:
|
||||
image: monimage:tag
|
||||
networks: [app_internal]
|
||||
# PAS d'exposition de ports sur l'hôte
|
||||
# PAS de labels traefik.*
|
||||
|
||||
tailscale:
|
||||
image: tailscale/tailscale:v1.98.3
|
||||
container_name: myapp-ts
|
||||
# ⚠ PAS de `hostname: myapp` ici (cf. risque collision DNS Docker)
|
||||
environment:
|
||||
TS_AUTHKEY: ${MYAPP_TS_AUTHKEY}
|
||||
TS_HOSTNAME: myapp
|
||||
TS_STATE_DIR: /var/lib/tailscale
|
||||
TS_SERVE_CONFIG: /config/serve.json
|
||||
TS_USERSPACE: "false"
|
||||
volumes:
|
||||
- /srv/docker-data/apps/myapp/tailscale/state:/var/lib/tailscale
|
||||
- /srv/docker-data/apps/myapp/tailscale/serve:/config:ro
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
cap_add: [NET_ADMIN, SYS_MODULE]
|
||||
networks: [app_internal]
|
||||
labels:
|
||||
# Auto-discovery homepage : la tile pointe vers le FQDN tailnet, pas vers Traefik
|
||||
- "homepage.group=Apps"
|
||||
- "homepage.name=MyApp"
|
||||
- "homepage.href=https://myapp.<tailnet>.ts.net/"
|
||||
|
||||
networks:
|
||||
app_internal:
|
||||
name: myapp-internal
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
`serve.json` :
|
||||
|
||||
```json
|
||||
{
|
||||
"TCP": { "443": { "HTTPS": true } },
|
||||
"Web": {
|
||||
"myapp.<tailnet>.ts.net:443": {
|
||||
"Handlers": { "/": { "Proxy": "http://myapp:<port-interne>" } }
|
||||
}
|
||||
},
|
||||
"AllowFunnel": { "myapp.<tailnet>.ts.net:443": false }
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠ Ne pas mettre `hostname: <app>` sur le sidecar si le service voisin s'appelle déjà `<app>` : collision DNS Docker non déterministe. `TS_HOSTNAME` fixe le nom côté tailnet et est décorrélé du DNS Docker interne. Voir `knowledge/infra/risques/docker.md`.
|
||||
|
||||
### Init obligatoire
|
||||
|
||||
1. Générer une **auth key Tailscale Reusable, non-Ephemeral** dans l'admin console.
|
||||
2. Stocker le state dans un volume **persistant** (sinon ré-enrôlement à chaque restart et pollution du tailnet).
|
||||
3. Si le tailnet a "Machine approval" activé : approuver le nœud manuellement après le premier `up`.
|
||||
4. **Disable key expiry** sur le nœud après enrôlement (sinon il faut refournir une authkey à l'expiration, par défaut 90 j).
|
||||
|
||||
### Coût du pattern
|
||||
|
||||
- Un nœud tailnet par app (limite gratuite : 100 devices).
|
||||
- Pas de middlewares Traefik partagés (rewriteBody pour `lang="fr"`, basic auth, etc.) → à accepter, ou patcher l'upstream.
|
||||
- Double restart si l'app upstream change : le sidecar continue à serve même si l'app est down → 502 propre, pas de cascade.
|
||||
|
||||
- Contexte technique : Docker / Tailscale / Spring Boot + Vite — apps/stirling-pdf 27-05-2026
|
||||
@@ -0,0 +1,13 @@
|
||||
# Infra — Risques & vigilance — Index
|
||||
|
||||
Risques d'infrastructure susceptibles de provoquer des incidents homelab/prod, des pertes de connectivité, ou des bugs non diagnostiquables (Docker, Traefik, Tailscale).
|
||||
|
||||
Avant toute proposition infra, identifie le fichier dont le nom et la description matchent le domaine traité, puis lis-le.
|
||||
|
||||
---
|
||||
|
||||
| Fichier | Domaine | Entrées clés |
|
||||
|---------|---------|--------------|
|
||||
| `traefik.md` | Traefik v3, routage, auth, proxy | Incompatibilité API Docker 29 (Traefik 3.6.1+), `PathPrefix(/api)` trop large → regex, Bun `new Response` perd `Location` → `c.redirect`, basic auth + WebSocket re-prompts WebKit/iOS |
|
||||
| `tailscale.md` | Certificats Tailscale, MagicDNS | `tailscale cert` ne couvre que le FQDN exact (ni sous-domaines ni wildcard), renouvellement ~3 mois via systemd timer idempotent |
|
||||
| `docker.md` | DNS Docker, hostname, réseaux partagés | Collision hostname container vs service name (DNS `127.0.0.11` non déterministe), diagnostic `getent hosts`, renvoi post-mortem réseau partagé entre stacks |
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Infra — Risques & vigilance : Docker
|
||||
domain: infra
|
||||
bucket: risques
|
||||
tags: [docker, dns, hostname, compose, tailscale, networking]
|
||||
applies_to: [implementation, review, debug, architecture]
|
||||
severity: high
|
||||
validated_on: 2026-06-25
|
||||
source_projects: [apps/stirling-pdf, infra/postgres]
|
||||
---
|
||||
|
||||
# Infra — Risques & vigilance : Docker
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-docker-collision-dns-hostname-service"></a>
|
||||
## Collision DNS Docker : hostname container vs service name
|
||||
|
||||
### Risques
|
||||
|
||||
- Dans un `docker-compose.yml`, déclarer `hostname: <X>` sur un service alors qu'un autre service du même compose s'appelle `<X>` crée **deux entrées A** pour le nom `<X>` dans le DNS embarqué Docker (`127.0.0.11`) :
|
||||
1. le service name `<X>` (résolu vers l'IP du container du service),
|
||||
2. le `hostname: <X>` du container voisin.
|
||||
- Un autre container du même network qui fait `getent hosts <X>` peut tomber sur **l'une ou l'autre selon l'ordre d'enregistrement** — non déterministe entre `docker compose up` successifs (dépend de l'ordre de démarrage).
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Un service en réseau interne retourne 200 sur son IP directe mais `connection refused` quand on le hit par son service name depuis un voisin.
|
||||
- **Aucune erreur explicite** dans les logs : juste un `connection refused` qui ressemble à "l'app n'écoute pas".
|
||||
- Cas typique sidecar Tailscale : `tailscale serve` proxie vers `app:8080` mais le DNS résout `app` vers le sidecar lui-même → 502 côté HTTPS.
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```yaml
|
||||
# ❌ collision DNS — "app" peut résoudre vers le sidecar
|
||||
services:
|
||||
app:
|
||||
image: monapp # service name DNS = "app"
|
||||
proxy:
|
||||
image: nginx
|
||||
hostname: app # ⚠ collision
|
||||
|
||||
# ✅ pas de hostname Docker : le service name reste la seule entrée DNS
|
||||
services:
|
||||
app:
|
||||
image: monapp
|
||||
proxy:
|
||||
image: nginx
|
||||
# hostname Docker non défini → ID container random
|
||||
# les voisins s'adressent au proxy via le service name "proxy"
|
||||
```
|
||||
|
||||
- **Règle** : ne jamais mettre `hostname: <X>` quand un service du même compose s'appelle `<X>`.
|
||||
- **Cas sidecar Tailscale** : ne pas mettre `hostname: <app>` pour matcher `TS_HOSTNAME`. `TS_HOSTNAME` est lu par tailscaled et fixe le nom **côté tailnet**, totalement décorrélé du DNS Docker interne. Laisser le hostname Docker par défaut (voir `knowledge/infra/patterns/tailscale.md`).
|
||||
|
||||
### Diagnostic
|
||||
|
||||
```bash
|
||||
# Dans le container qui fait l'appel
|
||||
docker exec <caller> getent hosts <name-resolved>
|
||||
# Plus d'une ligne pour le même nom → collision DNS Docker
|
||||
```
|
||||
|
||||
- Contexte technique : Docker DNS / Compose / sidecar Tailscale — apps/stirling-pdf 27-05-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-docker-reseau-partage-stacks"></a>
|
||||
## Réseau Docker partagé entre stacks — voir post-mortem
|
||||
|
||||
Un réseau Docker mutualisé entre plusieurs stacks (ex. Postgres partagé) déclaré `external: true` peut malgré tout être supprimé par un `docker compose down` quand aucun autre projet ne le revendique, avec perte de connectivité en cascade et conteneur recréé sans son mapping de ports.
|
||||
|
||||
Détail complet, règles opérationnelles et signal de détection : voir la section **« Réseau Docker partagé entre stacks — créer hors compose »** dans `90_debug_et_postmortem.md`.
|
||||
|
||||
- Contexte technique : Docker / réseau partagé / NUC — infra/postgres 22-04-2026
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: Infra — Risques & vigilance : Tailscale
|
||||
domain: infra
|
||||
bucket: risques
|
||||
tags: [tailscale, cert, magicdns, tls, letsencrypt, systemd]
|
||||
applies_to: [architecture, implementation, debug]
|
||||
severity: medium
|
||||
validated_on: 2026-06-25
|
||||
source_projects: [_Assistant_Cuisine]
|
||||
---
|
||||
|
||||
# Infra — Risques & vigilance : Tailscale
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-tailscale-cert-fqdn-exact"></a>
|
||||
## `tailscale cert` ne couvre QUE le FQDN exact du host
|
||||
|
||||
### Risques
|
||||
|
||||
- `tailscale cert <fqdn>` génère un certificat Let's Encrypt valide **uniquement** pour le FQDN passé, qui doit être le hostname Tailscale du device courant (ex. `nuc.wyvern-snapper.ts.net`). Toute attente de sous-domaines ou de wildcard est fausse.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Sous-domaines arbitraires (`cuisine.nuc.wyvern-snapper.ts.net`) : pas de cert auto, et MagicDNS ne route pas non plus.
|
||||
- Wildcard : non supporté.
|
||||
- FQDN d'un autre device : non couvert.
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Pour un reverse-proxy multi-apps, deux options :
|
||||
|
||||
- **Path-based routing** sous le FQDN unique (`/cuisine`, `/code`, etc.) — voir `knowledge/infra/patterns/reverse-proxy-paths.md`.
|
||||
- **Vrai domaine** avec challenge DNS-01 (Cloudflare, OVH, etc.) côté Traefik pour obtenir des sous-domaines réels.
|
||||
|
||||
**Règle** : `tailscale cert` est OK pour un homelab qui sert tout sous un seul FQDN avec routing par path. Pour des sous-domaines réels, prévoir un domaine perso.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-tailscale-cert-renouvellement"></a>
|
||||
## `tailscale cert` nécessite un renouvellement périodique
|
||||
|
||||
### Risques
|
||||
|
||||
- Le cert est valide ~3 mois (Let's Encrypt). Sans renouvellement automatisé, expiration silencieuse → TLS cassé sur toutes les apps servies sous ce FQDN.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Erreur de certificat expiré côté navigateur après ~90 j sans intervention.
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- **Pattern** : systemd timer hebdomadaire qui appelle `tailscale cert --cert-file ... --key-file ...` sur le FQDN du host. `tailscale cert` est **idempotent** et ne renouvelle qu'à moins de 30 j de la fin — aucun coût à le lancer souvent.
|
||||
- Le reverse proxy doit watcher le fichier pour reload auto (le file provider de Traefik le fait nativement).
|
||||
|
||||
- Contexte technique : Tailscale cert / MagicDNS / systemd — _Assistant_Cuisine 04-05-2026
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
title: Infra — Risques & vigilance : Traefik
|
||||
domain: infra
|
||||
bucket: risques
|
||||
tags: [traefik, docker, reverse-proxy, bun, websocket, basic-auth, ios]
|
||||
applies_to: [implementation, review, debug, architecture]
|
||||
severity: high
|
||||
validated_on: 2026-06-25
|
||||
source_projects: [_Assistant_Cuisine]
|
||||
---
|
||||
|
||||
# Infra — Risques & vigilance : Traefik
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-traefik-v3-docker29-api"></a>
|
||||
## Pièges Traefik v3 + Docker Engine 29
|
||||
|
||||
### Risques
|
||||
|
||||
- **Incompatibilité d'API Docker** : Docker 29 a élevé la minimum API version. Traefik < 3.6 utilise un client Docker SDK figé en `1.24` ; le daemon refuse la connexion et aucune route ne fonctionne.
|
||||
- **`PathPrefix(/api)` trop large** : un routeur `Host(...) && PathPrefix(/api)` vole les routes `/api/*` de toutes les autres apps du même domaine (Homepage `/api/services`, Next.js `/api/...`, n'importe quel SPA) → 401/404 silencieux.
|
||||
- **Bun `new Response(body, {status: 3xx})` perd le header `Location`** : un proxy HTTP en Bun qui forward un upstream 302/303 retransmet le status mais le client reçoit un 200 vide (ou un status sans `Location`). Le navigateur ne suit pas la redirection.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Container Traefik qui tourne mais aucune route active, logs en boucle :
|
||||
|
||||
```
|
||||
ERR Failed to retrieve information of the docker client and server host
|
||||
error="Error response from daemon: client version 1.24 is too old.
|
||||
Minimum supported API version is 1.40, please upgrade your client to a newer version"
|
||||
```
|
||||
|
||||
- Une app exposée sur le même domaine que le dashboard Traefik se met à retourner 401/404 sur ses propres `/api/*`.
|
||||
- Un proxy Bun qui forward une redirection : le client reçoit un 200 vide ou un `Location` nul.
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
**Incompatibilité API Docker**
|
||||
|
||||
- Utiliser **Traefik 3.6.1+** (client Docker avec négociation auto de version). Les tags `:v3.5` / `:v3.2` ne marchent pas, même avec `DOCKER_API_VERSION=1.44` en env (le client n'honore pas cette variable dans Traefik).
|
||||
- Pinner explicitement la version (ex. `traefik:v3.6.15`), jamais le tag mouvant `:v3.6`.
|
||||
- Vérifier la compatibilité Traefik ↔ Docker Engine **avant** un upgrade Docker.
|
||||
|
||||
**`PathPrefix(/api)` trop large**
|
||||
|
||||
```yaml
|
||||
# ❌ trop large — capture tous les /api/* du domaine
|
||||
- "traefik.http.routers.dashboard-api.rule=Host(`example.com`) && PathPrefix(`/api`)"
|
||||
|
||||
# ✅ ne capture que les endpoints natifs Traefik
|
||||
- "traefik.http.routers.dashboard-api.rule=Host(`example.com`) && PathRegexp(`^/api/(overview|version|rawdata|support-dump|entrypoints|http|tcp|udp)(/|$$)`)"
|
||||
```
|
||||
|
||||
Les endpoints natifs Traefik sont fixés : `overview`, `version`, `rawdata`, `support-dump`, `entrypoints`, `http/*`, `tcp/*`, `udp/*`. Sur un domaine partagé entre plusieurs apps : **jamais** de `PathPrefix(/api)` générique, toujours une regex explicite.
|
||||
|
||||
**Bun `new Response` perd le `Location`**
|
||||
|
||||
```ts
|
||||
// ❌ perd le Location dans Bun (~1.1.x)
|
||||
return new Response(upstream.body, {
|
||||
status: upstream.status, // 303
|
||||
headers: upstream.headers, // contient Location, ignoré au final
|
||||
});
|
||||
|
||||
// ✅ Hono c.redirect() ou équivalent
|
||||
if (upstream.status >= 300 && upstream.status < 400) {
|
||||
const loc = upstream.headers.get("location");
|
||||
if (loc) return c.redirect(loc, upstream.status as 301 | 302 | 303 | 307 | 308);
|
||||
}
|
||||
```
|
||||
|
||||
**Règle** : tout proxy HTTP qui forward des redirects upstream doit traiter explicitement la branche 3xx avant la branche "body normal".
|
||||
|
||||
- Contexte technique : Traefik v3 / Docker 29 / Bun + Hono — _Assistant_Cuisine 04-05-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-basic-auth-websocket-webkit-ios"></a>
|
||||
## Basic auth + WebSocket : re-prompts répétés (surtout WebKit/iOS)
|
||||
|
||||
### Risques
|
||||
|
||||
- Une app derrière une basic auth (Traefik, nginx) qui utilise des WebSockets (code-server, Jupyter, ttyd, et beaucoup d'apps "live") déclenche **plusieurs prompts d'authentification** à l'ouverture, même quand les credentials viennent d'être saisis.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- 3 prompts d'authentification successifs à chaque ouverture de l'app, observés notamment sur tablette iOS / iPadOS.
|
||||
- Persiste sur Chrome iOS et Firefox iOS (tous contraints d'utiliser WebKit sur iOS). Sporadique sur macOS, rare sur desktop Linux/Windows.
|
||||
|
||||
### Cause
|
||||
|
||||
Safari (et tous les navigateurs iOS, contraints à WebKit) ne **propage pas systématiquement les credentials basic auth aux requêtes WebSocket** d'une page déjà authentifiée. Chaque WS qui échoue redéclenche un prompt.
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Remplacer la basic auth par une auth à **cookie de session** :
|
||||
|
||||
- Soit l'auth native de l'app si elle existe (code-server `HASHED_PASSWORD`, JupyterHub login form, etc.).
|
||||
- Soit un système central type Authelia / Authentik / traefik-forward-auth qui pose un cookie partagé entre apps.
|
||||
|
||||
Le cookie passe automatiquement avec les requêtes WebSocket (même origine) → plus de re-prompt.
|
||||
|
||||
**Règle** : pour toute app qui utilise des WebSockets ET qui doit être protégée, ne **pas** utiliser la basic auth. Toujours cookie-based. La basic auth reste OK pour des UI sans WS (dashboards en pull, API REST simples).
|
||||
|
||||
- Contexte technique : Traefik / basic auth / WebKit iOS — _Assistant_Cuisine 04-05-2026
|
||||
Reference in New Issue
Block a user