seize-infra-doc/docs/04-oauth2-proxy-pattern.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

9.9 KiB

04 — Pattern OAuth2-Proxy par outil

Comment chaque outil "non-OIDC-native" reçoit le SSO via OAuth2-Proxy.


🎯 Principe

Beaucoup d'outils (Portainer, n8n, Code Server, Uptime Kuma...) n'ont pas de SSO OIDC natif. Pour leur ajouter le SSO, on intercale OAuth2-Proxy entre Traefik et l'outil :

User → Traefik → OAuth2-Proxy → Outil
                      ↓
                  Zitadel (auth)

Pourquoi 1 OAuth2-Proxy par outil (et pas centralisé) ?

Voir DECISIONS.md D2. En bref : isolation, config différenciée, debug plus simple.


📁 Anatomie d'un OAuth2-Proxy

Pour chaque outil, on a un dossier dédié :

~/docker/oauth2-proxy-<outil>/
├── docker-compose.yml
└── .env

⚠️ L'OAuth2-Proxy de l'intranet est dans ~/docker/oauth2-proxy/ (sans suffixe) pour des raisons historiques.


📄 docker-compose.yml — Template

services:
  oauth2-proxy-<outil>:
    image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
    container_name: oauth2-proxy-<outil>
    restart: unless-stopped
    env_file:
      - .env
    networks:
      - web
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.<outil>-sso.rule=Host(`<outil>.seizeconsulting.com`)"
      - "traefik.http.routers.<outil>-sso.entrypoints=websecure"
      - "traefik.http.routers.<outil>-sso.tls.certresolver=letsencrypt"
      - "traefik.http.routers.<outil>-sso.service=<outil>-sso"
      - "traefik.http.routers.<outil>-sso.priority=300"
      - "traefik.http.services.<outil>-sso.loadbalancer.server.port=4180"

networks:
  web:
    external: true

⚠️ Détails importants

  • priority=300 : pour que ce router prenne le pas sur d'autres routers potentiellement définis sur le même host (notamment Gitea SSO natif).
  • Le service backend (<outil>) ne doit PAS avoir ses propres labels Traefik pour exposer le host. Sinon conflit.
  • loadbalancer.server.port=4180 : OAuth2-Proxy écoute sur 4180 en interne.

📄 .env — Template

# ===== PROVIDER OIDC ZITADEL =====
OAUTH2_PROXY_PROVIDER=oidc
OAUTH2_PROXY_CLIENT_ID=<RECUPERER_DEPUIS_ZITADEL>
OAUTH2_PROXY_CLIENT_SECRET=<RECUPERER_DEPUIS_ZITADEL>
OAUTH2_PROXY_COOKIE_SECRET=<GENERER_32_CHARS_ALEATOIRES>
OAUTH2_PROXY_OIDC_ISSUER_URL=https://sso.seizeconsulting.com
OAUTH2_PROXY_REDIRECT_URL=https://<outil>.seizeconsulting.com/oauth2/callback

# ===== UPSTREAM (l'outil derrière) =====
OAUTH2_PROXY_UPSTREAMS=http://<outil>:<port_interne>

# ===== COOKIES =====
OAUTH2_PROXY_COOKIE_DOMAINS=.seizeconsulting.com
OAUTH2_PROXY_COOKIE_SECURE=true
OAUTH2_PROXY_COOKIE_HTTPONLY=true
OAUTH2_PROXY_COOKIE_SAMESITE=lax

# ===== SÉCURITÉ =====
OAUTH2_PROXY_EMAIL_DOMAINS=*
OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true
OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER=true
OAUTH2_PROXY_PASS_ACCESS_TOKEN=true
OAUTH2_PROXY_SET_AUTHORIZATION_HEADER=true
OAUTH2_PROXY_SET_XAUTHREQUEST=true

# ===== RÉSEAU =====
OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
OAUTH2_PROXY_REVERSE_PROXY=true
OAUTH2_PROXY_WHITELIST_DOMAINS=.seizeconsulting.com

# ===== SCOPES OIDC =====
# Le scope urn:zitadel:iam:org:project:roles est CRITIQUE pour le RBAC
OAUTH2_PROXY_SCOPE=openid email profile urn:zitadel:iam:org:project:roles

# ===== DIVERS =====
OAUTH2_PROXY_REDIRECT_URL_AUTH_DOMAIN_AS_REDIRECT_URL_SUFFIX=false
OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL=true
OAUTH2_PROXY_FOOTER=-
python3 -c 'import secrets; print(secrets.token_urlsafe(32))'

→ 32 caractères aléatoires, à utiliser comme OAUTH2_PROXY_COOKIE_SECRET.

⚠️ Si tu changes le cookie secret, tous les utilisateurs seront déconnectés et devront se relogger.


🔐 Côté Zitadel — Créer l'application

Pour chaque outil, créer une application dans Zitadel :

1. Console Zitadel → Projects → Infra Seize → Applications → New

2. Configuration

  • Name : <Outil> (ex: "n8n", "Portainer")
  • Type : Web
  • Click Continue

3. Authentication Method

  • Basic (Client ID + Client Secret)

4. Authorization

  • Authorization Code
  • Cocher Authorization Code uniquement
  • Ne PAS cocher Refresh Token sauf besoin

5. Redirect URLs

  • Redirect URLs : https://<outil>.seizeconsulting.com/oauth2/callback
  • Post-logout Redirect URLs : https://<outil>.seizeconsulting.com/

6. Confirmation et Token Settings

Après création, aller dans Token Settings de l'app :

Paramètre Valeur
Auth Token Type JWT
User roles inside ID Token
Include user's profile info in the ID Token
Add user roles to the access token
ClockSkew 0 (par défaut OK)

Save.

7. Récupérer Client ID + Client Secret

  • Client ID : visible en haut de l'app
  • Client Secret : Actions → Generate Client Secret → copier (montré 1 seule fois)

⚠️ Sauvegarder dans le gestionnaire de mots de passe immédiatement.


🚀 Déploiement d'un OAuth2-Proxy (pas à pas)

Exemple : OAuth2-Proxy pour n8n

# 1. Créer le dossier
mkdir -p ~/docker/oauth2-proxy-n8n
cd ~/docker/oauth2-proxy-n8n

# 2. Copier le template
cp ~/seize-infra-doc/configs/oauth2-proxy/docker-compose.yml.template docker-compose.yml
cp ~/seize-infra-doc/configs/oauth2-proxy/.env.example .env

# 3. Adapter le docker-compose.yml
# Remplacer <outil> par "n8n"
# Le sed ci-dessous fait ça automatiquement
sed -i 's|<outil>|n8n|g' docker-compose.yml

# 4. Éditer le .env (nano ou vim) avec les vraies valeurs
nano .env
# Remplir : CLIENT_ID, CLIENT_SECRET, COOKIE_SECRET, REDIRECT_URL, UPSTREAMS

# 5. Sécuriser le .env
chmod 600 .env

# 6. Démarrer
docker compose up -d

# 7. Vérifier les logs
docker logs --tail 30 oauth2-proxy-n8n

Vérification

# Test depuis le serveur
curl -sI http://localhost:4180/  # depuis le conteneur OAuth2-Proxy

# Test depuis l'extérieur (navigateur)
# Aller sur https://n8n.seizeconsulting.com
# → Redirection vers https://sso.seizeconsulting.com/oauth/v2/authorize?...

🐛 Troubleshooting OAuth2-Proxy

"Redirect loop infini"

Symptôme : le browser fait des allers-retours entre l'outil et Zitadel sans jamais aboutir.

Cause #1 : Cookie pas transmis par Traefik. → Vérifier ~/docker/traefik/dynamic.yml : authRequestHeaders contient bien Cookie.

Cause #2 : Cookie domain incorrect. → OAUTH2_PROXY_COOKIE_DOMAINS=.seizeconsulting.com (avec le point au début).

Cause #3 : Cookie secure mais HTTP utilisé. → S'assurer que toute la chaîne est en HTTPS.

"Bad Gateway" après login

Symptôme : login Zitadel OK, mais erreur 502 ensuite.

Cause : OAuth2-Proxy ne peut pas joindre l'upstream.

# Tester depuis le conteneur OAuth2-Proxy
docker exec -it oauth2-proxy-n8n wget -qO- http://n8n:5678

Si erreur : vérifier que l'upstream est sur le même réseau Docker (web).

"Erreur 401 sur /oauth2/callback"

Cause : Client Secret incorrect ou expiré.

# Vérifier dans Zitadel que le secret correspond à celui dans .env
# Regenerer le secret dans Zitadel et le remettre dans .env

"Access Denied" après login (alors qu'on a un compte)

Cause : projet Zitadel a "Only authorized users" coché, mais l'user n'a pas de rôle attribué.

Solution :

  • Soit attribuer un rôle à l'user
  • Soit décocher "Only authorized users" sur le project (voir DECISIONS.md D9)

"Le scope urn:zitadel:iam:org:project:roles ne fonctionne pas"

Vérifier :

  1. La case "Return user roles during authentication" est cochée sur le Project (pas seulement l'app)
  2. Le scope est bien dans .env OAuth2-Proxy
  3. Restart le conteneur après modif de .env
docker compose down
docker compose up -d

🔍 Voir le contenu du token JWT (debug)

⚠️ Procédure de debug uniquement — ne pas laisser activée en prod.

Voir 09-troubleshooting.md section "Nginx diagnostic".


📊 Headers transmis par OAuth2-Proxy à l'outil

Une fois la chaîne d'auth complète, OAuth2-Proxy passe ces headers à l'outil :

Header Valeur exemple Usage
X-Forwarded-User 375119988404518915 User ID Zitadel
X-Forwarded-Email hedi.kalboussi@seizeconsulting.com Email principal
X-Forwarded-Preferred-Username HKA Trigramme ou username
X-Forwarded-Groups (vide si claim Zitadel non plat) Groupes (pas exploitable directement avec Zitadel)
Authorization Bearer eyJ... Token JWT complet

⚠️ Pour récupérer les rôles, il faut décoder le JWT (les rôles sont dans un claim urn:zitadel:iam:org:project:roles, pas dans X-Forwarded-Groups).

→ C'est ce qu'on fait sur l'intranet via JavaScript + roles-mapping.json.


📋 Liste des OAuth2-Proxy déployés

OAuth2-Proxy Outil protégé Host
oauth2-proxy Intranet intranet.seizeconsulting.com
oauth2-proxy-n8n n8n n8n.seizeconsulting.com
oauth2-proxy-portainer Portainer portainer.seizeconsulting.com
oauth2-proxy-code Code Server code.seizeconsulting.com
oauth2-proxy-status Uptime Kuma status.seizeconsulting.com

⚠️ Gitea n'utilise PAS OAuth2-Proxy : il a son SSO OIDC natif. Voir 05-applications.md.


🔄 Maintenance

Mise à jour OAuth2-Proxy

cd ~/docker/oauth2-proxy-<outil>
docker compose pull
docker compose up -d

Renouveler le Client Secret

# 1. Dans Zitadel, regénérer le Client Secret de l'app
# 2. Copier la nouvelle valeur
# 3. Éditer .env
nano ~/docker/oauth2-proxy-<outil>/.env
# Remplacer OAUTH2_PROXY_CLIENT_SECRET

# 4. Restart
cd ~/docker/oauth2-proxy-<outil>
docker compose down
docker compose up -d

⚠️ Tous les utilisateurs seront déconnectés.

NEW_SECRET=$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')
echo "Nouveau cookie secret : $NEW_SECRET"
# → Mettre à jour .env