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:
MaksTinyWorkshop
2026-06-25 10:31:22 +02:00
parent 1c876309f1
commit ef24d85d57
31 changed files with 1042 additions and 27 deletions
+13
View File
@@ -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 |
+77
View File
@@ -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
+58
View File
@@ -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
+109
View File
@@ -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