seize-infra-doc/DECISIONS.md
Hedi Kalboussi 1e90a85359 Documentation initiale infra Seize — passation J7
- 6 docs racine (README, HANDOVER, ARCHITECTURE, DECISIONS, INSTALL, OPERATIONS)
- 9 docs thématiques dans docs/
- 10 templates docker-compose dans configs/
- 7 scripts Python/Shell d'import Furious vers Zitadel
- 2 matrices RBAC

Etat infra au 5 juin 2026 :
- 88 users Furious importes dans Zitadel
- 8 roles RBAC crees et attribues
- Intranet filtre les cartes selon role (Approche A)
- 7 outils SSO operationnels
2026-06-05 17:49:43 +00:00

357 lines
14 KiB
Markdown

# DECISIONS — Choix architecturaux et anti-patterns
> Pourquoi telle décision plutôt qu'une autre. À lire pour comprendre les **trade-offs** et **ne pas refaire les mêmes erreurs**.
---
## 🎯 D1 — Zitadel plutôt qu'Authentik comme IDP
### Contexte
Au jour 2, on a commencé par déployer **Authentik** comme IDP. Au jour 3, on a migré vers **Zitadel**.
### Décision
**Zitadel gagne.**
### Pourquoi
-**API plus stable** : la doc Zitadel est plus claire, l'API v2 est très propre
-**Support de Custom Claims** plus mature (urn:zitadel:iam:org:project:roles)
-**Performance** : Zitadel scale mieux que Authentik en background workers
-**Communauté** plus active sur les vrais besoins entreprise
-**MFA TOTP** plus simple à configurer
### Inconvénients acceptés
- ❌ Pas de GUI de gestion des flows aussi avancée qu'Authentik
- ❌ Custom Claims via Actions V2 (instable au moment de la décision)
### Anti-pattern évité
**Ne PAS hésiter à migrer** si l'outil initial pose problème, même si on a investi du temps. Mieux vaut perdre 1 jour de migration que rester bloqué 6 mois sur une stack inadaptée.
---
## 🎯 D2 — Pattern "OAuth2-Proxy par outil" plutôt que centralisé
### Contexte
Pour activer le SSO sur 5 outils différents (intranet, n8n, Portainer, Code Server, Uptime Kuma), 2 options :
- **Centralisé** : 1 seul OAuth2-Proxy pour tous les outils, distinguant par route
- **Par outil** : 1 OAuth2-Proxy dédié à chaque outil
### Décision
**OAuth2-Proxy par outil.**
### Pourquoi
-**Isolation** : si un OAuth2-Proxy crash, les autres outils continuent
-**Configuration différenciée** : chaque outil peut avoir son propre cookie, scope, upstream
-**Debugging simple** : `docker logs oauth2-proxy-n8n` ne mélange pas avec les autres
-**Clients OIDC séparés** dans Zitadel : permission fine
### Inconvénients acceptés
- 5 conteneurs OAuth2-Proxy au lieu de 1 (~50 Mo RAM chacun, négligeable)
- 5 configurations à maintenir (mais elles se ressemblent toutes)
### Anti-pattern évité
**Ne PAS factoriser à outrance** "pour faire propre" si ça crée du couplage entre outils indépendants.
---
## 🎯 D3 — Bug Traefik authRequestHeaders=Cookie
### Contexte
Pendant 1 journée entière, **TOUS les SSO étaient cassés** : à chaque requête, l'user était redirigé vers le login Zitadel, même après login réussi.
### Cause découverte
Par défaut, le middleware **ForwardAuth de Traefik** ne transmet **PAS** le header `Cookie` à OAuth2-Proxy. Du coup OAuth2-Proxy ne voit jamais le cookie de session qu'il a lui-même créé → tout est "non authentifié".
### Décision / Fix
Dans `~/docker/traefik/dynamic.yml` :
```yaml
http:
middlewares:
oauth2-headers:
forwardAuth:
address: "http://oauth2-proxy:4180/oauth2/auth"
authRequestHeaders:
- Cookie # ← LA LIGNE CRITIQUE
- Authorization
authResponseHeaders:
- X-Auth-Request-User
- X-Auth-Request-Email
- X-Auth-Request-Preferred-Username
- X-Auth-Request-Groups
```
### Anti-pattern évité
**Ne PAS supposer que les valeurs par défaut sont sensées.** Toujours vérifier la doc des middlewares quand quelque chose ne marche pas.
### Référence
- Issue Traefik : https://github.com/traefik/traefik/issues/...
- Forum OAuth2-Proxy : https://github.com/oauth2-proxy/oauth2-proxy/issues/...
---
## 🎯 D4 — Service account avec IAM_USER_MANAGER (pas IAM_OWNER)
### Contexte
Pour le script d'import Furious → Zitadel, il fallait un compte API avec droits admin Zitadel.
### Décision
**`IAM_USER_MANAGER`** uniquement, **pas** `IAM_OWNER`.
### Pourquoi (principe du moindre privilège)
| Rôle | Peut faire |
|---|---|
| IAM_USER_MANAGER | Créer, modifier, supprimer des **users** uniquement |
| IAM_OWNER | **TOUT** : modifier l'instance, supprimer projects, désactiver MFA d'admins, etc. |
→ Si la clé du service account fuite, l'attaquant peut **créer des users** mais **pas détruire l'instance**.
### Anti-pattern évité
**Ne JAMAIS donner les droits maximum à un service account** par flemme. Toujours commencer minimal, augmenter si besoin.
---
## 🎯 D5 — Fusion des doublons : garder anciens, supprimer nouveaux
### Contexte
Au moment de l'import, 4 users existaient déjà dans Zitadel avec leur trigramme :
- HKA (Hedi Kalboussi)
- PSP (Philippe Spoljaric)
- PYT (Pierre-Yves Tachon)
- Jules (Jules Bataille)
L'import a créé des **doublons** : `hedi.kalboussi@...`, `philippe.spoljaric@...`, etc.
### Décision
**Garder les anciens (avec MFA configuré), supprimer les nouveaux.**
### Pourquoi
| Critère | Anciens (HKA, PSP, ...) | Nouveaux (email) |
|---|---|---|
| MFA configuré | ✅ Oui | ❌ Non |
| Mdp connu par l'user | ✅ Oui | 🟡 Temporaire dans CSV |
| Si suppression : impact | 🔴 Hedi se bloque hors de Zitadel | 🟢 Aucun |
| Trigramme pratique | ✅ Oui (HKA, PSP) | ❌ Email long |
| Metadata Furious | ❌ Non (à recopier) | ✅ Oui |
### Solution finale
Script `merge-duplicates.py` :
1. Lit les metadata du nouveau compte
2. Les copie sur l'ancien compte
3. Supprime le nouveau
### Anti-pattern évité
**Ne JAMAIS supprimer un compte admin sans avoir validé** qu'on garde un accès admin alternatif. Risque de blocage hors de Zitadel.
---
## 🎯 D6 — Metadata custom plutôt que rôles "vrais" Zitadel au moment de l'import
### Contexte
Au moment de l'import (Phase 1 le jour 7 matin), 2 options :
- A : Importer les users **avec rôles** (decisions business prises seul)
- B : Importer les users **avec metadata custom** (job_title, bu_name, manager...), rôles attribués ensuite après validation
### Décision
**Option B — metadata d'abord, rôles ensuite (Phase 2)**.
### Pourquoi
-**Pas de décision business prématurée** (Philippe a validé l'import, pas le mapping de rôles)
-**Évolutif** : si Philippe change la matrice plus tard, on relance juste `assign-roles.py`
-**Traçabilité** : metadata Furious préservées de toute façon (utiles plus tard)
### Inconvénient accepté
- Phase 2 (attribution des rôles) à faire dans la même journée → finalement OK car on a eu le temps
### Anti-pattern évité
**Ne PAS mélanger import technique et décision business** dans un même script. Séparer les responsabilités.
---
## 🎯 D7 — RBAC Approche A (intranet uniquement) — la grande décision
### Contexte
Pour que chaque user voie uniquement les outils auxquels il a accès, 3 options évaluées :
### Option A — RBAC sur l'intranet uniquement
- Le HTML de l'intranet **cache** les cartes selon le rôle
- L'outil lui-même reste accessible si l'URL directe est tapée
- **Sécurité** : par "obscurité" (suffisant pour 88 employés non-malveillants)
### Option B — RBAC complet avec migrations gratuites
- Migrer n8n → Activepieces, Portainer CE → Komodo, Uptime Kuma → Gatus, Code Server → OpenVSCode
- Chaque outil filtre nativement par OIDC claims
- **Effort** : 5-6 jours
- **Coût** : 0€
### Option C — RBAC complet avec licences pro
- n8n Enterprise (~12k€/an), Portainer Business (~1800€/an), etc.
- **Effort** : 1 jour
- **Coût** : ~25 800€/an
### Décision (par Hedi, sponsor Philippe)
**Option A.**
### Pourquoi
-**0€** vs ~25 800€/an
-**2-3h** vs 5-6 jours
-**Suffisant** pour la culture Seize (88 employés, peu de techniciens)
-**Évolutif** : on peut passer à B ou C plus tard si besoin
- 🟡 **Limite acceptée** : un user qui connaît `n8n.seizeconsulting.com` peut bypass
### Risque accepté
Si un user malveillant connaît l'URL directe d'un outil, il y accède. Mais :
- L'authentification reste obligatoire (OAuth2-Proxy bloque les non-authentifiés)
- Tous les accès sont **logués** par Zitadel + Traefik
- On peut détecter des accès anormaux dans les logs
### Anti-pattern évité
**Ne JAMAIS sur-ingénier la sécurité** au-delà de la menace réelle. Pour 88 collègues non-malveillants, l'Approche A est suffisante.
---
## 🎯 D8 — Pas de SMTP configuré : distribution manuelle des mdp
### Contexte
Sans serveur SMTP configuré dans Zitadel, **aucun email automatique** :
- Pas d'email de bienvenue
- Pas de "Mot de passe oublié"
- Pas de notifications MFA
### Décision (validée par Philippe)
**Distribution manuelle des 88 mdp temporaires via Teams en privé.**
### Pourquoi
- 🟡 L'admin M365 chez Seize n'est pas Hedi → solliciter quelqu'un d'autre
- 🟡 Configuration SMTP M365 = créer compte `noreply@`, App Password, etc.
- ✅ Distribution manuelle = 88 messages Teams en privé, étalés sur 5-7 jours
### Anti-pattern évité
**Ne PAS bloquer le projet** parce qu'une dépendance externe (admin M365) n'est pas prête. Trouver un workaround manuel acceptable.
### À faire plus tard
Configurer SMTP M365 pour les futurs onboardings (cf. HANDOVER.md).
---
## 🎯 D9 — Action "Return user roles during authentication" cochée mais "Only authorized users" décochée
### Contexte
Dans la config du Project Zitadel "Infra Seize", il y a 2 options :
- **"Return user roles during authentication"** : ajoute les rôles dans le token OIDC
- **"Only authorized users can authenticate"** : bloque les users sans rôle
### Décision (par Hedi)
-**Cocher "Return user roles"** : nécessaire pour transmettre les rôles à l'intranet
-**Ne PAS cocher "Only authorized users"** : un nouvel arrivant qui n'a pas encore son rôle attribué doit pouvoir se logger
### Pourquoi le 2ème non
- Pendant l'onboarding RH d'un nouveau collaborateur, son compte est créé dans Furious, importé dans Zitadel, mais le **rôle** est attribué peut-être 1-2 jours après (le temps de remplir son `job_title`).
- Si on coche "Only authorized users", il se prendra un **"Access Denied"** à son premier login, même s'il a un compte valide.
### Anti-pattern évité
**Ne PAS optimiser pour le 99% des cas** si ça casse 1% des cas critiques (nouveau collaborateur premier jour).
---
## 🎯 D10 — Token JWT dans Authorization plutôt que dans cookie
### Contexte
OAuth2-Proxy peut transmettre le token JWT à l'outil via :
- Header `Authorization: Bearer <token>` (par défaut)
- Cookie HTTP-only
### Décision
**Authorization header.**
### Pourquoi
-**Standard OIDC** : tous les outils backends savent lire le header Authorization
-**Pas accessible par JavaScript** côté navigateur (sécurité XSS)
-**JavaScript ne peut PAS lire** le token côté browser (mais ce n'est pas notre besoin avec l'Approche A)
### Anti-pattern évité
**Ne PAS exposer le token JWT au JavaScript** côté navigateur via cookie non-HTTP-only. C'est une faille de sécurité courante.
---
## 🎯 D11 — Sub_filter nginx pour injecter l'email dans le HTML
### Contexte
Le JavaScript de l'intranet a besoin de connaître l'email de l'user connecté pour déterminer son rôle. Mais JS ne peut pas lire les headers HTTP serveur.
### Décision
**Module `sub_filter` de nginx** pour substituer `__USER_EMAIL__` par la vraie valeur du header `X-Forwarded-Email`.
### Pourquoi
-**Module natif nginx** : pas besoin d'installer Lua/njs
-**Simple à comprendre** : 3 lignes de config nginx
-**Performant** : aucun overhead notable
### Inconvénient accepté
- Si on change la valeur du placeholder (`__USER_EMAIL__`) dans l'HTML, faut aussi changer la config nginx
### Anti-pattern évité
**Ne PAS sur-ingénier** avec un mini-backend ou des modules nginx exotiques quand `sub_filter` natif suffit.
---
## 🎯 D12 — Stockage des secrets dans des `.env` séparés (1 par service)
### Contexte
Tous les services Docker ont leurs secrets dans des fichiers `.env`. On aurait pu :
- A : 1 seul `.env` global avec tous les secrets
- B : 1 `.env` par service
### Décision
**Option B — 1 `.env` par service.**
### Pourquoi
-**Isolation** : si un .env fuite, seul ce service est compromis
-**Permissions Linux** : `chmod 600` par fichier
-**Modifications ciblées** : changer le mdp Postgres ne touche pas les autres services
-**Sauvegarde différenciée** possible
### Anti-pattern évité
**Ne JAMAIS centraliser tous les secrets dans un seul fichier** "pour simplifier". C'est juste centraliser le risque.
---
## 🎯 D13 — Gitea SSO auto-registration activée
### Contexte
Gitea peut être configuré pour :
- A : Pré-créer les users en base + ils se loggent ensuite
- B : **Auto-créer** les users à leur 1ère connexion via OIDC
### Décision
**Option B — auto-registration via OIDC.**
### Configuration
Dans `~/docker/gitea/data/gitea/conf/app.ini` (ou via env vars) :
```ini
[oauth2_client]
ENABLE_AUTO_REGISTRATION = true
USERNAME = preferred_username
```
### Pourquoi
-**Pas de double création** : l'user existe dans Zitadel → 1ère connexion crée le compte Gitea
-**Username = trigramme** (préféré dans Seize)
-**Mise à jour automatique** des infos profile
### Anti-pattern évité
**Ne PAS créer manuellement les comptes** dans chaque outil si l'outil supporte l'auto-registration OIDC.
---
## 📚 Récap des anti-patterns à NE PAS reproduire
1.**IAM_OWNER pour les service accounts** → utiliser le rôle minimal
2.**Supprimer un compte admin sans backup** → garder un accès alternatif
3.**Nouveau project Zitadel** quand un existant a déjà les apps → ajouter dedans
4.**`scp` depuis le serveur SSH vers lui-même** → toujours depuis le PC client
5.**Coller des secrets dans des conversations IA** → gestionnaire de mots de passe
6.**Activer "Only authorized users"** sans process d'onboarding fluide
7.**Importer avec rôles + decisions business non validées** → metadata d'abord
8.**Pas de backup des données** → critique pour Zitadel et Gitea
9.**Sur-ingénierie sécurité** au-delà de la menace réelle
10.**Hardcoder les secrets** dans les `docker-compose.yml` → utiliser des `.env`