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

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-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 :

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


🎯 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).


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) :

[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