- 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
14 KiB
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-n8nne 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 :
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 :
- Lit les metadata du nouveau compte
- Les copie sur l'ancien compte
- 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.compeut 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
.envglobal avec tous les secrets - B : 1
.envpar service
Décision
Option B — 1 .env par service.
Pourquoi
- ✅ Isolation : si un .env fuite, seul ce service est compromis
- ✅ Permissions Linux :
chmod 600par 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) :
[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
- ❌ IAM_OWNER pour les service accounts → utiliser le rôle minimal
- ❌ Supprimer un compte admin sans backup → garder un accès alternatif
- ❌ Nouveau project Zitadel quand un existant a déjà les apps → ajouter dedans
- ❌
scpdepuis le serveur SSH vers lui-même → toujours depuis le PC client - ❌ Coller des secrets dans des conversations IA → gestionnaire de mots de passe
- ❌ Activer "Only authorized users" sans process d'onboarding fluide
- ❌ Importer avec rôles + decisions business non validées → metadata d'abord
- ❌ Pas de backup des données → critique pour Zitadel et Gitea
- ❌ Sur-ingénierie sécurité au-delà de la menace réelle
- ❌ Hardcoder les secrets dans les
docker-compose.yml→ utiliser des.env