# 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](../DECISIONS.md). 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-/ ├── 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 ```yaml services: oauth2-proxy-: image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 container_name: oauth2-proxy- restart: unless-stopped env_file: - .env networks: - web labels: - "traefik.enable=true" - "traefik.http.routers.-sso.rule=Host(`.seizeconsulting.com`)" - "traefik.http.routers.-sso.entrypoints=websecure" - "traefik.http.routers.-sso.tls.certresolver=letsencrypt" - "traefik.http.routers.-sso.service=-sso" - "traefik.http.routers.-sso.priority=300" - "traefik.http.services.-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 (``) **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 ```env # ===== PROVIDER OIDC ZITADEL ===== OAUTH2_PROXY_PROVIDER=oidc OAUTH2_PROXY_CLIENT_ID= OAUTH2_PROXY_CLIENT_SECRET= OAUTH2_PROXY_COOKIE_SECRET= OAUTH2_PROXY_OIDC_ISSUER_URL=https://sso.seizeconsulting.com OAUTH2_PROXY_REDIRECT_URL=https://.seizeconsulting.com/oauth2/callback # ===== UPSTREAM (l'outil derrière) ===== OAUTH2_PROXY_UPSTREAMS=http://: # ===== 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=- ``` ### 🔑 Génération du cookie secret ```bash 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** : `` (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://.seizeconsulting.com/oauth2/callback` - Post-logout Redirect URLs : `https://.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 ```bash # 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 par "n8n" # Le sed ci-dessous fait ça automatiquement sed -i 's||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 ```bash # 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. ```bash # 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é. ```bash # 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](../DECISIONS.md)) ### "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` ```bash 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](./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](./05-applications.md). --- ## 🔄 Maintenance ### Mise à jour OAuth2-Proxy ```bash cd ~/docker/oauth2-proxy- docker compose pull docker compose up -d ``` ### Renouveler le Client Secret ```bash # 1. Dans Zitadel, regénérer le Client Secret de l'app # 2. Copier la nouvelle valeur # 3. Éditer .env nano ~/docker/oauth2-proxy-/.env # Remplacer OAUTH2_PROXY_CLIENT_SECRET # 4. Restart cd ~/docker/oauth2-proxy- docker compose down docker compose up -d ``` ### Rotation du cookie secret ⚠️ Tous les utilisateurs seront déconnectés. ```bash NEW_SECRET=$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))') echo "Nouveau cookie secret : $NEW_SECRET" # → Mettre à jour .env ```