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

354 lines
9.9 KiB
Markdown

# 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-<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
```yaml
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
```env
# ===== 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=-
```
### 🔑 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** : `<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
```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 <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
```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-<outil>
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-<outil>/.env
# Remplacer OAUTH2_PROXY_CLIENT_SECRET
# 4. Restart
cd ~/docker/oauth2-proxy-<outil>
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
```