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
This commit is contained in:
commit
1e90a85359
68
.env.example
Normal file
68
.env.example
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# .ENV.EXAMPLE — Variables globales pour l'install
|
||||||
|
# ============================================================================
|
||||||
|
# Copier ce fichier en .env et adapter les valeurs.
|
||||||
|
# NE JAMAIS commit .env sur Git.
|
||||||
|
|
||||||
|
# ===== INFRA =====
|
||||||
|
DOMAIN=seizeconsulting.com
|
||||||
|
SERVER_IP=151.115.148.243
|
||||||
|
SERVER_USER=admin
|
||||||
|
LETSENCRYPT_EMAIL=admin@seizeconsulting.com
|
||||||
|
|
||||||
|
# ===== POSTGRES PRINCIPAL =====
|
||||||
|
POSTGRES_PASSWORD=GENERATE_STRONG_PASSWORD_HERE
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
|
||||||
|
# Databases dédiées par service
|
||||||
|
GITEA_DB_PASSWORD=GENERATE_STRONG_PASSWORD_HERE
|
||||||
|
N8N_DB_PASSWORD=GENERATE_STRONG_PASSWORD_HERE
|
||||||
|
|
||||||
|
# ===== ZITADEL =====
|
||||||
|
ZITADEL_DOMAIN=sso.seizeconsulting.com
|
||||||
|
ZITADEL_MASTER_KEY=GENERATE_32_CHARS_RANDOM
|
||||||
|
ZITADEL_ADMIN_EMAIL=zitadel-admin@zitadel.sso.seizeconsulting.com
|
||||||
|
ZITADEL_ADMIN_PASSWORD=GENERATE_STRONG_PASSWORD_HERE
|
||||||
|
|
||||||
|
# ===== OAUTH2-PROXY (commun à tous les outils) =====
|
||||||
|
# Pour chaque outil, créer une App dans Zitadel et récupérer Client ID / Secret
|
||||||
|
|
||||||
|
# Cookie secret : généré par exemple via
|
||||||
|
# python3 -c 'import secrets; print(secrets.token_urlsafe(32))'
|
||||||
|
|
||||||
|
# ===== GITEA =====
|
||||||
|
GITEA_DOMAIN=git.seizeconsulting.com
|
||||||
|
GITEA_SSH_PORT=22
|
||||||
|
|
||||||
|
# ===== N8N =====
|
||||||
|
N8N_DOMAIN=n8n.seizeconsulting.com
|
||||||
|
N8N_ENCRYPTION_KEY=GENERATE_32_CHARS_RANDOM
|
||||||
|
|
||||||
|
# ===== CODE SERVER =====
|
||||||
|
CODE_DOMAIN=code.seizeconsulting.com
|
||||||
|
CODE_PASSWORD=GENERATE_STRONG_PASSWORD_HERE_FALLBACK
|
||||||
|
# Note : avec SSO, ce mdp ne sert que de fallback si SSO down
|
||||||
|
|
||||||
|
# ===== UPTIME KUMA =====
|
||||||
|
STATUS_DOMAIN=status.seizeconsulting.com
|
||||||
|
|
||||||
|
# ===== PORTAINER =====
|
||||||
|
PORTAINER_DOMAIN=portainer.seizeconsulting.com
|
||||||
|
|
||||||
|
# ===== INTRANET =====
|
||||||
|
INTRANET_DOMAIN=intranet.seizeconsulting.com
|
||||||
|
|
||||||
|
# ===== TRAEFIK DASHBOARD =====
|
||||||
|
TRAEFIK_DOMAIN=traefik.seizeconsulting.com
|
||||||
|
# Pour le basic auth Traefik :
|
||||||
|
# Générer avec : htpasswd -nb admin <password>
|
||||||
|
TRAEFIK_DASHBOARD_AUTH=admin:$$apr1$$xxxxxxxx$$xxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
|
# ===== FURIOUS SQUAD API =====
|
||||||
|
FURIOUS_USERNAME=GENERATE_FROM_FURIOUS_CONFIG
|
||||||
|
FURIOUS_PASSWORD=GENERATE_FROM_FURIOUS_CONFIG
|
||||||
|
FURIOUS_API_URL=https://seize-consulting.furious-squad.com/api/v2
|
||||||
|
|
||||||
|
# ===== ZITADEL SERVICE ACCOUNT (pour scripts) =====
|
||||||
|
# Chemin vers la clé JSON du service account furious-import-service
|
||||||
|
ZITADEL_SERVICE_KEY_PATH=/home/admin/furious-to-zitadel/zitadelfuriousimportkey.json
|
||||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.env
|
||||||
|
*.env
|
||||||
|
!.env.example
|
||||||
|
!.env.template
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
zitadelfuriousimportkey.json
|
||||||
|
users-credentials.csv
|
||||||
|
distribution-teams.csv
|
||||||
|
roles-mapping.json
|
||||||
|
letsencrypt/acme.json
|
||||||
|
*.log
|
||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.DS_Store
|
||||||
344
ARCHITECTURE.md
Normal file
344
ARCHITECTURE.md
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
# ARCHITECTURE — Vue d'ensemble technique
|
||||||
|
|
||||||
|
> Comprendre comment tout s'imbrique dans l'infra Seize.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Vue d'ensemble
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet
|
||||||
|
│
|
||||||
|
│ HTTPS (443) / HTTP (80)
|
||||||
|
▼
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ Traefik (proxy) │
|
||||||
|
│ - Let's Encrypt │
|
||||||
|
│ - Routing par Host │
|
||||||
|
└──────────┬───────────┘
|
||||||
|
│
|
||||||
|
┌──────────────┬─────────┼─────────┬──────────────┐
|
||||||
|
│ │ │ │ │
|
||||||
|
▼ ▼ ▼ ▼ ▼
|
||||||
|
┌──────────┐ ┌─────────────┐ ... ┌─────────────┐ ┌─────────────┐
|
||||||
|
│ Zitadel │ │ OAuth2-Proxy│ │ OAuth2-Proxy│ │ Gitea │
|
||||||
|
│ (sso.*) │ │ (intranet) │ │ (n8n) │ │ (git.*) SSO │
|
||||||
|
│ │ │ │ │ │ │ integré │
|
||||||
|
└────┬─────┘ └──────┬──────┘ └──────┬──────┘ └─────────────┘
|
||||||
|
│ │ │
|
||||||
|
│ Auth OIDC │ Forward proxy │ Forward proxy
|
||||||
|
│ ▼ ▼
|
||||||
|
│ ┌───────────┐ ┌───────────┐
|
||||||
|
│ │ Intranet │ │ n8n │
|
||||||
|
│ │ (nginx) │ │ │
|
||||||
|
│ │ + RBAC JS │ │ │
|
||||||
|
│ └───────────┘ └─────┬─────┘
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌──────────┐ ┌──────────┐
|
||||||
|
│ Postgres │ │ Postgres │
|
||||||
|
│ (zitadel)│ │ (n8n) │
|
||||||
|
└──────────┘ └──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Réseaux Docker
|
||||||
|
|
||||||
|
| Réseau | Type | Utilisé par |
|
||||||
|
|---|---|---|
|
||||||
|
| `web` | external | Traefik, tous les services exposés publiquement |
|
||||||
|
| `backend` | external | Postgres, Redis, et services backend internes |
|
||||||
|
| `zitadel` | local | Zitadel + son Traefik dédié + Postgres |
|
||||||
|
| `proxy` | bridge | Réseau auxiliaire (vérifier usage) |
|
||||||
|
|
||||||
|
⚠️ **Important** : Zitadel a son **propre réseau** + son **propre Traefik** (intégré dans son docker-compose). Il est totalement isolé du Traefik principal pour des raisons de sécurité. Le Traefik principal route uniquement vers le Traefik Zitadel sur `sso.seizeconsulting.com`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Flux d'authentification SSO
|
||||||
|
|
||||||
|
### Flux complet pour un user accédant à l'intranet
|
||||||
|
|
||||||
|
```
|
||||||
|
1. User → https://intranet.seizeconsulting.com
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
2. Traefik principal → route vers OAuth2-Proxy "intranet"
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
3. OAuth2-Proxy vérifie le cookie de session
|
||||||
|
│
|
||||||
|
┌────┴────┐
|
||||||
|
│ Pas de │
|
||||||
|
│ session │
|
||||||
|
▼ │
|
||||||
|
4. OAuth2-Proxy redirige vers Zitadel
|
||||||
|
(URL : /oauth2/start → Zitadel login)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
5. User saisit ses identifiants Zitadel + MFA
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
6. Zitadel valide + génère un code OIDC
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
7. Redirect vers OAuth2-Proxy /oauth2/callback?code=xxx
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
8. OAuth2-Proxy échange le code contre un token JWT
|
||||||
|
(avec scope : openid email profile urn:zitadel:iam:org:project:roles)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
9. OAuth2-Proxy stocke le token + cookie de session
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
10. OAuth2-Proxy → forward request vers nginx (intranet)
|
||||||
|
avec headers :
|
||||||
|
- X-Forwarded-User: <user_id>
|
||||||
|
- X-Forwarded-Email: hedi.kalboussi@seizeconsulting.com
|
||||||
|
- X-Forwarded-Preferred-Username: HKA
|
||||||
|
- Authorization: Bearer <jwt_token>
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
11. nginx lit le header X-Forwarded-Email
|
||||||
|
Substitue __USER_EMAIL__ par la vraie valeur dans index.html (sub_filter)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
12. Browser reçoit l'HTML avec const USER_EMAIL = "hedi.kalboussi@..."
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
13. JavaScript :
|
||||||
|
- Fetch /roles-mapping.json
|
||||||
|
- Lookup userEmail → role (ex: "consultant_junior")
|
||||||
|
- Cache les cartes que ce rôle n'a pas le droit de voir
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
14. User voit uniquement les cartes autorisées
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ Structure des dossiers Docker sur le serveur
|
||||||
|
|
||||||
|
```
|
||||||
|
/home/hedi/docker/
|
||||||
|
├── traefik/ # Reverse proxy principal
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── traefik.yml # Config statique
|
||||||
|
│ ├── dynamic.yml # Config dynamique (middlewares, etc.)
|
||||||
|
│ └── letsencrypt/ # Certificats TLS auto
|
||||||
|
│
|
||||||
|
├── zitadel/ # IDP Zitadel (avec son propre Traefik)
|
||||||
|
│ ├── docker-compose.yml # Multi-services (proxy + login + api + db)
|
||||||
|
│ ├── .env # Tous les secrets Zitadel
|
||||||
|
│ └── machinekey/ # Clés service accounts
|
||||||
|
│
|
||||||
|
├── postgres/ # Postgres principal (Gitea, n8n, Authentik historique)
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ └── data/ # Persistance
|
||||||
|
│
|
||||||
|
├── redis/ # Plus utilisé (Authentik historique)
|
||||||
|
│
|
||||||
|
├── intranet/ # ⭐ Page d'accueil avec RBAC
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── index.html # HTML avec data-app="xxx" + script RBAC
|
||||||
|
│ ├── roles-mapping.json # Mapping email → rôle (généré par script)
|
||||||
|
│ ├── nginx-rbac.conf # nginx avec sub_filter pour __USER_EMAIL__
|
||||||
|
│ └── logo.png
|
||||||
|
│
|
||||||
|
├── gitea/ # Forge Git avec SSO Zitadel intégré
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── .env
|
||||||
|
│ └── data/
|
||||||
|
│
|
||||||
|
├── n8n/ # Workflows
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── .env
|
||||||
|
│ └── data/
|
||||||
|
│
|
||||||
|
├── portainer/ # Gestion Docker
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ └── data/
|
||||||
|
│
|
||||||
|
├── code-server/ # VS Code dans le navigateur
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── .env
|
||||||
|
│ └── config/
|
||||||
|
│
|
||||||
|
├── uptime-kuma/ # Monitoring
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ └── data/
|
||||||
|
│
|
||||||
|
├── oauth2-proxy/ # ⭐ OAuth2-Proxy pour l'intranet
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ └── .env
|
||||||
|
│
|
||||||
|
├── oauth2-proxy-n8n/ # OAuth2-Proxy pour n8n
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ └── .env
|
||||||
|
│
|
||||||
|
├── oauth2-proxy-portainer/ # OAuth2-Proxy pour Portainer
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ └── .env
|
||||||
|
│
|
||||||
|
├── oauth2-proxy-code/ # OAuth2-Proxy pour Code Server
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ └── .env
|
||||||
|
│
|
||||||
|
├── oauth2-proxy-status/ # OAuth2-Proxy pour Uptime Kuma
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ └── .env
|
||||||
|
│
|
||||||
|
├── authentik/ # ❌ Plus utilisé (à archiver/supprimer)
|
||||||
|
│
|
||||||
|
├── whoami/ # Test debug (à garder ou supprimer)
|
||||||
|
│
|
||||||
|
└── ctop/ # Outil CLI monitoring conteneurs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 Le pattern "OAuth2-Proxy par outil"
|
||||||
|
|
||||||
|
### Pourquoi ce pattern ?
|
||||||
|
|
||||||
|
Chaque outil (n8n, Portainer, etc.) a son **propre OAuth2-Proxy dédié**. C'est volontaire :
|
||||||
|
|
||||||
|
✅ **Avantages** :
|
||||||
|
- Configuration différenciée par outil (scopes, callbacks, etc.)
|
||||||
|
- Pas de SPOF : si un OAuth2-Proxy crash, les autres outils marchent
|
||||||
|
- Plus simple à débugger
|
||||||
|
- Permet d'avoir des secrets/clients OIDC différents
|
||||||
|
|
||||||
|
❌ **Inconvénients** :
|
||||||
|
- 5 conteneurs OAuth2-Proxy en plus (impact RAM minime)
|
||||||
|
- 5 configurations à maintenir
|
||||||
|
|
||||||
|
### Anatomie d'un OAuth2-Proxy
|
||||||
|
|
||||||
|
Pour chaque outil, on a :
|
||||||
|
|
||||||
|
1. **Un dossier `~/docker/oauth2-proxy-<outil>/`**
|
||||||
|
2. **Un `docker-compose.yml`** avec labels Traefik routant `<outil>.seizeconsulting.com` → OAuth2-Proxy port 4180
|
||||||
|
3. **Un `.env`** avec :
|
||||||
|
- `OAUTH2_PROXY_PROVIDER=oidc`
|
||||||
|
- `OAUTH2_PROXY_CLIENT_ID=<id_zitadel>`
|
||||||
|
- `OAUTH2_PROXY_CLIENT_SECRET=<secret_zitadel>`
|
||||||
|
- `OAUTH2_PROXY_OIDC_ISSUER_URL=https://sso.seizeconsulting.com`
|
||||||
|
- `OAUTH2_PROXY_REDIRECT_URL=https://<outil>.seizeconsulting.com/oauth2/callback`
|
||||||
|
- `OAUTH2_PROXY_UPSTREAMS=http://<outil>:<port>`
|
||||||
|
- `OAUTH2_PROXY_COOKIE_SECRET=<32_chars_random>`
|
||||||
|
- `OAUTH2_PROXY_SCOPE=openid email profile urn:zitadel:iam:org:project:roles`
|
||||||
|
4. **Une App créée dans Zitadel** (project "Infra Seize") avec les bons callbacks
|
||||||
|
|
||||||
|
Détails complets dans [docs/04-oauth2-proxy-pattern.md](./docs/04-oauth2-proxy-pattern.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧬 Le bug Traefik historique (à connaître)
|
||||||
|
|
||||||
|
**Symptôme initial** : tous les outils SSO étaient déconnectés à chaque requête.
|
||||||
|
|
||||||
|
**Cause** : Traefik supprimait par défaut le header `Cookie` quand il faisait un ForwardAuth vers OAuth2-Proxy. Du coup OAuth2-Proxy ne voyait pas le cookie de session et redirigeait vers login à chaque fois.
|
||||||
|
|
||||||
|
**Fix** : ajouter dans `~/docker/traefik/dynamic.yml` :
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
oauth2-headers:
|
||||||
|
forwardAuth:
|
||||||
|
address: "http://oauth2-proxy:4180/oauth2/auth"
|
||||||
|
authRequestHeaders:
|
||||||
|
- Cookie # ← LIGNE CRITIQUE
|
||||||
|
- Authorization
|
||||||
|
authResponseHeaders:
|
||||||
|
- X-Auth-Request-User
|
||||||
|
- X-Auth-Request-Email
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Sans `Cookie` dans `authRequestHeaders`, **rien ne fonctionne**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 RBAC : architecture choisie (Approche A)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Furious Squad (source de vérité RH)
|
||||||
|
│
|
||||||
|
│ API GraphQL-like
|
||||||
|
▼
|
||||||
|
2. Scripts Python d'import (~/furious-to-zitadel/)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
3. Zitadel : 88 users avec metadata custom
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
4. Script generate-roles-mapping.py
|
||||||
|
→ génère /docker/intranet/roles-mapping.json
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
5. Intranet (nginx + sub_filter)
|
||||||
|
→ injecte __USER_EMAIL__ depuis X-Forwarded-Email
|
||||||
|
→ JavaScript lookup dans roles-mapping.json
|
||||||
|
→ cache/affiche les cartes selon rôle
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pourquoi Approche A (et pas RBAC complet sur chaque outil) ?
|
||||||
|
|
||||||
|
Détails dans [DECISIONS.md](./DECISIONS.md), section "RBAC Approche A".
|
||||||
|
|
||||||
|
**TL;DR** : 0€, 2-3h de boulot, suffisant pour Seize (88 employés majoritairement non-techniques).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Volumes critiques à backuper
|
||||||
|
|
||||||
|
| Données | Volume / Chemin | Criticité |
|
||||||
|
|---|---|---|
|
||||||
|
| Base PostgreSQL Zitadel | `~/docker/zitadel/.../data/postgres/` | 🔴 Critique |
|
||||||
|
| Base PostgreSQL Gitea/n8n | `~/docker/postgres/data/` | 🔴 Critique |
|
||||||
|
| Données Gitea (repos) | `~/docker/gitea/data/` | 🔴 Critique |
|
||||||
|
| Données Portainer | `~/docker/portainer/data/` | 🟡 Important |
|
||||||
|
| Données Uptime Kuma | `~/docker/uptime-kuma/data/` | 🟡 Important |
|
||||||
|
| Données n8n | volume Docker `n8n_data` | 🟡 Important |
|
||||||
|
| Données Code Server | `~/docker/code-server/config/` | 🟢 Récupérable |
|
||||||
|
| Certificats Let's Encrypt | `~/docker/traefik/letsencrypt/` | 🟢 Auto-régénéré |
|
||||||
|
| Scripts d'import + clés | `~/furious-to-zitadel/` | 🔴 Critique |
|
||||||
|
|
||||||
|
⚠️ **Aucun backup automatique en place** au moment de la passation. À mettre en priorité.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Outils CLI utiles installés
|
||||||
|
|
||||||
|
| Outil | Usage |
|
||||||
|
|---|---|
|
||||||
|
| `docker` | Gestion conteneurs |
|
||||||
|
| `docker compose` | Orchestration |
|
||||||
|
| `ctop` | Top des conteneurs (consommation ressources) |
|
||||||
|
| `sudo journalctl` | Logs système |
|
||||||
|
| `htop` | Top processus |
|
||||||
|
| `python3` + venv | Pour les scripts d'import |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌍 DNS — Records configurés (chez le registrar)
|
||||||
|
|
||||||
|
Tous les sous-domaines pointent vers `151.115.148.243` :
|
||||||
|
|
||||||
|
| Record | Type | Valeur |
|
||||||
|
|---|---|---|
|
||||||
|
| `seizeconsulting.com` | A | 151.115.148.243 |
|
||||||
|
| `sso.seizeconsulting.com` | A | 151.115.148.243 |
|
||||||
|
| `intranet.seizeconsulting.com` | A | 151.115.148.243 |
|
||||||
|
| `git.seizeconsulting.com` | A | 151.115.148.243 |
|
||||||
|
| `n8n.seizeconsulting.com` | A | 151.115.148.243 |
|
||||||
|
| `portainer.seizeconsulting.com` | A | 151.115.148.243 |
|
||||||
|
| `code.seizeconsulting.com` | A | 151.115.148.243 |
|
||||||
|
| `status.seizeconsulting.com` | A | 151.115.148.243 |
|
||||||
|
| `traefik.seizeconsulting.com` | A | 151.115.148.243 |
|
||||||
|
| `app.seizeconsulting.com` | A | 151.115.148.243 |
|
||||||
|
|
||||||
|
⚠️ **Ne PAS oublier** d'ajouter un record DNS si tu ajoutes un nouvel outil. Sinon Let's Encrypt échoue.
|
||||||
356
DECISIONS.md
Normal file
356
DECISIONS.md
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
# 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` :
|
||||||
|
```yaml
|
||||||
|
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` :
|
||||||
|
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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 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 `.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) :
|
||||||
|
```ini
|
||||||
|
[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`
|
||||||
226
HANDOVER.md
Normal file
226
HANDOVER.md
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
# 🚨 HANDOVER — Passation au prochain mainteneur
|
||||||
|
|
||||||
|
> Ce document est **prioritaire**. Lis-le **avant tout** si tu reprends le projet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Ce qui marche (état au 5 juin 2026)
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- ✅ Stack Docker sur Scaleway, 24 conteneurs actifs
|
||||||
|
- ✅ Traefik comme reverse proxy avec Let's Encrypt auto
|
||||||
|
- ✅ Zitadel comme IDP, accessible sur `sso.seizeconsulting.com`
|
||||||
|
- ✅ 7 sous-domaines en HTTPS opérationnels
|
||||||
|
- ✅ Pattern OAuth2-Proxy par outil pour le SSO
|
||||||
|
|
||||||
|
### Utilisateurs
|
||||||
|
- ✅ 88 collaborateurs Seize importés dans Zitadel
|
||||||
|
- ✅ 4 anciens comptes (HKA, PSP, PYT, Jules) conservés avec leur MFA
|
||||||
|
- ✅ Metadata Furious copiées sur tous les comptes (job_title, bu_name, manager, etc.)
|
||||||
|
- ✅ Mots de passe temporaires générés dans `users-credentials.csv`
|
||||||
|
|
||||||
|
### RBAC
|
||||||
|
- ✅ 8 rôles créés dans le project "Infra Seize" (Zitadel)
|
||||||
|
- ✅ 87/88 users avec rôle attribué (1 sans : le user "vide" Furious à corriger)
|
||||||
|
- ✅ Intranet filtre les cartes selon le rôle
|
||||||
|
- ✅ Testé sur HKA (consultant_junior) : voit Gitea + Code Server uniquement
|
||||||
|
|
||||||
|
### Imports automatisés
|
||||||
|
- ✅ Scripts Python dans `~/furious-to-zitadel/` opérationnels
|
||||||
|
- ✅ Mode dry-run / test / prod pour `import-furious-to-zitadel.py`
|
||||||
|
- ✅ Script de fusion de doublons (`merge-duplicates.py`)
|
||||||
|
- ✅ Script de génération du mapping RBAC (`generate-roles-mapping.py`)
|
||||||
|
- ✅ Script d'attribution des rôles (`assign-roles.py`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 À faire en priorité (urgent)
|
||||||
|
|
||||||
|
### 1. Rotation des secrets exposés
|
||||||
|
|
||||||
|
Plusieurs secrets ont été partagés dans des conversations IA pendant le développement. **À régénérer immédiatement** :
|
||||||
|
|
||||||
|
| Secret | Où | Action |
|
||||||
|
|---|---|---|
|
||||||
|
| Compte API Furious `mick.emmaus.90849` | Furious > Configuration API | Régénérer le mdp |
|
||||||
|
| Token JWT user HKA (1 fois, expiré) | N/A | Déjà expiré, juste vérifier |
|
||||||
|
| Mdp Postgres Gitea | `~/docker/gitea/.env` (`GITEA_DB_PASSWORD`) | Régénérer + restart |
|
||||||
|
| Secrets Gitea (LFS_JWT, INTERNAL_TOKEN, JWT_SECRET) | `~/docker/gitea/.env` | Régénérer + restart |
|
||||||
|
| Clé service account Zitadel `zitadelfuriousimportkey.json` | `~/furious-to-zitadel/` | Régénérer si suspicion compromission |
|
||||||
|
|
||||||
|
### 2. Distribuer les 88 mdp temporaires
|
||||||
|
|
||||||
|
Le fichier `~/furious-to-zitadel/distribution-teams.csv` contient 88 lignes :
|
||||||
|
`Nom complet | Email | Mot de passe temporaire`
|
||||||
|
|
||||||
|
**Procédure recommandée** (validée avec Philippe) :
|
||||||
|
- Envoyer 1 mdp par message **privé Teams** (jamais en canal public)
|
||||||
|
- Indiquer URL : https://intranet.seizeconsulting.com
|
||||||
|
- Préciser : "Mdp temporaire à changer à la 1ère connexion"
|
||||||
|
- Étaler sur 5-7 jours (10-20 par jour) pour gérer le support 1er login
|
||||||
|
|
||||||
|
**Une fois distribution terminée** : supprimer le CSV (corbeille + vider la corbeille).
|
||||||
|
|
||||||
|
### 3. Configurer SMTP M365 (délégué à l'admin M365)
|
||||||
|
|
||||||
|
Sans SMTP, Zitadel ne peut pas envoyer d'emails (welcome, reset password, MFA).
|
||||||
|
|
||||||
|
**Procédure** :
|
||||||
|
1. Demander à l'admin M365 de créer `noreply@seizeconsulting.com`
|
||||||
|
2. Activer SMTP AUTH sur ce compte
|
||||||
|
3. Générer un App Password
|
||||||
|
4. Configurer dans Zitadel : Default settings → SMTP Provider
|
||||||
|
5. Tester l'envoi
|
||||||
|
|
||||||
|
Détails dans [docs/08-secrets-management.md](./docs/08-secrets-management.md).
|
||||||
|
|
||||||
|
### 4. Nettoyer Authentik (plus utilisé)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/docker/authentik
|
||||||
|
sudo docker compose down
|
||||||
|
# Optionnel : sauvegarder le dossier avant suppression
|
||||||
|
mv ~/docker/authentik ~/docker/authentik.archived
|
||||||
|
```
|
||||||
|
|
||||||
|
Idem pour Redis si plus utilisé par d'autres services (à vérifier).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 À faire ensuite (important)
|
||||||
|
|
||||||
|
### 5. Documenter pour Philippe
|
||||||
|
|
||||||
|
Philippe a validé l'import et la matrice RBAC en oral mais **n'a pas vu la liste exacte** des 88 users.
|
||||||
|
|
||||||
|
À lui transmettre :
|
||||||
|
- Liste des 88 users importés (sans les mdp, pour traçabilité)
|
||||||
|
- Matrice RBAC effective (cf. [matrices/rbac-matrix.md](./matrices/rbac-matrix.md))
|
||||||
|
- Date/heure d'import : 5 juin 2026 07:27-07:42
|
||||||
|
|
||||||
|
### 6. Workshop validation matrice RBAC
|
||||||
|
|
||||||
|
La matrice actuelle a été **tranchée par Hedi seul** car Philippe pas dispo le jour de l'import. À valider en workshop :
|
||||||
|
- Direction = Président + Directeur + Directeur Associé ?
|
||||||
|
- Manager = inclut Manager BI ?
|
||||||
|
- Experts = au-dessus ou en parallèle des Managers ?
|
||||||
|
- Apprentis vs Stagiaires : 2 rôles distincts ou 1 seul ?
|
||||||
|
|
||||||
|
### 7. Corriger l'anomalie du user "vide" Furious
|
||||||
|
|
||||||
|
1 collaborateur a `job_title` vide dans Furious → pas de rôle Zitadel attribué.
|
||||||
|
**Action** : identifier qui (Hedi sait) et compléter dans Furious, puis relancer `assign-roles.py`.
|
||||||
|
|
||||||
|
### 8. Corriger les anomalies d'orthographe Furious
|
||||||
|
|
||||||
|
Doublons dans Furious à standardiser :
|
||||||
|
- `Senior Manager` vs `Sénior Manager`
|
||||||
|
- `Consultant Expérimenté` vs `Consultant Expérimentée` vs `Consultant Sénior`
|
||||||
|
- `Consultant Expert niveau 1` (avec espace en début parfois)
|
||||||
|
|
||||||
|
Le script `assign-roles.py` gère ces variantes via mapping mais c'est mieux de standardiser à la source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 À faire à terme (moyen/long terme)
|
||||||
|
|
||||||
|
### 9. Décision business : migrations outils gratuits OU licences pro ?
|
||||||
|
|
||||||
|
Pour avoir un **vrai RBAC** (pas juste cosmétique sur l'intranet), il faut :
|
||||||
|
|
||||||
|
**Option A** — Migrations gratuites (5-6j de boulot, 0€/an) :
|
||||||
|
- n8n → Activepieces
|
||||||
|
- Portainer CE → Komodo
|
||||||
|
- Uptime Kuma → Gatus
|
||||||
|
- Code Server → OpenVSCode Server (RBAC limité)
|
||||||
|
|
||||||
|
**Option B** — Licences pro (~25 800€/an, 1j de config) :
|
||||||
|
- n8n Enterprise
|
||||||
|
- Portainer Business
|
||||||
|
- (le reste reste gratuit)
|
||||||
|
|
||||||
|
**Décision sponsor** : Philippe + Pierre-Yves Tachon.
|
||||||
|
|
||||||
|
### 10. Workflow n8n de sync continue Furious → Zitadel
|
||||||
|
|
||||||
|
Idée : tous les jours à 3h du matin, n8n :
|
||||||
|
- Récupère la liste des users Furious
|
||||||
|
- Identifie les nouveaux arrivants → crée dans Zitadel
|
||||||
|
- Identifie les départs (`departure_date != "0000-00-00"`) → désactive dans Zitadel
|
||||||
|
- Met à jour les metadata si changement
|
||||||
|
|
||||||
|
⏱️ ~1 journée de dev n8n.
|
||||||
|
|
||||||
|
### 11. Custom Claims OIDC (attendre Actions V2 Zitadel)
|
||||||
|
|
||||||
|
Pour vraiment transmettre les rôles aux outils backends (Gitea, n8n, etc.) en plus de l'intranet, on attend la stabilisation d'**Actions V2** dans Zitadel.
|
||||||
|
|
||||||
|
### 12. Fédération M365/Entra ID ↔ Zitadel
|
||||||
|
|
||||||
|
Plus tard : permettre aux users de se logger via leur compte M365 entreprise.
|
||||||
|
|
||||||
|
### 13. Backup chiffré + plan de reprise
|
||||||
|
|
||||||
|
Actuellement, **aucun backup automatisé**. Critique pour la base PostgreSQL de Zitadel et Gitea.
|
||||||
|
|
||||||
|
À mettre en place :
|
||||||
|
- Backup quotidien de PostgreSQL (avec `pg_dump`)
|
||||||
|
- Backup des volumes Docker (Gitea data, Zitadel data)
|
||||||
|
- Stockage offsite (Scaleway Object Storage chiffré)
|
||||||
|
|
||||||
|
### 14. Monitoring sécurité avancé
|
||||||
|
|
||||||
|
- **Fail2ban** ou **Crowdsec** pour bloquer les IPs malveillantes (les logs OAuth2-Proxy montrent beaucoup de tentatives de scans)
|
||||||
|
- Alerting sur tentatives de login échouées (via Uptime Kuma ou autre)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗣️ Conversations / décisions clés à connaître
|
||||||
|
|
||||||
|
### Décisions architecturales tranchées
|
||||||
|
|
||||||
|
1. **Zitadel plutôt qu'Authentik** : meilleur support API, plus stable, mieux documenté
|
||||||
|
2. **OAuth2-Proxy par outil (pas centralisé)** : permet config différenciée par app
|
||||||
|
3. **RBAC sur intranet uniquement (Approche A)** : zéro coût, suffisant à 90% pour 88 employés non-techniques
|
||||||
|
4. **Garder anciens comptes (HKA, PSP, PYT, Jules)** avec MFA, supprimer les doublons importés
|
||||||
|
5. **Metadata custom plutôt que rôles "vrais" Zitadel** au moment de l'import (rôles ajoutés ensuite)
|
||||||
|
6. **Pas de SMTP configuré** : mdp distribués manuellement via Teams
|
||||||
|
7. **Pas de migration M365 immédiate** : les users gardent leurs comptes Zitadel locaux
|
||||||
|
|
||||||
|
### Anti-patterns identifiés
|
||||||
|
|
||||||
|
- ❌ **Ne PAS donner IAM_OWNER aux service accounts** → utiliser le rôle minimal (`IAM_USER_MANAGER`)
|
||||||
|
- ❌ **Ne PAS créer un nouveau project Zitadel** pour les rôles si "Infra Seize" existe déjà (sinon OAuth2-Proxy ne les voit pas)
|
||||||
|
- ❌ **Ne PAS exécuter `scp` depuis le serveur SSH** vers lui-même — toujours depuis le PC client
|
||||||
|
- ❌ **Ne PAS coller de secrets** (tokens, mdp, clés API) dans des conversations IA — utiliser un gestionnaire de mots de passe
|
||||||
|
- ❌ **Ne PAS supprimer un user admin** sans avoir vérifié qu'on a un autre accès admin (risque de blocage)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Contacts utiles
|
||||||
|
|
||||||
|
| Personne | Rôle | Contact |
|
||||||
|
|---|---|---|
|
||||||
|
| Hedi Kalboussi (HKA) | Mainteneur sortant | Slack/Teams interne |
|
||||||
|
| Philippe Spoljaric (PSP) | Sponsor + Admin Infra | Slack/Teams interne |
|
||||||
|
| Pierre-Yves Tachon (PYT) | CEO + arbitrage budget | Slack/Teams interne |
|
||||||
|
| Support Furious Squad | API + données RH | seize-consulting.furious-squad.com |
|
||||||
|
| Scaleway | Hébergeur | console.scaleway.com |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Premier jour de la passation — checklist
|
||||||
|
|
||||||
|
- [ ] Lire ce HANDOVER en entier
|
||||||
|
- [ ] Lire ARCHITECTURE.md
|
||||||
|
- [ ] Se connecter en SSH au serveur : `ssh hedi@151.115.148.243`
|
||||||
|
- [ ] Demander accès Zitadel admin à Hedi
|
||||||
|
- [ ] Lister les conteneurs : `sudo docker ps`
|
||||||
|
- [ ] Vérifier les logs récents : `sudo docker logs --tail 50 traefik`
|
||||||
|
- [ ] Tester un login en HKA depuis incognito
|
||||||
|
- [ ] Faire un point téléphone avec Hedi pour Q&A
|
||||||
|
- [ ] Identifier les 1-2 actions urgentes à reprendre
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Bon courage, et bienvenue dans le projet !** 🚀
|
||||||
522
INSTALL.md
Normal file
522
INSTALL.md
Normal file
@ -0,0 +1,522 @@
|
|||||||
|
# INSTALL — Déploiement from scratch sur un nouveau serveur
|
||||||
|
|
||||||
|
> Procédure complète pour reproduire l'infrastructure Seize sur un nouveau serveur.
|
||||||
|
|
||||||
|
⏱️ **Temps total estimé** : 4-6h pour un sysadmin expérimenté.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Prérequis
|
||||||
|
|
||||||
|
### Côté infrastructure
|
||||||
|
|
||||||
|
- [ ] **Serveur Linux** : 4 vCPU, 8 Go RAM minimum (16 Go recommandé), 80 Go disque
|
||||||
|
- [ ] **OS** : Debian 13 (Trixie) ou Ubuntu 22.04+
|
||||||
|
- [ ] **IP publique fixe** (chez Scaleway ou autre)
|
||||||
|
- [ ] **Ports ouverts** : 22 (SSH), 80 (HTTP), 443 (HTTPS)
|
||||||
|
|
||||||
|
### Côté domaine
|
||||||
|
|
||||||
|
- [ ] Un **domaine** que vous contrôlez (ex: `seizeconsulting.com`)
|
||||||
|
- [ ] Accès à la **zone DNS** pour ajouter des records A
|
||||||
|
|
||||||
|
### Côté tiers
|
||||||
|
|
||||||
|
- [ ] Compte **Furious Squad** avec droits admin (pour créer un compte API)
|
||||||
|
- [ ] (Optionnel) Compte **Microsoft 365** avec admin pour le SMTP
|
||||||
|
|
||||||
|
### Côté local
|
||||||
|
|
||||||
|
- [ ] Un **PC** avec SSH et SCP (Windows PowerShell, Linux, ou macOS)
|
||||||
|
- [ ] Un **gestionnaire de mots de passe** (Bitwarden, KeePass, 1Password)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Étape 0 — Configuration DNS
|
||||||
|
|
||||||
|
⏱️ **15 min** (mais propagation peut prendre 1h+)
|
||||||
|
|
||||||
|
Chez votre registrar (Scaleway, OVH, Gandi, etc.), ajouter les records A suivants :
|
||||||
|
|
||||||
|
| Record | Type | Valeur |
|
||||||
|
|---|---|---|
|
||||||
|
| `@` (ou `seizeconsulting.com`) | A | `<IP_DU_SERVEUR>` |
|
||||||
|
| `sso` | A | `<IP_DU_SERVEUR>` |
|
||||||
|
| `intranet` | A | `<IP_DU_SERVEUR>` |
|
||||||
|
| `git` | A | `<IP_DU_SERVEUR>` |
|
||||||
|
| `n8n` | A | `<IP_DU_SERVEUR>` |
|
||||||
|
| `portainer` | A | `<IP_DU_SERVEUR>` |
|
||||||
|
| `code` | A | `<IP_DU_SERVEUR>` |
|
||||||
|
| `status` | A | `<IP_DU_SERVEUR>` |
|
||||||
|
| `traefik` | A | `<IP_DU_SERVEUR>` |
|
||||||
|
|
||||||
|
**Vérification** :
|
||||||
|
```bash
|
||||||
|
nslookup sso.seizeconsulting.com
|
||||||
|
```
|
||||||
|
→ Doit retourner l'IP du serveur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐧 Étape 1 — Préparation du serveur
|
||||||
|
|
||||||
|
⏱️ **20 min**
|
||||||
|
|
||||||
|
### 1.1 — Connexion SSH initiale
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@<IP_DU_SERVEUR>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 — Créer un user non-root
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adduser admin
|
||||||
|
usermod -aG sudo admin
|
||||||
|
mkdir -p /home/admin/.ssh
|
||||||
|
cp ~/.ssh/authorized_keys /home/admin/.ssh/
|
||||||
|
chown -R admin:admin /home/admin/.ssh
|
||||||
|
chmod 600 /home/admin/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 — Hardening SSH
|
||||||
|
|
||||||
|
Éditer `/etc/ssh/sshd_config` :
|
||||||
|
```
|
||||||
|
PermitRootLogin no
|
||||||
|
PasswordAuthentication no
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart : `systemctl restart sshd`
|
||||||
|
|
||||||
|
### 1.4 — Reconnexion en tant que l'utilisateur non-root
|
||||||
|
|
||||||
|
```bash
|
||||||
|
exit
|
||||||
|
ssh admin@<IP_DU_SERVEUR>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.5 — Mise à jour système
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update && sudo apt upgrade -y
|
||||||
|
sudo apt install -y curl wget git vim htop unzip
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 Étape 2 — Installation Docker
|
||||||
|
|
||||||
|
⏱️ **10 min**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Installation Docker depuis le repo officiel
|
||||||
|
curl -fsSL https://get.docker.com | sudo sh
|
||||||
|
|
||||||
|
# Ajouter l'utilisateur au groupe docker
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
|
||||||
|
# Se relogger pour prendre en compte le groupe
|
||||||
|
exit
|
||||||
|
ssh admin@<IP_DU_SERVEUR>
|
||||||
|
|
||||||
|
# Vérification
|
||||||
|
docker --version
|
||||||
|
docker compose version
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Tu dois voir : Docker version 27.x ou plus récent, Docker Compose v2.x
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Étape 3 — Création des réseaux Docker
|
||||||
|
|
||||||
|
⏱️ **2 min**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create web
|
||||||
|
docker network create backend
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Ces réseaux seront partagés entre tous les services.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Étape 4 — Installation Traefik
|
||||||
|
|
||||||
|
⏱️ **30 min**
|
||||||
|
|
||||||
|
Voir [docs/02-traefik-setup.md](./docs/02-traefik-setup.md) pour les détails.
|
||||||
|
|
||||||
|
### Résumé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/docker/traefik/letsencrypt
|
||||||
|
cd ~/docker/traefik
|
||||||
|
# Copier le docker-compose.yml depuis configs/traefik/
|
||||||
|
# Copier traefik.yml et dynamic.yml depuis configs/traefik/
|
||||||
|
# Adapter les emails Let's Encrypt
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vérification** :
|
||||||
|
```bash
|
||||||
|
docker logs traefik
|
||||||
|
# Doit afficher "Server is up"
|
||||||
|
```
|
||||||
|
|
||||||
|
Visiter `https://traefik.seizeconsulting.com` → doit afficher la page de login basic auth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Étape 5 — Installation Zitadel
|
||||||
|
|
||||||
|
⏱️ **45 min**
|
||||||
|
|
||||||
|
Voir [docs/03-zitadel-setup.md](./docs/03-zitadel-setup.md) pour les détails complets.
|
||||||
|
|
||||||
|
### Résumé des points clés
|
||||||
|
|
||||||
|
1. Créer `~/docker/zitadel/` avec son `docker-compose.yml` et `.env`
|
||||||
|
2. **Important** : Zitadel a son propre Traefik intégré + Postgres dédié
|
||||||
|
3. Lancer : `docker compose up -d`
|
||||||
|
4. Aller sur `https://sso.seizeconsulting.com/ui/console`
|
||||||
|
5. Créer le user **zitadel-admin** initial via console
|
||||||
|
6. Configurer les politiques :
|
||||||
|
- Password complexity (14 chars, maj+min+chiffres+symboles)
|
||||||
|
- MFA obligatoire
|
||||||
|
- Lockout après 5 échecs
|
||||||
|
|
||||||
|
⚠️ **Sauvegarder le mdp zitadel-admin** immédiatement dans le gestionnaire de mots de passe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ Étape 6 — Installation Postgres (pour Gitea, n8n)
|
||||||
|
|
||||||
|
⏱️ **10 min**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/docker/postgres/data
|
||||||
|
cd ~/docker/postgres
|
||||||
|
# Copier docker-compose.yml et .env.example depuis configs/postgres/
|
||||||
|
# Renommer .env.example en .env et générer un mdp fort
|
||||||
|
cat > .env << EOF
|
||||||
|
POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d '+/=')
|
||||||
|
EOF
|
||||||
|
chmod 600 .env
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Vérification
|
||||||
|
docker exec -it postgres psql -U postgres -c "SELECT version();"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Créer les databases pour Gitea et n8n
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it postgres psql -U postgres << EOF
|
||||||
|
CREATE USER gitea WITH PASSWORD 'PWD_GITEA';
|
||||||
|
CREATE DATABASE gitea OWNER gitea;
|
||||||
|
CREATE USER n8n WITH PASSWORD 'PWD_N8N';
|
||||||
|
CREATE DATABASE n8n OWNER n8n;
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ Remplacer `PWD_GITEA` et `PWD_N8N` par des mdp générés (`openssl rand -base64 24`) et les stocker dans le gestionnaire.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐙 Étape 7 — Installation Gitea avec SSO Zitadel
|
||||||
|
|
||||||
|
⏱️ **45 min**
|
||||||
|
|
||||||
|
Voir [docs/05-applications.md](./docs/05-applications.md) section Gitea.
|
||||||
|
|
||||||
|
### Étapes principales
|
||||||
|
|
||||||
|
1. Créer `~/docker/gitea/` avec son `docker-compose.yml` et `.env`
|
||||||
|
2. Démarrer Gitea : `docker compose up -d`
|
||||||
|
3. Aller sur `https://git.seizeconsulting.com` → faire le setup initial
|
||||||
|
4. **Dans Zitadel** : créer une application "Gitea" dans le project "Infra Seize"
|
||||||
|
5. Configurer dans Gitea : Settings → Authentication Sources → Add OAuth2
|
||||||
|
6. **Activer auto-registration** dans `app.ini` :
|
||||||
|
```ini
|
||||||
|
[oauth2_client]
|
||||||
|
ENABLE_AUTO_REGISTRATION = true
|
||||||
|
USERNAME = preferred_username
|
||||||
|
```
|
||||||
|
7. Restart Gitea : `docker compose restart`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Étape 8 — Pattern OAuth2-Proxy pour les autres outils
|
||||||
|
|
||||||
|
⏱️ **2h** (à répéter pour chaque outil)
|
||||||
|
|
||||||
|
Voir [docs/04-oauth2-proxy-pattern.md](./docs/04-oauth2-proxy-pattern.md) pour le pattern complet.
|
||||||
|
|
||||||
|
### Outils à protéger via OAuth2-Proxy
|
||||||
|
|
||||||
|
- n8n
|
||||||
|
- Portainer
|
||||||
|
- Code Server
|
||||||
|
- Uptime Kuma
|
||||||
|
- Intranet
|
||||||
|
|
||||||
|
### Étapes par outil (exemple : n8n)
|
||||||
|
|
||||||
|
1. **Déployer l'outil** (sans SSO) :
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/docker/n8n
|
||||||
|
# Copier docker-compose.yml depuis configs/n8n/
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Créer l'application dans Zitadel** :
|
||||||
|
- Console Zitadel → Projects → Infra Seize → Applications → New
|
||||||
|
- Type : Web
|
||||||
|
- Authentication Method : Basic
|
||||||
|
- Grant Types : Authorization Code
|
||||||
|
- Redirect URL : `https://n8n.seizeconsulting.com/oauth2/callback`
|
||||||
|
- Token Type : JWT
|
||||||
|
- Cocher : User roles inside ID Token, User profile info, Add user roles to access token
|
||||||
|
|
||||||
|
3. **Récupérer Client ID + Client Secret**
|
||||||
|
|
||||||
|
4. **Déployer OAuth2-Proxy dédié** :
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/docker/oauth2-proxy-n8n
|
||||||
|
# Copier docker-compose.yml et .env.example depuis configs/oauth2-proxy/
|
||||||
|
# Adapter les valeurs (host, client_id, client_secret, upstream)
|
||||||
|
cd ~/docker/oauth2-proxy-n8n
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Tester** : aller sur `https://n8n.seizeconsulting.com` → doit rediriger vers Zitadel login.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏠 Étape 9 — Installation Intranet avec RBAC
|
||||||
|
|
||||||
|
⏱️ **30 min**
|
||||||
|
|
||||||
|
Voir [docs/07-rbac-intranet.md](./docs/07-rbac-intranet.md) pour les détails.
|
||||||
|
|
||||||
|
### Étapes
|
||||||
|
|
||||||
|
1. **Créer le dossier intranet** :
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/docker/intranet
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Copier les fichiers** depuis `configs/intranet/` :
|
||||||
|
- `docker-compose.yml`
|
||||||
|
- `nginx-rbac.conf`
|
||||||
|
- `index.html.template` (à personnaliser)
|
||||||
|
- `logo.png` (le logo Seize)
|
||||||
|
|
||||||
|
3. **Créer le `roles-mapping.json`** :
|
||||||
|
Il sera généré plus tard par le script `generate-roles-mapping.py` (cf. Étape 10).
|
||||||
|
|
||||||
|
En attendant, créer un mapping vide :
|
||||||
|
```bash
|
||||||
|
echo '{}' > roles-mapping.json
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Déployer OAuth2-Proxy pour l'intranet** : voir Étape 8 mais avec :
|
||||||
|
- Host : `intranet.seizeconsulting.com`
|
||||||
|
- Upstream : `http://intranet:80`
|
||||||
|
|
||||||
|
5. **Démarrer l'intranet** :
|
||||||
|
```bash
|
||||||
|
cd ~/docker/intranet
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Tester** : aller sur `https://intranet.seizeconsulting.com` → login Zitadel → voir les cartes (toutes affichées tant que roles-mapping.json est vide).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 Étape 10 — Import des users depuis Furious Squad
|
||||||
|
|
||||||
|
⏱️ **1h**
|
||||||
|
|
||||||
|
Voir [docs/06-import-furious.md](./docs/06-import-furious.md) pour la procédure complète.
|
||||||
|
|
||||||
|
### Étapes principales
|
||||||
|
|
||||||
|
1. **Créer un compte API Furious** :
|
||||||
|
- Connexion Furious → Configuration → Générer un nouvel utilisateur d'API
|
||||||
|
- Cocher les permissions : `User > Search` uniquement
|
||||||
|
- Sauvegarder identifiant + mdp dans le gestionnaire
|
||||||
|
|
||||||
|
2. **Créer le service account Zitadel** :
|
||||||
|
- Console Zitadel → Users → New → Service user
|
||||||
|
- Nom : `furious-import-service`
|
||||||
|
- Access Token Type : JWT
|
||||||
|
- Dans Default settings → IAM Members → ajouter ce service user avec rôle `IAM User Manager`
|
||||||
|
- Générer une clé : Keys → New → Type JSON → télécharger immédiatement
|
||||||
|
|
||||||
|
3. **Préparer l'environnement Python** :
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/furious-to-zitadel
|
||||||
|
cd ~/furious-to-zitadel
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install requests pyjwt cryptography
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Copier les scripts** depuis `scripts/furious-to-zitadel/` du repo.
|
||||||
|
|
||||||
|
5. **Créer le `.env`** :
|
||||||
|
```env
|
||||||
|
FURIOUS_USERNAME=<credentials créés à l'étape 1>
|
||||||
|
FURIOUS_PASSWORD=<credentials créés à l'étape 1>
|
||||||
|
FURIOUS_API_URL=https://seize-consulting.furious-squad.com/api/v2
|
||||||
|
```
|
||||||
|
`chmod 600 .env`
|
||||||
|
|
||||||
|
6. **Placer la clé JSON Zitadel** :
|
||||||
|
- Transférer la clé depuis le PC vers le serveur via SCP
|
||||||
|
- `chmod 600 zitadelfuriousimportkey.json`
|
||||||
|
|
||||||
|
7. **Lancer l'import** :
|
||||||
|
```bash
|
||||||
|
# Étape A : Lire les users depuis Furious
|
||||||
|
./read-all-users.sh
|
||||||
|
# → Génère furious-users-raw.json
|
||||||
|
|
||||||
|
# Étape B : Dry-run (simulation, aucune écriture)
|
||||||
|
python3 import-furious-to-zitadel.py --mode dry-run
|
||||||
|
|
||||||
|
# Étape C : Test sur 3 users
|
||||||
|
python3 import-furious-to-zitadel.py --mode test
|
||||||
|
|
||||||
|
# Étape D : Vérifier dans Zitadel UI que les 3 users sont créés avec metadata
|
||||||
|
|
||||||
|
# Étape E : Import production (les 85 restants)
|
||||||
|
python3 import-furious-to-zitadel.py --mode prod
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **Gérer les doublons** (si users existaient déjà dans Zitadel) :
|
||||||
|
```bash
|
||||||
|
# Adapter DUPLICATES dans merge-duplicates.py selon ton cas
|
||||||
|
python3 merge-duplicates.py
|
||||||
|
```
|
||||||
|
|
||||||
|
9. **Vérifier le CSV** :
|
||||||
|
```bash
|
||||||
|
wc -l users-credentials.csv
|
||||||
|
# Doit être : 1 header + N users
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 Étape 11 — Configuration des rôles RBAC
|
||||||
|
|
||||||
|
⏱️ **30 min**
|
||||||
|
|
||||||
|
Voir [docs/07-rbac-intranet.md](./docs/07-rbac-intranet.md) pour les détails.
|
||||||
|
|
||||||
|
### Étapes
|
||||||
|
|
||||||
|
1. **Créer les 8 rôles dans Zitadel** :
|
||||||
|
- Console Zitadel → Projects → Infra Seize → Roles → New
|
||||||
|
- Créer un par un :
|
||||||
|
- direction / Direction / hierarchie
|
||||||
|
- manager / Manager / hierarchie
|
||||||
|
- expert / Expert / hierarchie
|
||||||
|
- consultant_senior / Consultant Senior / hierarchie
|
||||||
|
- consultant_junior / Consultant Junior / hierarchie
|
||||||
|
- alternant / Alternant / hierarchie
|
||||||
|
- stagiaire / Stagiaire / hierarchie
|
||||||
|
- support / Support / hierarchie
|
||||||
|
|
||||||
|
2. **Activer "Return user roles during authentication"** :
|
||||||
|
- Console Zitadel → Projects → Infra Seize → General
|
||||||
|
- Cocher "Return user roles during authentication"
|
||||||
|
- Save
|
||||||
|
|
||||||
|
3. **Activer "User roles inside ID Token"** sur chaque application :
|
||||||
|
- Pour chaque app (Intranet, n8n, etc.) : Token Settings
|
||||||
|
- Auth Token Type : JWT
|
||||||
|
- Cocher : User roles inside ID Token, Include user's profile info, Add user roles to the access token
|
||||||
|
|
||||||
|
4. **Modifier OAuth2-Proxy** pour demander le scope des rôles :
|
||||||
|
- Pour chaque OAuth2-Proxy : éditer `.env`
|
||||||
|
- Modifier : `OAUTH2_PROXY_SCOPE=openid email profile urn:zitadel:iam:org:project:roles`
|
||||||
|
- Restart : `docker compose down && docker compose up -d`
|
||||||
|
|
||||||
|
5. **Lancer les scripts d'attribution** :
|
||||||
|
```bash
|
||||||
|
cd ~/furious-to-zitadel
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Générer le mapping email → rôle (pour l'intranet)
|
||||||
|
python3 generate-roles-mapping.py
|
||||||
|
|
||||||
|
# Attribuer les rôles dans Zitadel (87 users)
|
||||||
|
python3 assign-roles.py
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Copier le mapping vers l'intranet** :
|
||||||
|
```bash
|
||||||
|
cp roles-mapping.json ~/docker/intranet/
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Restart de l'intranet** :
|
||||||
|
```bash
|
||||||
|
cd ~/docker/intranet
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **Test final** :
|
||||||
|
- Login en incognito avec un user (ex: consultant_junior)
|
||||||
|
- Doit voir uniquement les cartes autorisées (Gitea + Code Server par défaut)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Étape 12 — Validation finale
|
||||||
|
|
||||||
|
⏱️ **30 min**
|
||||||
|
|
||||||
|
### Checklist de validation
|
||||||
|
|
||||||
|
- [ ] Tous les conteneurs `Up` : `docker ps`
|
||||||
|
- [ ] Tous les sous-domaines répondent en HTTPS
|
||||||
|
- [ ] Login Zitadel fonctionne avec MFA
|
||||||
|
- [ ] Login intranet fonctionne et filtre les cartes
|
||||||
|
- [ ] Login Gitea fonctionne avec auto-registration
|
||||||
|
- [ ] Login n8n / Portainer / Code Server / Uptime Kuma fonctionne
|
||||||
|
- [ ] Le service account Furious peut lire l'API Furious
|
||||||
|
- [ ] Les scripts Python sont opérationnels
|
||||||
|
- [ ] Les fichiers sensibles ont `chmod 600`
|
||||||
|
- [ ] Les `.env` ne sont pas committés sur Git
|
||||||
|
- [ ] Backup automatique en place (à mettre en place — voir OPERATIONS.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 Points d'attention pendant l'install
|
||||||
|
|
||||||
|
### Ne pas oublier
|
||||||
|
- ✅ Sauvegarder TOUS les secrets dans le gestionnaire de mdp
|
||||||
|
- ✅ Configurer le firewall (ufw ou iptables) pour limiter aux ports 22/80/443
|
||||||
|
- ✅ Tester chaque étape avant de passer à la suivante
|
||||||
|
- ✅ Documenter les écarts par rapport à cette procédure si tu en fais
|
||||||
|
|
||||||
|
### Pièges classiques
|
||||||
|
- ❌ Oublier d'ajouter le record DNS avant de tester le HTTPS (Let's Encrypt échoue)
|
||||||
|
- ❌ Lancer `docker compose up -d` sans avoir créé le `.env` (erreurs cryptiques)
|
||||||
|
- ❌ Donner IAM_OWNER au service account (utiliser IAM_USER_MANAGER)
|
||||||
|
- ❌ Coller un secret dans une conversation IA (gestionnaire de mdp uniquement)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Pour aller plus loin
|
||||||
|
|
||||||
|
Une fois l'install terminée, consulter :
|
||||||
|
- [OPERATIONS.md](./OPERATIONS.md) pour la maintenance quotidienne
|
||||||
|
- [docs/09-troubleshooting.md](./docs/09-troubleshooting.md) pour les problèmes fréquents
|
||||||
|
- [HANDOVER.md](./HANDOVER.md) pour les actions à mener
|
||||||
512
OPERATIONS.md
Normal file
512
OPERATIONS.md
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
# OPERATIONS — Maintenance et opérations courantes
|
||||||
|
|
||||||
|
> Procédures pour maintenir, dépanner, et faire évoluer l'infrastructure Seize.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Vérifications quotidiennes (5 min/jour)
|
||||||
|
|
||||||
|
### Healthcheck rapide
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tous les conteneurs sont-ils Up ?
|
||||||
|
sudo docker ps --format "table {{.Names}}\t{{.Status}}" | grep -v "Up"
|
||||||
|
# → Aucune ligne sauf le header = tout va bien
|
||||||
|
|
||||||
|
# Tous les sous-domaines répondent-ils en HTTPS ?
|
||||||
|
for domain in sso intranet git n8n portainer code status; do
|
||||||
|
echo -n "$domain : "
|
||||||
|
curl -sk -o /dev/null -w "%{http_code}\n" "https://$domain.seizeconsulting.com"
|
||||||
|
done
|
||||||
|
# → 200 ou 302 sur chaque ligne = OK
|
||||||
|
# → 500, 502, 503 = problème
|
||||||
|
|
||||||
|
# Espace disque restant
|
||||||
|
df -h /
|
||||||
|
# → Surveiller si > 80% utilisé
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs récents
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Logs Traefik (erreurs uniquement)
|
||||||
|
sudo docker logs --tail 100 traefik 2>&1 | grep -i "error\|warn"
|
||||||
|
|
||||||
|
# Logs Zitadel (erreurs)
|
||||||
|
sudo docker logs --tail 100 zitadel-zitadel-api-1 2>&1 | grep -i "error\|level=err"
|
||||||
|
|
||||||
|
# Logs OAuth2-Proxy intranet (erreurs récentes)
|
||||||
|
sudo docker logs --tail 100 oauth2-proxy 2>&1 | grep -i "error"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Backups
|
||||||
|
|
||||||
|
### ⚠️ État actuel : AUCUN backup automatique
|
||||||
|
|
||||||
|
À mettre en place en priorité.
|
||||||
|
|
||||||
|
### Procédure backup manuel (à exécuter avant grosse modif)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Créer un dossier de backup daté
|
||||||
|
BACKUP_DIR=~/backups/$(date +%Y%m%d-%H%M%S)
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
|
||||||
|
# Backup Postgres (Gitea, n8n)
|
||||||
|
docker exec postgres pg_dumpall -U postgres > $BACKUP_DIR/postgres.sql
|
||||||
|
|
||||||
|
# Backup Postgres Zitadel (sa propre instance)
|
||||||
|
docker exec zitadel-postgres-1 pg_dumpall -U postgres > $BACKUP_DIR/postgres-zitadel.sql
|
||||||
|
|
||||||
|
# Backup volumes Docker (Gitea data, etc.)
|
||||||
|
sudo tar czf $BACKUP_DIR/gitea-data.tar.gz -C ~/docker/gitea data/
|
||||||
|
sudo tar czf $BACKUP_DIR/uptime-data.tar.gz -C ~/docker/uptime-kuma data/
|
||||||
|
sudo tar czf $BACKUP_DIR/portainer-data.tar.gz -C ~/docker/portainer data/
|
||||||
|
|
||||||
|
# Backup configs (.env, docker-compose.yml)
|
||||||
|
tar czf $BACKUP_DIR/configs.tar.gz -C ~/docker .
|
||||||
|
|
||||||
|
# Backup scripts furious-to-zitadel
|
||||||
|
tar czf $BACKUP_DIR/furious-scripts.tar.gz -C ~/ furious-to-zitadel/
|
||||||
|
|
||||||
|
echo "✅ Backup terminé dans $BACKUP_DIR"
|
||||||
|
ls -lh $BACKUP_DIR
|
||||||
|
```
|
||||||
|
|
||||||
|
### Procédure backup automatique (à mettre en place)
|
||||||
|
|
||||||
|
Voir [docs/09-troubleshooting.md](./docs/09-troubleshooting.md) section Backup automatisé.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Mises à jour
|
||||||
|
|
||||||
|
### Mise à jour d'un service Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/docker/<service>
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérifier qu'une mise à jour ne casse rien
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Backup avant
|
||||||
|
~/backup-manual.sh # script à créer si pas existant
|
||||||
|
|
||||||
|
# 2. Pull la nouvelle image
|
||||||
|
docker compose pull
|
||||||
|
|
||||||
|
# 3. Restart
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 4. Vérifier les logs
|
||||||
|
docker compose logs -f
|
||||||
|
# Ctrl+C pour quitter
|
||||||
|
|
||||||
|
# 5. Tester le service (login, fonctionnalités principales)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mises à jour critiques à surveiller
|
||||||
|
|
||||||
|
| Service | Risque | Procédure |
|
||||||
|
|---|---|---|
|
||||||
|
| Traefik | Changement breaking config | Lire le CHANGELOG avant |
|
||||||
|
| Zitadel | Schema DB peut changer | Backup obligatoire avant |
|
||||||
|
| Postgres | Migration majeure | Backup + lire migration guide |
|
||||||
|
| OAuth2-Proxy | Changement de scopes/headers | Tester en dry-run |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ➕ Ajouter un nouvel outil
|
||||||
|
|
||||||
|
### Procédure standard (~1h)
|
||||||
|
|
||||||
|
#### 1. Ajouter le record DNS
|
||||||
|
|
||||||
|
Chez le registrar : créer record A `<outil>.seizeconsulting.com` → IP du serveur.
|
||||||
|
|
||||||
|
#### 2. Déployer l'outil (sans SSO)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/docker/<outil>
|
||||||
|
cd ~/docker/<outil>
|
||||||
|
|
||||||
|
# Créer docker-compose.yml
|
||||||
|
cat > docker-compose.yml << EOF
|
||||||
|
services:
|
||||||
|
<outil>:
|
||||||
|
image: <image>:<tag>
|
||||||
|
container_name: <outil>
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
# PAS de labels Traefik ici → c'est OAuth2-Proxy qui sera exposé
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Créer l'application dans Zitadel
|
||||||
|
|
||||||
|
Console Zitadel → Projects → Infra Seize → New application :
|
||||||
|
- Type : **Web**
|
||||||
|
- Authentication Method : **Basic**
|
||||||
|
- Grant Types : **Authorization Code**
|
||||||
|
- Redirect URL : `https://<outil>.seizeconsulting.com/oauth2/callback`
|
||||||
|
- Token Settings :
|
||||||
|
- 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 : ✅
|
||||||
|
|
||||||
|
Récupérer : **Client ID** + **Client Secret**.
|
||||||
|
|
||||||
|
#### 4. Déployer un OAuth2-Proxy dédié
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/docker/oauth2-proxy-<outil>
|
||||||
|
cd ~/docker/oauth2-proxy-<outil>
|
||||||
|
|
||||||
|
# docker-compose.yml (adapter depuis configs/oauth2-proxy/docker-compose.yml.template)
|
||||||
|
# .env (adapter depuis configs/oauth2-proxy/.env.example)
|
||||||
|
```
|
||||||
|
|
||||||
|
`.env` (variables à remplacer) :
|
||||||
|
```env
|
||||||
|
OAUTH2_PROXY_PROVIDER=oidc
|
||||||
|
OAUTH2_PROXY_CLIENT_ID=<de Zitadel>
|
||||||
|
OAUTH2_PROXY_CLIENT_SECRET=<de Zitadel>
|
||||||
|
OAUTH2_PROXY_COOKIE_SECRET=<32 chars random>
|
||||||
|
OAUTH2_PROXY_OIDC_ISSUER_URL=https://sso.seizeconsulting.com
|
||||||
|
OAUTH2_PROXY_REDIRECT_URL=https://<outil>.seizeconsulting.com/oauth2/callback
|
||||||
|
OAUTH2_PROXY_UPSTREAMS=http://<outil>:<port_interne>
|
||||||
|
OAUTH2_PROXY_SCOPE=openid email profile urn:zitadel:iam:org:project:roles
|
||||||
|
OAUTH2_PROXY_EMAIL_DOMAINS=*
|
||||||
|
OAUTH2_PROXY_COOKIE_DOMAINS=.seizeconsulting.com
|
||||||
|
OAUTH2_PROXY_COOKIE_SECURE=true
|
||||||
|
OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
|
||||||
|
OAUTH2_PROXY_REVERSE_PROXY=true
|
||||||
|
OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER=true
|
||||||
|
OAUTH2_PROXY_PASS_ACCESS_TOKEN=true
|
||||||
|
OAUTH2_PROXY_SET_AUTHORIZATION_HEADER=true
|
||||||
|
OAUTH2_PROXY_SET_XAUTHREQUEST=true
|
||||||
|
OAUTH2_PROXY_WHITELIST_DOMAINS=.seizeconsulting.com
|
||||||
|
OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true
|
||||||
|
```
|
||||||
|
|
||||||
|
`chmod 600 .env` + `docker compose up -d`.
|
||||||
|
|
||||||
|
#### 5. Ajouter une carte sur l'intranet
|
||||||
|
|
||||||
|
Éditer `~/docker/intranet/index.html` et ajouter une nouvelle carte dans la section `<a class="service-card">` :
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a class="service-card" data-app="<outil>" data-color="<color>" href="https://<outil>.seizeconsulting.com" target="_blank" rel="noopener">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon"><!-- SVG icon --></div>
|
||||||
|
<div class="card-arrow"><!-- SVG arrow --></div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-title"><Nom de l'outil></div>
|
||||||
|
<div class="card-desc">Description courte</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span class="card-url"><outil>.seizeconsulting.com</span>
|
||||||
|
<span class="card-tag">Catégorie</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Mettre à jour la matrice RBAC dans le JavaScript
|
||||||
|
|
||||||
|
Éditer le `<script>` RBAC dans `index.html` :
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ACCESS_MATRIX = {
|
||||||
|
"portainer": ["direction", "manager"],
|
||||||
|
"status": ["direction"],
|
||||||
|
// ...
|
||||||
|
"<outil>": ["direction", "manager"], // ← Ajouter ta nouvelle ligne
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7. Restart de l'intranet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/docker/intranet
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8. Tester
|
||||||
|
|
||||||
|
Se logger en incognito avec un user ayant accès → vérifier que la carte s'affiche et que le clic mène à l'outil avec SSO.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗑️ Retirer un outil
|
||||||
|
|
||||||
|
### Procédure inverse de l'ajout
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Arrêter l'outil et son OAuth2-Proxy
|
||||||
|
cd ~/docker/<outil>
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
cd ~/docker/oauth2-proxy-<outil>
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# 2. Archiver les configs (au cas où)
|
||||||
|
mv ~/docker/<outil> ~/docker/<outil>.archived-$(date +%Y%m%d)
|
||||||
|
mv ~/docker/oauth2-proxy-<outil> ~/docker/oauth2-proxy-<outil>.archived-$(date +%Y%m%d)
|
||||||
|
|
||||||
|
# 3. Supprimer l'application dans Zitadel
|
||||||
|
# Console → Projects → Infra Seize → Applications → <outil> → Delete
|
||||||
|
|
||||||
|
# 4. Retirer la carte sur l'intranet (HTML + matrice RBAC)
|
||||||
|
# Éditer ~/docker/intranet/index.html
|
||||||
|
# Restart : docker compose restart intranet
|
||||||
|
|
||||||
|
# 5. Optionnel : retirer le record DNS chez le registrar
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 Gestion des utilisateurs
|
||||||
|
|
||||||
|
### Ajouter un nouveau user manuellement
|
||||||
|
|
||||||
|
Quand un nouveau collaborateur arrive et n'est pas encore dans Furious :
|
||||||
|
|
||||||
|
```
|
||||||
|
Console Zitadel → Users → New → Human user :
|
||||||
|
- Email : prenom.nom@seizeconsulting.com
|
||||||
|
- Username : <trigramme> (ex: ABC)
|
||||||
|
- Prénom, Nom
|
||||||
|
- Cocher : Email verified, Password change required
|
||||||
|
- Generate password : oui (mdp temporaire 16 chars)
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Distribuer le mdp via Teams privé** au nouveau collaborateur.
|
||||||
|
|
||||||
|
### Désactiver un user (départ)
|
||||||
|
|
||||||
|
```
|
||||||
|
Console Zitadel → Users → <user> → Actions → Deactivate
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Ne PAS supprimer** le user : ses traces dans les outils (commits Gitea, workflows n8n, etc.) seraient orphelines. Désactiver suffit.
|
||||||
|
|
||||||
|
### Changer le rôle d'un user
|
||||||
|
|
||||||
|
#### Méthode UI (1 user)
|
||||||
|
|
||||||
|
```
|
||||||
|
Console Zitadel → Users → <user> → Authorizations → Edit roles
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Méthode script (plusieurs users)
|
||||||
|
|
||||||
|
1. Mettre à jour le mapping dans `assign-roles.py` (variable `JOB_TITLE_TO_ROLE`)
|
||||||
|
2. Lancer :
|
||||||
|
```bash
|
||||||
|
cd ~/furious-to-zitadel
|
||||||
|
source venv/bin/activate
|
||||||
|
python3 assign-roles.py
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ Le script **ajoute** les rôles, il ne **retire pas** les anciens. Pour retirer, utiliser l'UI ou un script custom.
|
||||||
|
|
||||||
|
### Synchronisation périodique avec Furious
|
||||||
|
|
||||||
|
Workflow recommandé (à mettre en place) :
|
||||||
|
|
||||||
|
1. **Tous les jours** : un cron exécute `read-all-users.sh` pour récupérer les users Furious
|
||||||
|
2. **Compare** avec les users Zitadel actuels
|
||||||
|
3. **Pour chaque diff** :
|
||||||
|
- Nouveau dans Furious → créer dans Zitadel
|
||||||
|
- Départ détecté dans Furious (`departure_date != "0000-00-00"`) → désactiver dans Zitadel
|
||||||
|
- Changement de `job_title` → réattribuer le rôle
|
||||||
|
|
||||||
|
⚠️ **Pas encore automatisé**. À mettre en place via n8n.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Rotation des secrets
|
||||||
|
|
||||||
|
### Quand rotater ?
|
||||||
|
|
||||||
|
- 🔴 **Immédiatement** : si secret exposé (dans un commit Git, conversation IA, etc.)
|
||||||
|
- 🟡 **Tous les 6 mois** : pratique de bonne hygiène
|
||||||
|
- 🟢 **Tous les ans** : pour les secrets très internes (postgres root, etc.)
|
||||||
|
|
||||||
|
### Procédure pour un secret OAuth2-Proxy (Client Secret Zitadel)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Aller dans Zitadel : Projects → Infra Seize → Applications → <outil>
|
||||||
|
# 2. Cliquer "Generate Client Secret" → copier le nouveau secret
|
||||||
|
# 3. Mettre à jour le .env d'OAuth2-Proxy
|
||||||
|
|
||||||
|
cd ~/docker/oauth2-proxy-<outil>
|
||||||
|
# Éditer .env, remplacer OAUTH2_PROXY_CLIENT_SECRET
|
||||||
|
# Sauvegarder dans le gestionnaire de mots de passe
|
||||||
|
|
||||||
|
# 4. Restart OAuth2-Proxy
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Procédure pour la clé service account Zitadel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Console Zitadel → Users → furious-import-service → Keys
|
||||||
|
# 2. Supprimer l'ancienne clé (et l'invalider)
|
||||||
|
# 3. Créer une nouvelle clé (Type : JSON)
|
||||||
|
# 4. Télécharger la nouvelle clé immédiatement
|
||||||
|
|
||||||
|
# 5. Remplacer sur le serveur
|
||||||
|
scp <nouvelle_cle>.json admin@<serveur>:~/furious-to-zitadel/zitadelfuriousimportkey.json
|
||||||
|
ssh admin@<serveur>
|
||||||
|
chmod 600 ~/furious-to-zitadel/zitadelfuriousimportkey.json
|
||||||
|
|
||||||
|
# 6. Tester les scripts
|
||||||
|
cd ~/furious-to-zitadel
|
||||||
|
source venv/bin/activate
|
||||||
|
python3 assign-roles.py # Doit fonctionner avec la nouvelle clé
|
||||||
|
```
|
||||||
|
|
||||||
|
### Procédure pour Postgres (Gitea/n8n)
|
||||||
|
|
||||||
|
⚠️ **À faire HORS heures de bureau** car les services s'arrêteront temporairement.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Générer un nouveau mdp
|
||||||
|
NEW_PWD=$(openssl rand -base64 32 | tr -d '+/=' | head -c 32)
|
||||||
|
echo "Nouveau mdp : $NEW_PWD"
|
||||||
|
# → Sauvegarder dans le gestionnaire de mots de passe
|
||||||
|
|
||||||
|
# 2. Changer le mdp dans Postgres
|
||||||
|
docker exec -it postgres psql -U postgres -c "ALTER USER gitea WITH PASSWORD '$NEW_PWD';"
|
||||||
|
|
||||||
|
# 3. Mettre à jour le .env de Gitea
|
||||||
|
nano ~/docker/gitea/.env
|
||||||
|
# Remplacer GITEA_DB_PASSWORD=$NEW_PWD
|
||||||
|
|
||||||
|
# 4. Restart Gitea
|
||||||
|
cd ~/docker/gitea
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 5. Tester Gitea
|
||||||
|
curl -sI https://git.seizeconsulting.com # Doit retourner 200 ou 302
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting commun
|
||||||
|
|
||||||
|
Voir [docs/09-troubleshooting.md](./docs/09-troubleshooting.md) pour une liste exhaustive.
|
||||||
|
|
||||||
|
### "Mon SSO ne fonctionne plus, l'user est redirigé en boucle"
|
||||||
|
|
||||||
|
→ Probablement le bug Traefik authRequestHeaders=Cookie. Vérifier que `~/docker/traefik/dynamic.yml` contient bien `Cookie` dans `authRequestHeaders` du middleware ForwardAuth.
|
||||||
|
|
||||||
|
### "Le certificat Let's Encrypt a expiré"
|
||||||
|
|
||||||
|
→ Traefik renouvelle automatiquement. Si pas le cas :
|
||||||
|
```bash
|
||||||
|
# Vérifier les logs Traefik
|
||||||
|
sudo docker logs traefik 2>&1 | grep -i "letsencrypt\|certificate"
|
||||||
|
|
||||||
|
# Forcer le renouvellement
|
||||||
|
cd ~/docker/traefik
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Le script Python d'import échoue avec un 401 Unauthorized"
|
||||||
|
|
||||||
|
→ Le token Zitadel a expiré (1h) OU les droits du service account ont été révoqués.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier que la clé JSON est valide
|
||||||
|
python3 -c "import json; data=json.load(open('zitadelfuriousimportkey.json')); print('UserId:', data.get('userId'), 'KeyId:', data.get('keyId'))"
|
||||||
|
|
||||||
|
# Vérifier dans Zitadel Console que le service account a toujours IAM_USER_MANAGER
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Un user ne peut pas se logger après avoir été créé"
|
||||||
|
|
||||||
|
→ Vérifier :
|
||||||
|
1. `Email verified` cocher dans son profil Zitadel
|
||||||
|
2. Si "Only authorized users can authenticate" est activé sur le project → il doit avoir un rôle
|
||||||
|
3. MFA pas mal configuré → faire un reset MFA si bloqué
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Monitoring
|
||||||
|
|
||||||
|
### Métriques à surveiller
|
||||||
|
|
||||||
|
| Métrique | Outil | Seuil d'alerte |
|
||||||
|
|---|---|---|
|
||||||
|
| Disponibilité des sous-domaines | Uptime Kuma | Down > 5 min |
|
||||||
|
| Taux d'erreur HTTP (5xx) | Logs Traefik | > 1% sur 5 min |
|
||||||
|
| Espace disque | `df -h` | > 80% |
|
||||||
|
| RAM utilisée | `free -h` | > 90% |
|
||||||
|
| CPU load | `uptime` | > 4 (sur 4 vCPU) |
|
||||||
|
| Tentatives de login échouées | Logs Zitadel | > 100/heure (potentielle attaque) |
|
||||||
|
|
||||||
|
### Mettre en place le monitoring
|
||||||
|
|
||||||
|
Uptime Kuma est déjà déployé. Ajouter des moniteurs pour :
|
||||||
|
- Chaque sous-domaine (HTTP check)
|
||||||
|
- Le serveur lui-même (ping)
|
||||||
|
- L'API Furious (HTTP check)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Escalation
|
||||||
|
|
||||||
|
### Quand appeler Philippe ?
|
||||||
|
|
||||||
|
- 🔴 Service critique down (Zitadel, Gitea, intranet) > 1h
|
||||||
|
- 🔴 Faille sécurité avérée
|
||||||
|
- 🔴 Perte de données
|
||||||
|
|
||||||
|
### Quand appeler le support Scaleway ?
|
||||||
|
|
||||||
|
- 🔴 Serveur inaccessible (panne hardware)
|
||||||
|
- 🔴 Problème réseau Scaleway
|
||||||
|
|
||||||
|
### Quand appeler le support Furious ?
|
||||||
|
|
||||||
|
- 🟡 API Furious down ou très lente
|
||||||
|
- 🟡 Permissions API qui ne fonctionnent pas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Checklist hebdomadaire (5-10 min)
|
||||||
|
|
||||||
|
- [ ] Vérifier tous les conteneurs Up
|
||||||
|
- [ ] Vérifier l'espace disque < 80%
|
||||||
|
- [ ] Lire les alertes Uptime Kuma
|
||||||
|
- [ ] Vérifier les logs Traefik pour erreurs récurrentes
|
||||||
|
- [ ] Backup manuel des données critiques (si pas encore automatisé)
|
||||||
|
- [ ] Vérifier les certificats Let's Encrypt (expire dans > 7 jours)
|
||||||
|
|
||||||
|
## 📝 Checklist mensuelle (30 min)
|
||||||
|
|
||||||
|
- [ ] Faire un Docker prune des images inutilisées : `docker image prune -a`
|
||||||
|
- [ ] Vérifier les mises à jour disponibles : `apt list --upgradable`
|
||||||
|
- [ ] Auditer les users Zitadel (départs non synchronisés ?)
|
||||||
|
- [ ] Vérifier que les scripts Python fonctionnent toujours
|
||||||
|
- [ ] Lire les logs sécurité (tentatives de login échouées, etc.)
|
||||||
149
README.md
Normal file
149
README.md
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
# Seize Consulting — Infrastructure SSO & RBAC
|
||||||
|
|
||||||
|
> Documentation complète de l'infrastructure auto-hébergée de Seize Consulting.
|
||||||
|
> Stack Docker sur Scaleway, SSO Zitadel, RBAC sur intranet, import automatisé depuis Furious Squad.
|
||||||
|
|
||||||
|
**Dernière mise à jour :** 5 juin 2026
|
||||||
|
**Mainteneur sortant :** Hedi Kalboussi (HKA)
|
||||||
|
**Repère :** Jour 7 — Import 88 users + RBAC opérationnel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Démarrage rapide pour le nouveau mainteneur
|
||||||
|
|
||||||
|
Si tu reprends ce projet, **lis dans cet ordre** :
|
||||||
|
|
||||||
|
1. **[HANDOVER.md](./HANDOVER.md)** — Ce qui marche, ce qui reste à faire, alertes, secrets exposés
|
||||||
|
2. **[ARCHITECTURE.md](./ARCHITECTURE.md)** — Comprendre comment tout s'imbrique
|
||||||
|
3. **[DECISIONS.md](./DECISIONS.md)** — Pourquoi tel choix plutôt qu'un autre
|
||||||
|
4. **[OPERATIONS.md](./OPERATIONS.md)** — Comment maintenir, dépanner, ajouter un outil
|
||||||
|
5. **[INSTALL.md](./INSTALL.md)** — Comment redéployer from scratch sur un nouveau serveur
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 État actuel de l'infrastructure (au 5 juin 2026)
|
||||||
|
|
||||||
|
### Serveur
|
||||||
|
- **Provider :** Scaleway
|
||||||
|
- **OS :** Debian 13 (Trixie)
|
||||||
|
- **Docker :** 29.5.1
|
||||||
|
- **IP publique :** 151.115.148.243 (à ne pas exposer dans la doc publique)
|
||||||
|
- **Domaine principal :** `seizeconsulting.com`
|
||||||
|
|
||||||
|
### Services en production
|
||||||
|
|
||||||
|
| Service | URL | Rôle | SSO |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Traefik | `traefik.seizeconsulting.com` | Reverse proxy + TLS | ✅ |
|
||||||
|
| Zitadel | `sso.seizeconsulting.com` | Identity Provider (IDP) | N/A |
|
||||||
|
| Intranet | `intranet.seizeconsulting.com` | Page d'accueil + RBAC | ✅ |
|
||||||
|
| Gitea | `git.seizeconsulting.com` | Forge Git | ✅ auto-registration |
|
||||||
|
| n8n | `n8n.seizeconsulting.com` | Workflows | ✅ |
|
||||||
|
| Portainer | `portainer.seizeconsulting.com` | Gestion Docker | ✅ |
|
||||||
|
| Code Server | `code.seizeconsulting.com` | IDE web | ✅ |
|
||||||
|
| Uptime Kuma | `status.seizeconsulting.com` | Monitoring | ✅ |
|
||||||
|
|
||||||
|
### Utilisateurs Zitadel
|
||||||
|
- **88 collaborateurs Seize** importés depuis Furious Squad (jour 7)
|
||||||
|
- **1 admin technique** : `zitadel-admin`
|
||||||
|
- **1 service account** : `furious-import-service` (IAM_USER_MANAGER)
|
||||||
|
- **1 client technique** : `login-client` (auto-créé par Zitadel)
|
||||||
|
|
||||||
|
### RBAC (Approche A : filtrage côté intranet)
|
||||||
|
- **8 rôles** dans le project Zitadel "Infra Seize" :
|
||||||
|
`direction`, `manager`, `expert`, `consultant_senior`, `consultant_junior`,
|
||||||
|
`alternant`, `stagiaire`, `support`
|
||||||
|
- **Filtrage** : l'intranet HTML cache/affiche les cartes selon le rôle de l'user (lu via fichier JSON statique `roles-mapping.json`)
|
||||||
|
- **Limitation acceptée** : un user qui connaît l'URL directe d'un outil bypass le filtre
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ Structure du repo
|
||||||
|
|
||||||
|
```
|
||||||
|
seize-infra-doc/
|
||||||
|
├── README.md # ← Tu es ici
|
||||||
|
├── HANDOVER.md # 🚨 À LIRE EN PRIORITÉ
|
||||||
|
├── ARCHITECTURE.md # Vue d'ensemble technique
|
||||||
|
├── DECISIONS.md # Choix architecturaux et anti-patterns
|
||||||
|
├── INSTALL.md # Procédure d'install from scratch
|
||||||
|
├── OPERATIONS.md # Maintenance, dépannage, ajouts
|
||||||
|
├── install.sh # Script d'installation modulaire
|
||||||
|
├── .env.example # Template variables d'environnement
|
||||||
|
│
|
||||||
|
├── docs/ # Documentation thématique détaillée
|
||||||
|
│ ├── 01-dns-prerequis.md
|
||||||
|
│ ├── 02-traefik-setup.md
|
||||||
|
│ ├── 03-zitadel-setup.md
|
||||||
|
│ ├── 04-oauth2-proxy-pattern.md
|
||||||
|
│ ├── 05-applications.md
|
||||||
|
│ ├── 06-import-furious.md
|
||||||
|
│ ├── 07-rbac-intranet.md
|
||||||
|
│ ├── 08-secrets-management.md
|
||||||
|
│ └── 09-troubleshooting.md
|
||||||
|
│
|
||||||
|
├── configs/ # Templates docker-compose anonymisés
|
||||||
|
│ ├── traefik/
|
||||||
|
│ ├── zitadel/
|
||||||
|
│ ├── intranet/
|
||||||
|
│ ├── oauth2-proxy/
|
||||||
|
│ ├── gitea/
|
||||||
|
│ ├── n8n/
|
||||||
|
│ ├── portainer/
|
||||||
|
│ ├── code-server/
|
||||||
|
│ ├── uptime-kuma/
|
||||||
|
│ └── postgres/
|
||||||
|
│
|
||||||
|
├── scripts/ # Scripts opérationnels
|
||||||
|
│ ├── furious-to-zitadel/ # Scripts Python import users
|
||||||
|
│ └── intranet/ # Template index.html avec RBAC
|
||||||
|
│
|
||||||
|
└── matrices/ # Matrices RBAC et mappings
|
||||||
|
├── rbac-matrix.md
|
||||||
|
└── job-title-to-role.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Avertissements importants
|
||||||
|
|
||||||
|
1. **Authentik** tourne encore sur le serveur mais **n'est plus utilisé** (migration faite vers Zitadel). À supprimer pour libérer ressources.
|
||||||
|
2. **Redis** tourne encore — vérifier s'il est utilisé par autre chose avant suppression.
|
||||||
|
3. **Secrets exposés dans des conversations IA** : voir [HANDOVER.md](./HANDOVER.md) pour la liste des rotations à faire.
|
||||||
|
4. **SMTP non configuré dans Zitadel** : les emails de bienvenue/reset password ne partent pas. Distribution des mdp temporaires se fait manuellement via Teams.
|
||||||
|
5. **Migration outils gratuits** vs **licences pro** : décision business reportée — voir [DECISIONS.md](./DECISIONS.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
- **Mainteneur sortant** : Hedi Kalboussi (`HKA`)
|
||||||
|
- **Sponsor projet** : Philippe Spoljaric (`PSP`, Admin Infra)
|
||||||
|
- **CEO** : Pierre-Yves Tachon (`PYT`)
|
||||||
|
|
||||||
|
Pour toute question, contacter d'abord Hedi pour passation, puis Philippe pour décisions architecturales.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📜 Historique du projet
|
||||||
|
|
||||||
|
| Jour | Date | Étape majeure |
|
||||||
|
|---|---|---|
|
||||||
|
| J1 | 20 mai 2026 | Provisioning Scaleway + Docker + premiers tests |
|
||||||
|
| J2 | 21 mai 2026 | Première tentative SSO via Authentik |
|
||||||
|
| J3 | 26-29 mai 2026 | Migration Zitadel, 7 apps SSO configurées |
|
||||||
|
| J4 | 30 mai 2026 | Gitea auto-registration, fix bug Traefik authRequestHeaders |
|
||||||
|
| J5 | 1-2 juin 2026 | Analyse Furious API, plan migration outils gratuits |
|
||||||
|
| J6 | 3-4 juin 2026 | Préparation import, briefings Philippe |
|
||||||
|
| **J7** | **5 juin 2026** | **Import 88 users + RBAC opérationnel** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Liens utiles
|
||||||
|
|
||||||
|
- Console Zitadel : https://sso.seizeconsulting.com/ui/console
|
||||||
|
- Dashboard Traefik : https://traefik.seizeconsulting.com (direction only)
|
||||||
|
- Gitea : https://git.seizeconsulting.com
|
||||||
|
- Documentation Zitadel : https://zitadel.com/docs
|
||||||
|
- Documentation OAuth2-Proxy : https://oauth2-proxy.github.io/oauth2-proxy/
|
||||||
|
- Documentation Traefik : https://doc.traefik.io/traefik/
|
||||||
22
configs/code-server/docker-compose.yml.template
Normal file
22
configs/code-server/docker-compose.yml.template
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
code-server:
|
||||||
|
image: lscr.io/linuxserver/code-server:latest
|
||||||
|
container_name: code-server
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PUID: 1000
|
||||||
|
PGID: 1000
|
||||||
|
TZ: Europe/Paris
|
||||||
|
# Mdp fallback si SSO down (normalement on accède via SSO)
|
||||||
|
PASSWORD: ${CODE_PASSWORD}
|
||||||
|
SUDO_PASSWORD: ${CODE_PASSWORD}
|
||||||
|
DEFAULT_WORKSPACE: /config/workspace
|
||||||
|
volumes:
|
||||||
|
- ./config:/config
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
# ⚠️ Pas de labels Traefik : OAuth2-Proxy expose le host
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
48
configs/gitea/docker-compose.yml.template
Normal file
48
configs/gitea/docker-compose.yml.template
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
services:
|
||||||
|
gitea:
|
||||||
|
image: gitea/gitea:1.22
|
||||||
|
container_name: gitea
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
USER_UID: 1000
|
||||||
|
USER_GID: 1000
|
||||||
|
# Base de données
|
||||||
|
GITEA__database__DB_TYPE: postgres
|
||||||
|
GITEA__database__HOST: postgres:5432
|
||||||
|
GITEA__database__NAME: gitea
|
||||||
|
GITEA__database__USER: gitea
|
||||||
|
GITEA__database__PASSWD: ${GITEA_DB_PASSWORD}
|
||||||
|
# Serveur
|
||||||
|
GITEA__server__DOMAIN: git.seizeconsulting.com
|
||||||
|
GITEA__server__ROOT_URL: https://git.seizeconsulting.com/
|
||||||
|
GITEA__server__SSH_DOMAIN: git.seizeconsulting.com
|
||||||
|
# OIDC Auto-registration (rempli après config SSO)
|
||||||
|
GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION: "true"
|
||||||
|
GITEA__oauth2_client__USERNAME: preferred_username
|
||||||
|
GITEA__oauth2_client__ACCOUNT_LINKING: auto
|
||||||
|
# Désactiver le register manuel (SSO uniquement)
|
||||||
|
GITEA__service__DISABLE_REGISTRATION: "true"
|
||||||
|
GITEA__service__SHOW_REGISTRATION_BUTTON: "false"
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
ports:
|
||||||
|
- "22:22" # SSH Git
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.gitea.rule=Host(`git.seizeconsulting.com`)"
|
||||||
|
- "traefik.http.routers.gitea.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.gitea.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
|
backend:
|
||||||
|
external: true
|
||||||
18
configs/intranet/docker-compose.yml.template
Normal file
18
configs/intranet/docker-compose.yml.template
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
intranet:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: intranet
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./index.html:/usr/share/nginx/html/index.html:ro
|
||||||
|
- ./logo.png:/usr/share/nginx/html/logo.png:ro
|
||||||
|
- ./roles-mapping.json:/usr/share/nginx/html/roles-mapping.json:ro
|
||||||
|
- ./nginx-rbac.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
# ⚠️ Pas de labels Traefik ici : c'est OAuth2-Proxy qui est exposé sur intranet.<domain>
|
||||||
|
# L'intranet est en upstream d'OAuth2-Proxy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
48
configs/intranet/nginx-rbac.conf.template
Normal file
48
configs/intranet/nginx-rbac.conf.template
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# nginx-rbac.conf.template — Config nginx pour l'intranet avec RBAC
|
||||||
|
# ============================================================================
|
||||||
|
# Substitue __USER_EMAIL__ dans le HTML par la valeur du header X-Forwarded-Email
|
||||||
|
# (envoyé par OAuth2-Proxy après authentification Zitadel).
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Cache désactivé pour l'HTML (sinon le sub_filter peut servir un email obsolète)
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||||
|
|
||||||
|
# ⭐ Substitue __USER_EMAIL__ par la valeur du header HTTP
|
||||||
|
sub_filter '__USER_EMAIL__' '$http_x_forwarded_email';
|
||||||
|
sub_filter_once off;
|
||||||
|
sub_filter_types text/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pareil pour la racine /
|
||||||
|
location = / {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||||
|
sub_filter '__USER_EMAIL__' '$http_x_forwarded_email';
|
||||||
|
sub_filter_once off;
|
||||||
|
sub_filter_types text/html;
|
||||||
|
try_files /index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Le mapping JSON doit être accessible mais pas mis en cache
|
||||||
|
location = /roles-mapping.json {
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
|
add_header Content-Type application/json;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logo et autres fichiers statiques
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
configs/n8n/docker-compose.yml.template
Normal file
39
configs/n8n/docker-compose.yml.template
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
services:
|
||||||
|
n8n:
|
||||||
|
image: n8nio/n8n:latest
|
||||||
|
container_name: n8n
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# Base de données
|
||||||
|
DB_TYPE: postgresdb
|
||||||
|
DB_POSTGRESDB_HOST: postgres
|
||||||
|
DB_POSTGRESDB_PORT: 5432
|
||||||
|
DB_POSTGRESDB_DATABASE: n8n
|
||||||
|
DB_POSTGRESDB_USER: n8n
|
||||||
|
DB_POSTGRESDB_PASSWORD: ${N8N_DB_PASSWORD}
|
||||||
|
# ⚠️ Critique : sans cette clé on perd accès aux credentials chiffrés
|
||||||
|
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
|
||||||
|
# Config serveur
|
||||||
|
N8N_HOST: n8n.seizeconsulting.com
|
||||||
|
N8N_PORT: 5678
|
||||||
|
N8N_PROTOCOL: https
|
||||||
|
WEBHOOK_URL: https://n8n.seizeconsulting.com/
|
||||||
|
# Désactiver le sign-up : SSO uniquement
|
||||||
|
N8N_USER_MANAGEMENT_DISABLED: "false"
|
||||||
|
volumes:
|
||||||
|
- n8n_data:/home/node/.n8n
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
- backend
|
||||||
|
# ⚠️ Pas de labels Traefik : OAuth2-Proxy est exposé devant
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
n8n_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
|
backend:
|
||||||
|
external: true
|
||||||
51
configs/oauth2-proxy/.env.example
Normal file
51
configs/oauth2-proxy/.env.example
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# OAuth2-Proxy .env.example
|
||||||
|
# ============================================================================
|
||||||
|
# Pour chaque outil, créer un dossier ~/docker/oauth2-proxy-<outil>/
|
||||||
|
# avec son docker-compose.yml et son .env adapté.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# ===== PROVIDER OIDC ZITADEL =====
|
||||||
|
OAUTH2_PROXY_PROVIDER=oidc
|
||||||
|
OAUTH2_PROXY_CLIENT_ID=__REPLACE_WITH_ZITADEL_CLIENT_ID__
|
||||||
|
OAUTH2_PROXY_CLIENT_SECRET=__REPLACE_WITH_ZITADEL_CLIENT_SECRET__
|
||||||
|
|
||||||
|
# Générer un cookie secret 32 chars :
|
||||||
|
# python3 -c 'import secrets; print(secrets.token_urlsafe(32))'
|
||||||
|
OAUTH2_PROXY_COOKIE_SECRET=__REPLACE_WITH_32_CHARS_RANDOM__
|
||||||
|
|
||||||
|
OAUTH2_PROXY_OIDC_ISSUER_URL=https://sso.seizeconsulting.com
|
||||||
|
OAUTH2_PROXY_REDIRECT_URL=https://__TOOL__.seizeconsulting.com/oauth2/callback
|
||||||
|
|
||||||
|
# ===== UPSTREAM (l'outil derrière) =====
|
||||||
|
# Exemple pour n8n : http://n8n:5678
|
||||||
|
# Exemple pour intranet : http://intranet:80
|
||||||
|
OAUTH2_PROXY_UPSTREAMS=http://__TOOL_CONTAINER__:__TOOL_PORT__
|
||||||
|
|
||||||
|
# ===== 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
|
||||||
|
|
||||||
|
# ===== 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
|
||||||
|
OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL=true
|
||||||
|
|
||||||
|
# ===== RÉSEAU =====
|
||||||
|
OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
|
||||||
|
OAUTH2_PROXY_REVERSE_PROXY=true
|
||||||
|
OAUTH2_PROXY_WHITELIST_DOMAINS=.seizeconsulting.com
|
||||||
|
|
||||||
|
# ===== DIVERS =====
|
||||||
|
OAUTH2_PROXY_FOOTER=-
|
||||||
|
OAUTH2_PROXY_REDIRECT_URL_AUTH_DOMAIN_AS_REDIRECT_URL_SUFFIX=false
|
||||||
26
configs/oauth2-proxy/docker-compose.yml.template
Normal file
26
configs/oauth2-proxy/docker-compose.yml.template
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
# Pour adapter ce template :
|
||||||
|
# 1. Remplacer <outil> par le nom de l'outil (ex: n8n, portainer, code, status, intranet)
|
||||||
|
# 2. Adapter le host dans la règle Traefik
|
||||||
|
# 3. Créer le fichier .env avec les variables OAUTH2_PROXY_*
|
||||||
16
configs/portainer/docker-compose.yml.template
Normal file
16
configs/portainer/docker-compose.yml.template
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
portainer:
|
||||||
|
image: portainer/portainer-ce:latest
|
||||||
|
container_name: portainer
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# ⚠️ Accès au socket Docker = pouvoir total sur les conteneurs
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./data:/data
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
# ⚠️ Pas de labels Traefik : OAuth2-Proxy expose le host
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
23
configs/postgres/docker-compose.yml.template
Normal file
23
configs/postgres/docker-compose.yml.template
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
volumes:
|
||||||
|
- ./data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
# Pas exposé publiquement (uniquement réseau backend)
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
external: true
|
||||||
63
configs/traefik/docker-compose.yml.template
Normal file
63
configs/traefik/docker-compose.yml.template
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v3.7.1
|
||||||
|
container_name: traefik
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
# Dashboard sécurisé
|
||||||
|
- "--api.dashboard=true"
|
||||||
|
- "--api.insecure=false"
|
||||||
|
|
||||||
|
# Découverte Docker
|
||||||
|
- "--providers.docker=true"
|
||||||
|
- "--providers.docker.exposedbydefault=false"
|
||||||
|
- "--providers.docker.network=web"
|
||||||
|
|
||||||
|
# Config file dynamique
|
||||||
|
- "--providers.file.filename=/etc/traefik/dynamic.yml"
|
||||||
|
- "--providers.file.watch=true"
|
||||||
|
|
||||||
|
# Entrypoints
|
||||||
|
- "--entrypoints.web.address=:80"
|
||||||
|
- "--entrypoints.websecure.address=:443"
|
||||||
|
|
||||||
|
# Redirection HTTP → HTTPS
|
||||||
|
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
|
||||||
|
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
|
||||||
|
|
||||||
|
# Let's Encrypt
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
- "--log.level=INFO"
|
||||||
|
- "--accesslog=true"
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./letsencrypt:/letsencrypt
|
||||||
|
- ./dynamic.yml:/etc/traefik/dynamic.yml:ro
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
labels:
|
||||||
|
# Dashboard accessible via traefik.<domain>
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DOMAIN}`)"
|
||||||
|
- "traefik.http.routers.traefik.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.routers.traefik.service=api@internal"
|
||||||
|
- "traefik.http.routers.traefik.middlewares=traefik-auth"
|
||||||
|
# Basic auth (à remplacer par hash htpasswd réel)
|
||||||
|
- "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
54
configs/traefik/dynamic.yml.template
Normal file
54
configs/traefik/dynamic.yml.template
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# dynamic.yml — Configuration dynamique Traefik
|
||||||
|
# ============================================================================
|
||||||
|
# Middlewares partagés, notamment ForwardAuth pour le SSO.
|
||||||
|
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
# ⭐ Middleware ForwardAuth pour l'authentification SSO via OAuth2-Proxy
|
||||||
|
# ⚠️ La ligne "- Cookie" dans authRequestHeaders est CRITIQUE
|
||||||
|
oauth2-headers:
|
||||||
|
forwardAuth:
|
||||||
|
address: "http://oauth2-proxy:4180/oauth2/auth"
|
||||||
|
trustForwardHeader: true
|
||||||
|
authRequestHeaders:
|
||||||
|
- Cookie # ← INDISPENSABLE (sans, le SSO casse)
|
||||||
|
- Authorization
|
||||||
|
- X-Forwarded-Host
|
||||||
|
- X-Forwarded-Proto
|
||||||
|
- X-Forwarded-Uri
|
||||||
|
authResponseHeaders:
|
||||||
|
- X-Auth-Request-User
|
||||||
|
- X-Auth-Request-Email
|
||||||
|
- X-Auth-Request-Preferred-Username
|
||||||
|
- X-Auth-Request-Groups
|
||||||
|
- X-Auth-Request-Access-Token
|
||||||
|
- X-Forwarded-User
|
||||||
|
- X-Forwarded-Email
|
||||||
|
- X-Forwarded-Preferred-Username
|
||||||
|
- X-Forwarded-Groups
|
||||||
|
- Authorization
|
||||||
|
|
||||||
|
# Middleware "secure headers" - bonne pratique sécurité
|
||||||
|
secure-headers:
|
||||||
|
headers:
|
||||||
|
accessControlAllowMethods:
|
||||||
|
- GET
|
||||||
|
- POST
|
||||||
|
- OPTIONS
|
||||||
|
accessControlMaxAge: 100
|
||||||
|
hostsProxyHeaders:
|
||||||
|
- "X-Forwarded-Host"
|
||||||
|
sslRedirect: true
|
||||||
|
sslHost: ""
|
||||||
|
sslForceHost: true
|
||||||
|
sslProxyHeaders:
|
||||||
|
X-Forwarded-Proto: https
|
||||||
|
stsSeconds: 63072000
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
frameDeny: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
browserXssFilter: true
|
||||||
|
referrerPolicy: "strict-origin-when-cross-origin"
|
||||||
14
configs/uptime-kuma/docker-compose.yml.template
Normal file
14
configs/uptime-kuma/docker-compose.yml.template
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
uptime-kuma:
|
||||||
|
image: louislam/uptime-kuma:1
|
||||||
|
container_name: uptime-kuma
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
# ⚠️ Pas de labels Traefik : OAuth2-Proxy expose le host
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
30
configs/zitadel/.env.template
Normal file
30
configs/zitadel/.env.template
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Zitadel .env.template
|
||||||
|
# ============================================================================
|
||||||
|
# Copier en .env et adapter.
|
||||||
|
# ⚠️ ZITADEL_MASTERKEY est IMMUABLE après le premier démarrage !
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# ===== DOMAINE =====
|
||||||
|
ZITADEL_EXTERNALDOMAIN=sso.seizeconsulting.com
|
||||||
|
|
||||||
|
# ===== TRAEFIK PROXY INTERNE =====
|
||||||
|
TRAEFIK_IMAGE=traefik:v3.6.8
|
||||||
|
TRAEFIK_DASHBOARD_ENABLED=false
|
||||||
|
TRAEFIK_LOG_LEVEL=INFO
|
||||||
|
TRAEFIK_ACCESSLOG_ENABLED=false
|
||||||
|
PROXY_HTTP_PUBLISHED_PORT=8080
|
||||||
|
|
||||||
|
# ===== MASTER KEY (32 caractères) =====
|
||||||
|
# ⚠️ NE JAMAIS CHANGER après le premier démarrage
|
||||||
|
# Générer avec : openssl rand -base64 24 | head -c 32
|
||||||
|
ZITADEL_MASTERKEY=CHANGE_ME_32_CHARS_RANDOM_KEY_HERE
|
||||||
|
|
||||||
|
# ===== POSTGRES ZITADEL =====
|
||||||
|
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=CHANGE_ME_STRONG_PASSWORD
|
||||||
|
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=CHANGE_ME_STRONG_PASSWORD
|
||||||
|
|
||||||
|
# ===== ADMIN INITIAL =====
|
||||||
|
# À changer au premier login
|
||||||
|
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME=zitadel-admin
|
||||||
|
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD=CHANGE_ME_AT_FIRST_LOGIN
|
||||||
136
configs/zitadel/docker-compose.yml.template
Normal file
136
configs/zitadel/docker-compose.yml.template
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Zitadel docker-compose.yml.template
|
||||||
|
# ============================================================================
|
||||||
|
# Basé sur la quickstart officielle Zitadel avec proxy Traefik intégré.
|
||||||
|
# Source : https://github.com/zitadel/zitadel-charts/tree/main/charts/zitadel
|
||||||
|
#
|
||||||
|
# Ce docker-compose contient :
|
||||||
|
# - proxy : Traefik dédié à Zitadel
|
||||||
|
# - zitadel : Serveur API
|
||||||
|
# - zitadel-login : UI de login v2
|
||||||
|
# - postgres : DB dédiée
|
||||||
|
#
|
||||||
|
# ⚠️ Ce fichier est SIMPLIFIÉ. Pour la version complète production-ready,
|
||||||
|
# voir le repo officiel : https://github.com/zitadel/zitadel-charts
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
name: zitadel
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Traefik dédié à Zitadel (isolé du Traefik principal)
|
||||||
|
proxy:
|
||||||
|
image: ${TRAEFIK_IMAGE}
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --providers.docker=true
|
||||||
|
- --providers.docker.exposedbydefault=false
|
||||||
|
- --providers.docker.network=zitadel
|
||||||
|
- --entrypoints.web.address=:80
|
||||||
|
- --entrypoints.websecure.address=:443
|
||||||
|
- --api.dashboard=${TRAEFIK_DASHBOARD_ENABLED}
|
||||||
|
- --api.insecure=false
|
||||||
|
- --ping=true
|
||||||
|
- --ping.entrypoint=web
|
||||||
|
- --log.level=${TRAEFIK_LOG_LEVEL}
|
||||||
|
- --accesslog=${TRAEFIK_ACCESSLOG_ENABLED}
|
||||||
|
ports:
|
||||||
|
- ${PROXY_HTTP_PUBLISHED_PORT}:80
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
networks:
|
||||||
|
- zitadel
|
||||||
|
- web # Pour que le Traefik principal puisse router vers ici
|
||||||
|
labels:
|
||||||
|
# Permet au Traefik principal externe de router vers ce Traefik interne
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.zitadel-external.rule=Host(`${ZITADEL_EXTERNALDOMAIN}`)"
|
||||||
|
- "traefik.http.routers.zitadel-external.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.zitadel-external.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.zitadel-external.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
zitadel:
|
||||||
|
image: ghcr.io/zitadel/zitadel:v4.13.0
|
||||||
|
restart: unless-stopped
|
||||||
|
command: 'start-from-init --masterkey "${ZITADEL_MASTERKEY}" --tlsMode disabled'
|
||||||
|
environment:
|
||||||
|
ZITADEL_DATABASE_POSTGRES_HOST: postgres
|
||||||
|
ZITADEL_DATABASE_POSTGRES_PORT: 5432
|
||||||
|
ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
|
||||||
|
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
|
||||||
|
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: ${ZITADEL_DATABASE_POSTGRES_USER_PASSWORD}
|
||||||
|
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
|
||||||
|
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
|
||||||
|
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: ${ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD}
|
||||||
|
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
|
||||||
|
ZITADEL_EXTERNALDOMAIN: ${ZITADEL_EXTERNALDOMAIN}
|
||||||
|
ZITADEL_EXTERNALPORT: 443
|
||||||
|
ZITADEL_EXTERNALSECURE: "true"
|
||||||
|
ZITADEL_TLS_ENABLED: "false"
|
||||||
|
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: ${ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME}
|
||||||
|
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: ${ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD}
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- zitadel
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "/app/zitadel", "ready", "--config", "/example-zitadel-config.yaml"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 30s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
|
volumes:
|
||||||
|
- ./machinekey:/machinekey
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.zitadel.rule=Host(`${ZITADEL_EXTERNALDOMAIN}`) && (PathPrefix(`/zitadel.`) || PathPrefix(`/oauth/v2`) || PathPrefix(`/openid`) || PathPrefix(`/saml`) || PathPrefix(`/.well-known`))"
|
||||||
|
- "traefik.http.routers.zitadel.entrypoints=web"
|
||||||
|
- "traefik.http.services.zitadel.loadbalancer.server.port=8080"
|
||||||
|
|
||||||
|
zitadel-login:
|
||||||
|
image: ghcr.io/zitadel/zitadel-login:v4.13.0
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
ZITADEL_API_URL: http://zitadel:8080
|
||||||
|
ZITADEL_LOGIN_BASE_URL: https://${ZITADEL_EXTERNALDOMAIN}/ui/v2/login
|
||||||
|
NEXTAUTH_URL: https://${ZITADEL_EXTERNALDOMAIN}/ui/v2/login
|
||||||
|
depends_on:
|
||||||
|
zitadel:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- zitadel
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3000"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 30s
|
||||||
|
retries: 5
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.zitadel-login.rule=Host(`${ZITADEL_EXTERNALDOMAIN}`)"
|
||||||
|
- "traefik.http.routers.zitadel-login.entrypoints=web"
|
||||||
|
- "traefik.http.routers.zitadel-login.priority=10"
|
||||||
|
- "traefik.http.services.zitadel-login.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:17.2-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD}
|
||||||
|
POSTGRES_DB: zitadel
|
||||||
|
networks:
|
||||||
|
- zitadel
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d zitadel"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 30s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
|
volumes:
|
||||||
|
- ./postgres-data:/var/lib/postgresql/data:rw
|
||||||
|
|
||||||
|
networks:
|
||||||
|
zitadel:
|
||||||
|
driver: bridge
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
218
docs/01-dns-prerequis.md
Normal file
218
docs/01-dns-prerequis.md
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
# 01 — DNS et prérequis
|
||||||
|
|
||||||
|
> Tout ce qu'il faut côté DNS et serveur avant de commencer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Records DNS à configurer
|
||||||
|
|
||||||
|
Chez votre registrar (Scaleway, OVH, Gandi, etc.), ajouter les records suivants.
|
||||||
|
|
||||||
|
⚠️ **Tous les records sont des `A`** (pas de `CNAME`) pointant vers l'IP publique du serveur.
|
||||||
|
|
||||||
|
| Sous-domaine | Type | Valeur | Usage |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `@` (racine) | A | `<IP>` | Site vitrine (optionnel) |
|
||||||
|
| `sso` | A | `<IP>` | Zitadel — Identity Provider |
|
||||||
|
| `intranet` | A | `<IP>` | Page d'accueil RBAC |
|
||||||
|
| `git` | A | `<IP>` | Gitea |
|
||||||
|
| `n8n` | A | `<IP>` | n8n workflows |
|
||||||
|
| `portainer` | A | `<IP>` | Portainer Docker |
|
||||||
|
| `code` | A | `<IP>` | Code Server (VS Code web) |
|
||||||
|
| `status` | A | `<IP>` | Uptime Kuma |
|
||||||
|
| `traefik` | A | `<IP>` | Dashboard Traefik (admin) |
|
||||||
|
| `app` | A | `<IP>` | whoami debug (optionnel) |
|
||||||
|
|
||||||
|
### TTL recommandé : 300 (5 min)
|
||||||
|
|
||||||
|
Pour pouvoir corriger rapidement en cas d'erreur.
|
||||||
|
|
||||||
|
### Vérification après ajout
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sur ton PC
|
||||||
|
for sub in sso intranet git n8n portainer code status traefik; do
|
||||||
|
echo -n "$sub.seizeconsulting.com : "
|
||||||
|
dig +short $sub.seizeconsulting.com
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Toutes les lignes doivent afficher l'IP du serveur.
|
||||||
|
|
||||||
|
⚠️ La propagation DNS peut prendre **1-24h**. Tester depuis plusieurs réseaux (4G, autre wifi) si pas immédiat.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖥️ Spécifications serveur recommandées
|
||||||
|
|
||||||
|
### Minimum (88 users actifs Seize)
|
||||||
|
|
||||||
|
- **CPU** : 4 vCPU
|
||||||
|
- **RAM** : 8 Go
|
||||||
|
- **Disque** : 80 Go SSD
|
||||||
|
- **Bande passante** : 100 Mbps
|
||||||
|
|
||||||
|
### Recommandé (pour confort + croissance)
|
||||||
|
|
||||||
|
- **CPU** : 8 vCPU
|
||||||
|
- **RAM** : 16 Go
|
||||||
|
- **Disque** : 160 Go SSD
|
||||||
|
- **Bande passante** : 1 Gbps
|
||||||
|
|
||||||
|
### Provider testé : Scaleway
|
||||||
|
|
||||||
|
- Type d'instance : `PRO2-S` (4 vCPU, 16 Go RAM)
|
||||||
|
- Région : `fr-par-1`
|
||||||
|
- OS : Debian 13 (Trixie)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Firewall (recommandé)
|
||||||
|
|
||||||
|
### Configuration via ufw
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y ufw
|
||||||
|
sudo ufw default deny incoming
|
||||||
|
sudo ufw default allow outgoing
|
||||||
|
sudo ufw allow 22/tcp comment 'SSH'
|
||||||
|
sudo ufw allow 80/tcp comment 'HTTP (Traefik redirect)'
|
||||||
|
sudo ufw allow 443/tcp comment 'HTTPS'
|
||||||
|
sudo ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ufw status verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Doit montrer ports 22, 80, 443 autorisés.
|
||||||
|
|
||||||
|
⚠️ **Ne PAS oublier d'autoriser le port 22** avant d'activer ufw, sinon tu te bloques hors du serveur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Hardening SSH
|
||||||
|
|
||||||
|
### Désactiver le login root
|
||||||
|
|
||||||
|
Éditer `/etc/ssh/sshd_config` :
|
||||||
|
```
|
||||||
|
PermitRootLogin no
|
||||||
|
PasswordAuthentication no
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart : `sudo systemctl restart sshd`
|
||||||
|
|
||||||
|
⚠️ **Avoir un user non-root configuré AVANT** de désactiver root.
|
||||||
|
|
||||||
|
### Changer le port SSH (optionnel)
|
||||||
|
|
||||||
|
Si beaucoup de scans bots :
|
||||||
|
```
|
||||||
|
Port 2222 # Au lieu de 22
|
||||||
|
```
|
||||||
|
|
||||||
|
Ne pas oublier d'ouvrir le nouveau port dans ufw avant le restart sshd.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 Prérequis Docker
|
||||||
|
|
||||||
|
### Installer Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://get.docker.com | sudo sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ajouter user au groupe docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
exit # Se déconnecter
|
||||||
|
# Se reconnecter
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérifier l'install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker --version
|
||||||
|
docker compose version
|
||||||
|
docker ps
|
||||||
|
```
|
||||||
|
|
||||||
|
Tous doivent fonctionner sans `sudo`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Réseaux Docker à créer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create web
|
||||||
|
docker network create backend
|
||||||
|
```
|
||||||
|
|
||||||
|
- **web** : réseau pour les services exposés publiquement (via Traefik)
|
||||||
|
- **backend** : réseau pour les services internes (Postgres, Redis si utilisé)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏰ NTP / Synchronisation horaire
|
||||||
|
|
||||||
|
⚠️ **Critique** pour les certificats TLS et les tokens JWT.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y systemd-timesyncd
|
||||||
|
sudo systemctl enable --now systemd-timesyncd
|
||||||
|
timedatectl status
|
||||||
|
```
|
||||||
|
|
||||||
|
→ "System clock synchronized: yes" doit apparaître.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Monitoring serveur de base
|
||||||
|
|
||||||
|
### Installer htop, iotop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y htop iotop ncdu
|
||||||
|
```
|
||||||
|
|
||||||
|
### Espace disque alert
|
||||||
|
|
||||||
|
Optionnel : alert si > 80% d'espace utilisé :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > ~/disk-alert.sh << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
|
||||||
|
if [ $USAGE -gt 80 ]; then
|
||||||
|
echo "⚠️ Espace disque > 80% : $USAGE%" | mail -s "Alert disk Seize" admin@seizeconsulting.com
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
chmod +x ~/disk-alert.sh
|
||||||
|
|
||||||
|
# Crontab tous les jours à 8h
|
||||||
|
(crontab -l ; echo "0 8 * * * /home/admin/disk-alert.sh") | crontab -
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ Nécessite un client mail configuré (mailutils, ssmtp, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist avant de passer aux étapes suivantes
|
||||||
|
|
||||||
|
- [ ] DNS configurés et propagés
|
||||||
|
- [ ] Serveur accessible en SSH avec user non-root
|
||||||
|
- [ ] Firewall actif (22, 80, 443 ouverts)
|
||||||
|
- [ ] SSH hardening fait (pas de root, pas de password auth)
|
||||||
|
- [ ] Docker + Docker Compose installés et fonctionnels
|
||||||
|
- [ ] Réseaux Docker `web` et `backend` créés
|
||||||
|
- [ ] Horloge synchronisée (NTP)
|
||||||
|
- [ ] htop / outils de base installés
|
||||||
|
- [ ] Espace disque > 50 Go libre
|
||||||
|
|
||||||
|
→ Tout coché ? Passer à [02-traefik-setup.md](./02-traefik-setup.md).
|
||||||
255
docs/02-traefik-setup.md
Normal file
255
docs/02-traefik-setup.md
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
# 02 — Traefik (Reverse Proxy)
|
||||||
|
|
||||||
|
> Traefik est le **point d'entrée** unique de l'infrastructure. Il route le trafic vers les bons conteneurs et gère les certificats TLS automatiquement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Rôle de Traefik
|
||||||
|
|
||||||
|
| Fonction | Description |
|
||||||
|
|---|---|
|
||||||
|
| **Reverse proxy** | Route `<sous-domaine>.seizeconsulting.com` → conteneur correspondant |
|
||||||
|
| **TLS termination** | Certificats Let's Encrypt automatiques pour tous les sous-domaines |
|
||||||
|
| **HTTP → HTTPS redirect** | Force le HTTPS partout |
|
||||||
|
| **ForwardAuth middleware** | Délègue l'auth à OAuth2-Proxy pour le SSO |
|
||||||
|
| **Dashboard** | UI admin sur `traefik.seizeconsulting.com` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
~/docker/traefik/
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── traefik.yml # Config statique (entrypoints, providers)
|
||||||
|
├── dynamic.yml # Config dynamique (middlewares, routers manuels)
|
||||||
|
└── letsencrypt/
|
||||||
|
└── acme.json # Certificats stockés (chmod 600)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 docker-compose.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v3.7.1
|
||||||
|
container_name: traefik
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- "--api.dashboard=true"
|
||||||
|
- "--api.insecure=false"
|
||||||
|
- "--providers.docker=true"
|
||||||
|
- "--providers.docker.exposedbydefault=false"
|
||||||
|
- "--providers.docker.network=web"
|
||||||
|
- "--providers.file.filename=/etc/traefik/dynamic.yml"
|
||||||
|
- "--providers.file.watch=true"
|
||||||
|
- "--entrypoints.web.address=:80"
|
||||||
|
- "--entrypoints.websecure.address=:443"
|
||||||
|
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
|
||||||
|
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.email=admin@seizeconsulting.com"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
|
||||||
|
- "--log.level=INFO"
|
||||||
|
- "--accesslog=true"
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./letsencrypt:/letsencrypt
|
||||||
|
- ./dynamic.yml:/etc/traefik/dynamic.yml:ro
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
labels:
|
||||||
|
# Dashboard sur traefik.seizeconsulting.com
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.traefik.rule=Host(`traefik.seizeconsulting.com`)"
|
||||||
|
- "traefik.http.routers.traefik.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.routers.traefik.service=api@internal"
|
||||||
|
- "traefik.http.routers.traefik.middlewares=traefik-auth"
|
||||||
|
# Basic auth pour le dashboard (à remplacer par OAuth2-Proxy plus tard si on veut SSO)
|
||||||
|
- "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$apr1$$xxxxxxxx$$xxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Remplacer** :
|
||||||
|
- `admin@seizeconsulting.com` par votre email Let's Encrypt
|
||||||
|
- Le hash basicauth via `htpasswd -nb admin <mdp>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 dynamic.yml (middlewares ForwardAuth)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
# Middleware ForwardAuth générique pour les outils protégés par OAuth2-Proxy
|
||||||
|
oauth2-headers:
|
||||||
|
forwardAuth:
|
||||||
|
address: "http://oauth2-proxy:4180/oauth2/auth"
|
||||||
|
trustForwardHeader: true
|
||||||
|
# ⚠️ LIGNE CRITIQUE : sans 'Cookie', le SSO ne marche pas
|
||||||
|
authRequestHeaders:
|
||||||
|
- Cookie
|
||||||
|
- Authorization
|
||||||
|
- X-Forwarded-Host
|
||||||
|
- X-Forwarded-Proto
|
||||||
|
- X-Forwarded-Uri
|
||||||
|
authResponseHeaders:
|
||||||
|
- X-Auth-Request-User
|
||||||
|
- X-Auth-Request-Email
|
||||||
|
- X-Auth-Request-Preferred-Username
|
||||||
|
- X-Auth-Request-Groups
|
||||||
|
- X-Auth-Request-Access-Token
|
||||||
|
- X-Forwarded-User
|
||||||
|
- X-Forwarded-Email
|
||||||
|
- X-Forwarded-Preferred-Username
|
||||||
|
- X-Forwarded-Groups
|
||||||
|
- Authorization
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Le `Cookie` dans `authRequestHeaders` est ESSENTIEL** — sans ça, le SSO casse complètement (cf. [DECISIONS.md D3](../DECISIONS.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Démarrage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/docker/traefik
|
||||||
|
# Créer l'acme.json
|
||||||
|
touch letsencrypt/acme.json
|
||||||
|
chmod 600 letsencrypt/acme.json
|
||||||
|
|
||||||
|
# Lancer
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Vérifier les logs
|
||||||
|
docker compose logs -f
|
||||||
|
# Ctrl+C pour quitter
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérifications
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Conteneur tourne ?
|
||||||
|
docker ps | grep traefik
|
||||||
|
|
||||||
|
# Logs propres ?
|
||||||
|
docker logs --tail 30 traefik 2>&1 | grep -i "error\|level=error"
|
||||||
|
|
||||||
|
# Accès au dashboard ?
|
||||||
|
curl -sI https://traefik.seizeconsulting.com
|
||||||
|
# Doit retourner 401 (demande de basic auth)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Comment ajouter un service derrière Traefik
|
||||||
|
|
||||||
|
### Méthode 1 — Labels Docker (recommandé)
|
||||||
|
|
||||||
|
Sur le `docker-compose.yml` du service à exposer, ajouter ces labels :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
myservice:
|
||||||
|
image: ...
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.myservice.rule=Host(`myservice.seizeconsulting.com`)"
|
||||||
|
- "traefik.http.routers.myservice.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.myservice.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.myservice.loadbalancer.server.port=8080" # port interne du service
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Important** : le service doit être sur le réseau `web` partagé avec Traefik.
|
||||||
|
|
||||||
|
### Méthode 2 — Avec SSO (via OAuth2-Proxy)
|
||||||
|
|
||||||
|
Pour les services qui n'ont pas de SSO natif, on ajoute OAuth2-Proxy entre Traefik et le service. Voir [04-oauth2-proxy-pattern.md](./04-oauth2-proxy-pattern.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### "Certificat Let's Encrypt en échec"
|
||||||
|
|
||||||
|
Symptômes : navigateur dit "Connexion non sécurisée".
|
||||||
|
|
||||||
|
Causes possibles :
|
||||||
|
1. **DNS pas propagé** : `dig +short sub.seizeconsulting.com` doit retourner l'IP du serveur
|
||||||
|
2. **Ports 80/443 bloqués** : firewall + cloud provider
|
||||||
|
3. **Rate limit Let's Encrypt** : trop de tentatives, attendre 1h
|
||||||
|
4. **acme.json corrompu** :
|
||||||
|
```bash
|
||||||
|
sudo rm letsencrypt/acme.json
|
||||||
|
sudo touch letsencrypt/acme.json
|
||||||
|
sudo chmod 600 letsencrypt/acme.json
|
||||||
|
docker compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### "404 page not found" sur un sous-domaine
|
||||||
|
|
||||||
|
Causes possibles :
|
||||||
|
1. Le service ciblé n'a pas les bons labels Traefik
|
||||||
|
2. Le service n'est pas sur le réseau `web`
|
||||||
|
3. Le port interne du service est incorrect dans `loadbalancer.server.port`
|
||||||
|
4. La règle `Host(...)` est mal écrite (vérifier les backticks)
|
||||||
|
|
||||||
|
Vérifier dans le dashboard Traefik : `https://traefik.seizeconsulting.com` → Routers, Services.
|
||||||
|
|
||||||
|
### "Bad Gateway 502"
|
||||||
|
|
||||||
|
Causes possibles :
|
||||||
|
1. Le service backend est down
|
||||||
|
2. Le service écoute sur un port différent de celui annoncé à Traefik
|
||||||
|
3. Le service écoute sur 127.0.0.1 au lieu de 0.0.0.0
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier que le service répond bien sur le port interne attendu
|
||||||
|
docker exec -it traefik wget -qO- http://myservice:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Le SSO ne marche plus depuis 5 min"
|
||||||
|
|
||||||
|
Vérifier la config ForwardAuth :
|
||||||
|
```bash
|
||||||
|
grep -A 10 "oauth2-headers" ~/docker/traefik/dynamic.yml
|
||||||
|
# La ligne "- Cookie" doit être présente dans authRequestHeaders
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Mise à jour Traefik
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/docker/traefik
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ Avant une **major version** (v2 → v3 par exemple) :
|
||||||
|
- Lire le CHANGELOG
|
||||||
|
- Tester en local d'abord
|
||||||
|
- Backup de `acme.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Liens utiles
|
||||||
|
|
||||||
|
- Documentation Traefik : https://doc.traefik.io/traefik/
|
||||||
|
- Référence des middlewares : https://doc.traefik.io/traefik/middlewares/overview/
|
||||||
|
- ForwardAuth doc : https://doc.traefik.io/traefik/middlewares/http/forwardauth/
|
||||||
281
docs/03-zitadel-setup.md
Normal file
281
docs/03-zitadel-setup.md
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
# 03 — Zitadel (Identity Provider)
|
||||||
|
|
||||||
|
> Zitadel est le **cœur de l'authentification**. Tous les outils se loggent via lui.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Architecture Zitadel
|
||||||
|
|
||||||
|
⚠️ **Spécificité importante** : Zitadel se déploie via un **docker-compose complet** qui inclut :
|
||||||
|
- Le serveur API Zitadel
|
||||||
|
- L'UI de login (login v2)
|
||||||
|
- Postgres dédié (séparé du Postgres principal)
|
||||||
|
- Un Traefik dédié interne (séparé du Traefik principal !)
|
||||||
|
|
||||||
|
→ Le Traefik principal de Seize **route uniquement** vers le Traefik interne de Zitadel via la règle `Host(sso.seizeconsulting.com)`.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────┐
|
||||||
|
│ Réseau Docker "zitadel" │
|
||||||
|
│ │
|
||||||
|
Traefik │ ┌──────────┐ ┌──────────────┐ │
|
||||||
|
principal ─────▶│ │ Traefik │───▶│ Zitadel │ │
|
||||||
|
(web) │ │ interne │ │ - API │ │
|
||||||
|
│ │ │ │ - Login UI │ │
|
||||||
|
│ └──────────┘ └──────┬───────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────▼──────┐ │
|
||||||
|
│ │ Postgres │ │
|
||||||
|
│ │ (zitadel) │ │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
└───────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
~/docker/zitadel/
|
||||||
|
├── docker-compose.yml # Multi-services (proxy + api + login + db)
|
||||||
|
├── .env # Configuration et secrets
|
||||||
|
└── machinekey/ # Clés des service accounts (à backup)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 docker-compose.yml (résumé)
|
||||||
|
|
||||||
|
⚠️ Le fichier officiel de Zitadel fait ~150 lignes. Voir `configs/zitadel/docker-compose.yml.template` pour la version complète.
|
||||||
|
|
||||||
|
Services inclus :
|
||||||
|
- `proxy` (Traefik interne)
|
||||||
|
- `zitadel-api` (le moteur Zitadel)
|
||||||
|
- `zitadel-login` (UI de connexion v2)
|
||||||
|
- `postgres` (database dédiée)
|
||||||
|
|
||||||
|
### Volumes critiques à backuper
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./postgres-data:/var/lib/postgresql/data # ⚠️ CRITIQUE
|
||||||
|
- ./machinekey:/machinekey # ⚠️ CRITIQUE
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 .env (variables clés)
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Domaine d'accès
|
||||||
|
ZITADEL_EXTERNALDOMAIN=sso.seizeconsulting.com
|
||||||
|
ZITADEL_EXTERNALPORT=443
|
||||||
|
ZITADEL_EXTERNALSECURE=true
|
||||||
|
ZITADEL_TLS_ENABLED=false # TLS géré par Traefik externe
|
||||||
|
|
||||||
|
# Master key (⚠️ NE JAMAIS perdre, NE JAMAIS changer après init)
|
||||||
|
ZITADEL_MASTERKEY=<32-char-random-key>
|
||||||
|
|
||||||
|
# Postgres
|
||||||
|
ZITADEL_DATABASE_POSTGRES_HOST=postgres
|
||||||
|
ZITADEL_DATABASE_POSTGRES_PORT=5432
|
||||||
|
ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel
|
||||||
|
ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel
|
||||||
|
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=<password>
|
||||||
|
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=postgres
|
||||||
|
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=<admin-password>
|
||||||
|
|
||||||
|
# Admin initial
|
||||||
|
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME=zitadel-admin
|
||||||
|
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD=<temp-password-to-change>
|
||||||
|
|
||||||
|
# Traefik image (pour le proxy interne)
|
||||||
|
TRAEFIK_IMAGE=traefik:v3.6.8
|
||||||
|
TRAEFIK_DASHBOARD_ENABLED=false
|
||||||
|
TRAEFIK_LOG_LEVEL=INFO
|
||||||
|
TRAEFIK_ACCESSLOG_ENABLED=false
|
||||||
|
PROXY_HTTP_PUBLISHED_PORT=80
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **`ZITADEL_MASTERKEY` est immuable** une fois l'instance initialisée. Le perdre = pouvoir reconstruire from scratch uniquement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Premier démarrage
|
||||||
|
|
||||||
|
### 1. Préparer l'environnement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/docker/zitadel/{postgres-data,machinekey}
|
||||||
|
cd ~/docker/zitadel
|
||||||
|
# Copier docker-compose.yml et .env depuis les templates
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Lancer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Suivre les logs (premier démarrage = 1-2 min)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f zitadel-api
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendre le message :
|
||||||
|
```
|
||||||
|
init step done: type=instance_setup
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Tester l'UI
|
||||||
|
|
||||||
|
Navigateur : `https://sso.seizeconsulting.com`
|
||||||
|
|
||||||
|
→ Doit afficher la page de login Zitadel.
|
||||||
|
|
||||||
|
### 5. Se logger en admin
|
||||||
|
|
||||||
|
- Username : `zitadel-admin@zitadel.sso.seizeconsulting.com`
|
||||||
|
- Password : celui défini dans `.env` `ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD`
|
||||||
|
|
||||||
|
⚠️ **Changer le mdp immédiatement** au premier login.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Configuration post-install via UI
|
||||||
|
|
||||||
|
### 1. Default Settings → Policies
|
||||||
|
|
||||||
|
Aller dans **Default Settings** (icône ⚙️ en haut à droite).
|
||||||
|
|
||||||
|
#### Password Complexity
|
||||||
|
- ☑️ Min length : 14
|
||||||
|
- ☑️ Has uppercase
|
||||||
|
- ☑️ Has lowercase
|
||||||
|
- ☑️ Has number
|
||||||
|
- ☑️ Has symbol
|
||||||
|
|
||||||
|
#### Lockout Policy
|
||||||
|
- ☑️ Max password attempts : 5
|
||||||
|
- ☑️ Show lockout failures (sécurité : ne pas révéler les comptes)
|
||||||
|
|
||||||
|
#### MFA (Force MFA pour tous)
|
||||||
|
- ☑️ Force MFA : Enabled
|
||||||
|
- ☑️ MFA Init methods : TOTP, U2F, OTP Email
|
||||||
|
|
||||||
|
### 2. Branding
|
||||||
|
|
||||||
|
- Logo Seize
|
||||||
|
- Couleurs : `--c-accent: #008A81`
|
||||||
|
- Texte personnalisé
|
||||||
|
|
||||||
|
### 3. SMTP Provider (à faire quand admin M365 dispo)
|
||||||
|
|
||||||
|
Default Settings → Notifications → SMTP Provider :
|
||||||
|
|
||||||
|
```
|
||||||
|
Host : smtp.office365.com:587
|
||||||
|
Username : noreply@seizeconsulting.com
|
||||||
|
Password : <App Password M365>
|
||||||
|
From : noreply@seizeconsulting.com
|
||||||
|
TLS : ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Créer un Project "Infra Seize"
|
||||||
|
|
||||||
|
Menu Projects → New :
|
||||||
|
- Name : `Infra Seize`
|
||||||
|
- Type : Owned project
|
||||||
|
|
||||||
|
Project ID à noter : sera utilisé dans tous les scripts.
|
||||||
|
|
||||||
|
### 5. Créer le service account `furious-import-service`
|
||||||
|
|
||||||
|
Voir [06-import-furious.md](./06-import-furious.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Gestion des Administrators IAM
|
||||||
|
|
||||||
|
Pour ajouter un user/service account comme admin de toute l'instance :
|
||||||
|
|
||||||
|
1. Default Settings → en haut à droite, cliquer sur **"+"** à côté des avatars
|
||||||
|
2. Page "Administrators" → **"+ New"**
|
||||||
|
3. Sélectionner l'user
|
||||||
|
4. Cocher les **rôles minimum nécessaires** :
|
||||||
|
- `IAM_USER_MANAGER` : pour gérer les users via API
|
||||||
|
- `IAM_ORG_MANAGER` : pour gérer les organisations
|
||||||
|
- ❌ Éviter `IAM_OWNER` (trop puissant)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Volumes critiques à backuper
|
||||||
|
|
||||||
|
| Volume | Contenu | Criticité |
|
||||||
|
|---|---|---|
|
||||||
|
| `postgres-data/` | Base Zitadel (users, projects, sessions) | 🔴 CRITIQUE |
|
||||||
|
| `machinekey/` | Clés des service accounts | 🔴 CRITIQUE |
|
||||||
|
| `.env` | Secrets de config + MASTERKEY | 🔴 CRITIQUE |
|
||||||
|
|
||||||
|
### Backup recommandé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup quotidien automatique (cron)
|
||||||
|
0 3 * * * docker exec zitadel-postgres-1 pg_dump -U zitadel zitadel > /home/admin/backups/zitadel-$(date +\%Y\%m\%d).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### "Service zitadel-api ne démarre pas"
|
||||||
|
|
||||||
|
Causes possibles :
|
||||||
|
1. Postgres pas prêt → attendre 30s et `docker compose restart zitadel-api`
|
||||||
|
2. `.env` mal configuré → vérifier les variables
|
||||||
|
3. Migration DB échouée → `docker compose logs zitadel-api | grep -i error`
|
||||||
|
|
||||||
|
### "Master key mismatch"
|
||||||
|
|
||||||
|
⚠️ **Très grave** : ZITADEL_MASTERKEY a changé alors que l'instance était déjà init.
|
||||||
|
|
||||||
|
Solution :
|
||||||
|
- Soit retrouver l'ancienne clé
|
||||||
|
- Soit restore depuis backup
|
||||||
|
- Soit reset complet (perte de toutes les données)
|
||||||
|
|
||||||
|
→ Ne jamais changer ZITADEL_MASTERKEY après le premier démarrage.
|
||||||
|
|
||||||
|
### "Login impossible pour un user"
|
||||||
|
|
||||||
|
Vérifier :
|
||||||
|
1. User existe : Console → Users → Search
|
||||||
|
2. User actif (pas désactivé)
|
||||||
|
3. Email vérifié (sinon le login peut bloquer)
|
||||||
|
4. MFA bien configuré (si forcé au niveau policy)
|
||||||
|
5. Pas de lockout actif (Failed login attempts)
|
||||||
|
|
||||||
|
### "Le scope `urn:zitadel:iam:org:project:roles` ne retourne pas les rôles"
|
||||||
|
|
||||||
|
Vérifier :
|
||||||
|
1. Project setting **"Return user roles during authentication"** est coché
|
||||||
|
2. App setting **"User roles inside ID Token"** est coché
|
||||||
|
3. Auth Token Type = **JWT** (pas Bearer Token)
|
||||||
|
4. OAuth2-Proxy demande bien ce scope dans `OAUTH2_PROXY_SCOPE`
|
||||||
|
|
||||||
|
### "Service account peut pas créer de users (401/403)"
|
||||||
|
|
||||||
|
Vérifier :
|
||||||
|
1. Service account a bien le rôle `IAM_USER_MANAGER` (dans Default Settings → Administrators)
|
||||||
|
2. Clé JSON valide et non expirée
|
||||||
|
3. Token JWT signé correctement par le script Python
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Liens utiles
|
||||||
|
|
||||||
|
- Documentation Zitadel : https://zitadel.com/docs
|
||||||
|
- API v2 reference : https://zitadel.com/docs/apis/resources/user_service_v2
|
||||||
|
- Self-hosting guide : https://zitadel.com/docs/self-hosting/deploy/overview
|
||||||
|
- Custom claims (Actions V2) : https://zitadel.com/docs/concepts/features/actions_v2
|
||||||
353
docs/04-oauth2-proxy-pattern.md
Normal file
353
docs/04-oauth2-proxy-pattern.md
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
358
docs/05-applications.md
Normal file
358
docs/05-applications.md
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
# 05 — Applications déployées
|
||||||
|
|
||||||
|
> Détails de chaque application protégée par SSO.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Vue d'ensemble
|
||||||
|
|
||||||
|
| App | Image Docker | SSO via | Port interne | URL |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Gitea | `gitea/gitea:1.22` | OIDC natif | 3000 (HTTP), 22 (SSH) | git.seizeconsulting.com |
|
||||||
|
| n8n | `n8nio/n8n:latest` | OAuth2-Proxy | 5678 | n8n.seizeconsulting.com |
|
||||||
|
| Portainer | `portainer/portainer-ce:latest` | OAuth2-Proxy | 9000 | portainer.seizeconsulting.com |
|
||||||
|
| Code Server | `lscr.io/linuxserver/code-server:latest` | OAuth2-Proxy | 8443 | code.seizeconsulting.com |
|
||||||
|
| Uptime Kuma | `louislam/uptime-kuma:1` | OAuth2-Proxy | 3001 | status.seizeconsulting.com |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐙 Gitea
|
||||||
|
|
||||||
|
### Spécificité : SSO OIDC natif
|
||||||
|
|
||||||
|
Contrairement aux autres, Gitea **ne passe PAS par OAuth2-Proxy**. Il a sa propre intégration OIDC.
|
||||||
|
|
||||||
|
### docker-compose.yml (résumé)
|
||||||
|
|
||||||
|
Voir `configs/gitea/docker-compose.yml.template`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
gitea:
|
||||||
|
image: gitea/gitea:1.22
|
||||||
|
container_name: gitea
|
||||||
|
environment:
|
||||||
|
USER_UID: 1000
|
||||||
|
USER_GID: 1000
|
||||||
|
GITEA__database__DB_TYPE: postgres
|
||||||
|
GITEA__database__HOST: postgres:5432
|
||||||
|
GITEA__database__NAME: gitea
|
||||||
|
GITEA__database__USER: gitea
|
||||||
|
GITEA__database__PASSWD: ${GITEA_DB_PASSWORD}
|
||||||
|
GITEA__server__DOMAIN: git.seizeconsulting.com
|
||||||
|
GITEA__server__ROOT_URL: https://git.seizeconsulting.com/
|
||||||
|
GITEA__server__SSH_DOMAIN: git.seizeconsulting.com
|
||||||
|
# Auto-registration via OIDC
|
||||||
|
GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION: "true"
|
||||||
|
GITEA__oauth2_client__USERNAME: preferred_username
|
||||||
|
GITEA__oauth2_client__ACCOUNT_LINKING: auto
|
||||||
|
# Désactiver le register manuel (SSO uniquement)
|
||||||
|
GITEA__service__DISABLE_REGISTRATION: "true"
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
ports:
|
||||||
|
- "22:22" # SSH Git
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.gitea.rule=Host(`git.seizeconsulting.com`)"
|
||||||
|
- "traefik.http.routers.gitea.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.gitea.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
- backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration SSO côté Gitea (après premier login)
|
||||||
|
|
||||||
|
1. Aller sur `https://git.seizeconsulting.com` → setup initial
|
||||||
|
2. Créer un compte admin Gitea (local, sera utilisé pour configurer le reste)
|
||||||
|
3. Site Administration → Authentication Sources → Add :
|
||||||
|
- Type : **OAuth2**
|
||||||
|
- Authentication Name : `Zitadel`
|
||||||
|
- OAuth2 Provider : **OpenID Connect**
|
||||||
|
- Client ID : `<de Zitadel>`
|
||||||
|
- Client Secret : `<de Zitadel>`
|
||||||
|
- OpenID Connect Auto Discovery URL : `https://sso.seizeconsulting.com/.well-known/openid-configuration`
|
||||||
|
- Skip Local 2FA : ✅ (Zitadel gère MFA)
|
||||||
|
4. **Save**
|
||||||
|
|
||||||
|
### Configuration côté Zitadel
|
||||||
|
|
||||||
|
Créer une app dans Projects → Infra Seize → Applications :
|
||||||
|
- Type : Web
|
||||||
|
- Authentication Method : Basic
|
||||||
|
- Grant Type : Authorization Code
|
||||||
|
- Redirect URL : `https://git.seizeconsulting.com/user/oauth2/Zitadel/callback`
|
||||||
|
- Post-logout Redirect : `https://git.seizeconsulting.com/`
|
||||||
|
- Token Settings : JWT + User profile info dans ID Token
|
||||||
|
|
||||||
|
### Caractéristiques
|
||||||
|
|
||||||
|
- ✅ **Auto-registration** : à la 1ère connexion via SSO, le compte Gitea est auto-créé
|
||||||
|
- ✅ **Username = trigramme** (lu depuis `preferred_username` Zitadel)
|
||||||
|
- ❌ **Registration manuelle désactivée** : SSO obligatoire pour les nouveaux users
|
||||||
|
|
||||||
|
### Rôles RBAC
|
||||||
|
|
||||||
|
Gitea n'utilise **pas** les rôles Zitadel directement. Les permissions Gitea sont gérées en interne (organizations, teams, repos). Pour l'instant, **tous les users ont accès en lecture** sur les repos publics.
|
||||||
|
|
||||||
|
À configurer plus tard : créer des **organizations** par BU et donner accès en fonction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ n8n
|
||||||
|
|
||||||
|
### docker-compose.yml (résumé)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
n8n:
|
||||||
|
image: n8nio/n8n:latest
|
||||||
|
container_name: n8n
|
||||||
|
environment:
|
||||||
|
DB_TYPE: postgresdb
|
||||||
|
DB_POSTGRESDB_HOST: postgres
|
||||||
|
DB_POSTGRESDB_PORT: 5432
|
||||||
|
DB_POSTGRESDB_DATABASE: n8n
|
||||||
|
DB_POSTGRESDB_USER: n8n
|
||||||
|
DB_POSTGRESDB_PASSWORD: ${N8N_DB_PASSWORD}
|
||||||
|
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
|
||||||
|
N8N_HOST: n8n.seizeconsulting.com
|
||||||
|
N8N_PORT: 5678
|
||||||
|
N8N_PROTOCOL: https
|
||||||
|
WEBHOOK_URL: https://n8n.seizeconsulting.com/
|
||||||
|
# ⚠️ Pas de labels Traefik ici : c'est OAuth2-Proxy qui est exposé
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
- backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Particularités
|
||||||
|
|
||||||
|
- ⚠️ **N8N_ENCRYPTION_KEY** : critique pour décrypter les workflows et credentials. **À sauvegarder absolument**.
|
||||||
|
- L'auth n8n native est désactivée car OAuth2-Proxy fait l'auth en amont.
|
||||||
|
- Les **workflows** sont stockés dans Postgres (base `n8n`).
|
||||||
|
|
||||||
|
### Limitation RBAC
|
||||||
|
|
||||||
|
n8n CE (Community Edition) **ne supporte pas le RBAC granulaire** :
|
||||||
|
- Tous les users authentifiés ont accès à TOUS les workflows
|
||||||
|
- Pas de notion de "team" ou "project" en CE
|
||||||
|
|
||||||
|
→ C'est pourquoi le RBAC se fait **côté intranet** (cacher la carte aux non-autorisés). Voir [DECISIONS.md D7](../DECISIONS.md).
|
||||||
|
|
||||||
|
Pour vrai RBAC :
|
||||||
|
- Soit migrer vers **Activepieces** (gratuit, multi-tenant natif)
|
||||||
|
- Soit acheter **n8n Enterprise** (~12k€/an)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 Portainer
|
||||||
|
|
||||||
|
### docker-compose.yml (résumé)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
portainer:
|
||||||
|
image: portainer/portainer-ce:latest
|
||||||
|
container_name: portainer
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock # ⚠️ Accès complet à Docker !
|
||||||
|
- ./data:/data
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Particularités
|
||||||
|
|
||||||
|
- ⚠️ **Accès au socket Docker** : Portainer a un contrôle **total** sur tous les conteneurs du serveur. À protéger absolument derrière SSO.
|
||||||
|
- Auth Portainer native = créée au premier login, à protéger.
|
||||||
|
- **RBAC interne** dans Portainer CE = très limité.
|
||||||
|
|
||||||
|
### Limitation RBAC
|
||||||
|
|
||||||
|
Portainer CE :
|
||||||
|
- 1 seul utilisateur admin
|
||||||
|
- Pas d'organisations/teams
|
||||||
|
|
||||||
|
→ RBAC sur l'intranet uniquement. Si besoin de vrai RBAC : **Portainer Business** (~1800€/an) ou migration vers **Komodo**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Code Server
|
||||||
|
|
||||||
|
### docker-compose.yml (résumé)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
code-server:
|
||||||
|
image: lscr.io/linuxserver/code-server:latest
|
||||||
|
container_name: code-server
|
||||||
|
environment:
|
||||||
|
PUID: 1000
|
||||||
|
PGID: 1000
|
||||||
|
TZ: Europe/Paris
|
||||||
|
PASSWORD: ${CODE_PASSWORD} # Fallback si SSO down
|
||||||
|
SUDO_PASSWORD: ${CODE_PASSWORD}
|
||||||
|
DEFAULT_WORKSPACE: /config/workspace
|
||||||
|
volumes:
|
||||||
|
- ./config:/config
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Particularités
|
||||||
|
|
||||||
|
- VS Code dans le navigateur
|
||||||
|
- L'OAuth2-Proxy gère l'auth en amont, mais Code Server a aussi son propre mdp en fallback (`CODE_PASSWORD`)
|
||||||
|
- Workspaces partagés dans `./config/workspace/`
|
||||||
|
|
||||||
|
### Limitation RBAC
|
||||||
|
|
||||||
|
Pas de notion de "team" ou "workspace par user" en local. Tout user accède au même workspace.
|
||||||
|
|
||||||
|
→ Pour vrai RBAC : **OpenVSCode Server** (multi-user natif).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Uptime Kuma
|
||||||
|
|
||||||
|
### docker-compose.yml (résumé)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
uptime-kuma:
|
||||||
|
image: louislam/uptime-kuma:1
|
||||||
|
container_name: uptime-kuma
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Particularités
|
||||||
|
|
||||||
|
- Outil de monitoring (HTTP/TCP/Ping checks)
|
||||||
|
- Statuspages publiques possibles (mais on les laisse derrière SSO ici)
|
||||||
|
- 1 seul user admin
|
||||||
|
|
||||||
|
### Configuration recommandée
|
||||||
|
|
||||||
|
Une fois SSO en place, **désactiver le compte admin local** ou changer le mdp en quelque chose de très long (le SSO suffit).
|
||||||
|
|
||||||
|
### Limitation RBAC
|
||||||
|
|
||||||
|
Pareil que les autres : 1 seul user, pas de RBAC.
|
||||||
|
|
||||||
|
→ Pour vrai RBAC : **Gatus** (config-as-code, multi-tenant).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏠 Intranet (page d'accueil)
|
||||||
|
|
||||||
|
### docker-compose.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
intranet:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: intranet
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./index.html:/usr/share/nginx/html/index.html:ro
|
||||||
|
- ./logo.png:/usr/share/nginx/html/logo.png:ro
|
||||||
|
- ./roles-mapping.json:/usr/share/nginx/html/roles-mapping.json:ro
|
||||||
|
- ./nginx-rbac.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caractéristiques
|
||||||
|
|
||||||
|
- Simple nginx avec un HTML statique
|
||||||
|
- **Filtrage RBAC côté JavaScript** (voir [07-rbac-intranet.md](./07-rbac-intranet.md))
|
||||||
|
- Nginx fait du `sub_filter` pour injecter l'email user dans l'HTML
|
||||||
|
- L'HTML cache les cartes selon le rôle
|
||||||
|
|
||||||
|
### Fichiers critiques
|
||||||
|
|
||||||
|
| Fichier | Usage |
|
||||||
|
|---|---|
|
||||||
|
| `index.html` | Le HTML avec les cartes et le JavaScript RBAC |
|
||||||
|
| `roles-mapping.json` | Mapping `email → role` (généré par script Python) |
|
||||||
|
| `nginx-rbac.conf` | Config nginx avec `sub_filter` pour injecter `__USER_EMAIL__` |
|
||||||
|
| `logo.png` | Logo Seize |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ PostgreSQL (principal)
|
||||||
|
|
||||||
|
### docker-compose.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
volumes:
|
||||||
|
- ./data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Particularités
|
||||||
|
|
||||||
|
- Postgres **mutualisé** entre Gitea et n8n (databases séparées dans la même instance)
|
||||||
|
- Pas exposé publiquement (réseau `backend` uniquement)
|
||||||
|
- ⚠️ **Postgres Zitadel est séparé** (dans son propre docker-compose)
|
||||||
|
|
||||||
|
### Databases hébergées
|
||||||
|
|
||||||
|
| Database | Owner | Service |
|
||||||
|
|---|---|---|
|
||||||
|
| postgres | postgres | (admin) |
|
||||||
|
| gitea | gitea | Gitea |
|
||||||
|
| n8n | n8n | n8n |
|
||||||
|
| authentik | authentik | Authentik (obsolète) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Volumes critiques par app
|
||||||
|
|
||||||
|
| App | Volume | Quoi |
|
||||||
|
|---|---|---|
|
||||||
|
| Gitea | `./data` | Repos, config, attachments |
|
||||||
|
| Portainer | `./data` | Compose stacks, snapshots |
|
||||||
|
| Code Server | `./config` | Workspace, settings, plugins |
|
||||||
|
| Uptime Kuma | `./data` | Monitors, history, statuspages |
|
||||||
|
| Postgres | `./data` | Toutes les DBs (sauf Zitadel) |
|
||||||
|
| Zitadel | `./postgres-data` + `./machinekey` | Auth complète |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Ajouter une nouvelle app — checklist
|
||||||
|
|
||||||
|
Voir [OPERATIONS.md](../OPERATIONS.md) section "Ajouter un nouvel outil".
|
||||||
|
|
||||||
|
1. ✅ DNS record A `<app>.seizeconsulting.com`
|
||||||
|
2. ✅ Créer `~/docker/<app>/` avec docker-compose.yml
|
||||||
|
3. ✅ Créer l'app dans Zitadel (Project Infra Seize)
|
||||||
|
4. ✅ Récupérer Client ID + Secret
|
||||||
|
5. ✅ Créer OAuth2-Proxy dédié `~/docker/oauth2-proxy-<app>/`
|
||||||
|
6. ✅ Tester le login
|
||||||
|
7. ✅ Ajouter la carte dans `~/docker/intranet/index.html` avec `data-app="<app>"`
|
||||||
|
8. ✅ Ajouter la ligne dans la matrice RBAC du JavaScript
|
||||||
|
9. ✅ Restart intranet
|
||||||
|
10. ✅ Tester avec différents profils
|
||||||
435
docs/06-import-furious.md
Normal file
435
docs/06-import-furious.md
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
# 06 — Import des utilisateurs depuis Furious Squad
|
||||||
|
|
||||||
|
> Procédure complète qui a permis d'importer 88 collaborateurs Seize de Furious vers Zitadel le 5 juin 2026.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Objectif
|
||||||
|
|
||||||
|
Synchroniser les **88 collaborateurs Seize actifs** depuis Furious Squad (source de vérité RH) vers Zitadel (Identity Provider), avec :
|
||||||
|
- 1 compte Zitadel par collaborateur
|
||||||
|
- Metadata Furious préservées (job_title, manager, BU, etc.)
|
||||||
|
- Mots de passe temporaires générés
|
||||||
|
- Possibilité d'attribuer des rôles RBAC ensuite
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Architecture du process
|
||||||
|
|
||||||
|
```
|
||||||
|
Furious API (REST)
|
||||||
|
│
|
||||||
|
│ HTTP Basic Auth (FURIOUS_USERNAME:PASSWORD)
|
||||||
|
▼
|
||||||
|
read-all-users.sh ──────────► furious-users-raw.json
|
||||||
|
│
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
import-furious-to-zitadel.py
|
||||||
|
│
|
||||||
|
┌───────────────┼───────────────┐
|
||||||
|
│ │ │
|
||||||
|
dry-run test prod
|
||||||
|
(rien) (3 users) (85 users)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Zitadel API v2 ──── service account
|
||||||
|
│ furious-import-service
|
||||||
|
▼
|
||||||
|
88 users créés
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
users-credentials.csv (mdps temporaires)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
distribution-teams.csv (3 colonnes)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Distribution manuelle via Teams
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Structure des scripts
|
||||||
|
|
||||||
|
```
|
||||||
|
~/furious-to-zitadel/
|
||||||
|
├── .env # FURIOUS_USERNAME, PASSWORD, etc.
|
||||||
|
├── venv/ # Environnement Python
|
||||||
|
├── zitadelfuriousimportkey.json # ⚠️ Clé service account (chmod 600)
|
||||||
|
│
|
||||||
|
├── read-all-users.sh # Lecture Furious → JSON
|
||||||
|
├── test-auth.sh # Test connexion API Furious
|
||||||
|
├── test-1-user.sh # Test lecture d'un user spécifique
|
||||||
|
│
|
||||||
|
├── import-furious-to-zitadel.py # ⭐ Script principal d'import
|
||||||
|
├── merge-duplicates.py # Fusion des doublons (HKA, PSP, ...)
|
||||||
|
├── reset-test-users-password.py # Reset mdp des users de test
|
||||||
|
├── extract-distribution-csv.py # Génère CSV pour distribution
|
||||||
|
├── generate-roles-mapping.py # Génère JSON mapping email → rôle
|
||||||
|
├── assign-roles.py # Attribue les rôles dans Zitadel
|
||||||
|
│
|
||||||
|
├── furious-users-raw.json # Données brutes Furious
|
||||||
|
├── users-credentials.csv # 89 lignes avec mdps
|
||||||
|
├── distribution-teams.csv # 88 lignes pour Teams
|
||||||
|
├── roles-mapping.json # 87 mappings email → rôle
|
||||||
|
└── import.log # Logs d'exécution
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Configuration `.env`
|
||||||
|
|
||||||
|
```env
|
||||||
|
FURIOUS_USERNAME=<creer_dans_furious_config>
|
||||||
|
FURIOUS_PASSWORD=<creer_dans_furious_config>
|
||||||
|
FURIOUS_API_URL=https://seize-consulting.furious-squad.com/api/v2
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ `chmod 600 .env`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Création du compte API Furious
|
||||||
|
|
||||||
|
### 1. Connexion Furious
|
||||||
|
|
||||||
|
Connexion à `https://seize-consulting.furious-squad.com` avec un compte admin.
|
||||||
|
|
||||||
|
### 2. Configuration → Générer un utilisateur d'API
|
||||||
|
|
||||||
|
- Nom : `import-zitadel` (ou similaire)
|
||||||
|
- Permissions :
|
||||||
|
- ☑️ User > Search (lecture seule)
|
||||||
|
- ❌ Tout le reste (principe du moindre privilège)
|
||||||
|
- Generate
|
||||||
|
|
||||||
|
### 3. Récupérer les identifiants
|
||||||
|
|
||||||
|
Format identifiant : `<nom>.<6chiffres>` (exemple : `import-zitadel.123456`)
|
||||||
|
+ Mot de passe généré aléatoirement.
|
||||||
|
|
||||||
|
⚠️ **Sauvegarder immédiatement dans le gestionnaire de mots de passe.**
|
||||||
|
|
||||||
|
### 4. Tester
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/furious-to-zitadel
|
||||||
|
source venv/bin/activate
|
||||||
|
./test-auth.sh
|
||||||
|
# Doit retourner un JSON avec une réponse valide (pas 401/403)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 Création du service account Zitadel
|
||||||
|
|
||||||
|
### 1. Console Zitadel → Users → New
|
||||||
|
|
||||||
|
- **Type** : Service user
|
||||||
|
- **Username** : `furious-import-service`
|
||||||
|
- **Access Token Type** : **JWT** (important !)
|
||||||
|
|
||||||
|
### 2. Ajouter au rôle IAM_USER_MANAGER
|
||||||
|
|
||||||
|
- Aller dans Default Settings (top-right) → Administrators → New
|
||||||
|
- Sélectionner le service user `furious-import-service`
|
||||||
|
- Cocher **IAM_USER_MANAGER**
|
||||||
|
- ❌ Ne PAS cocher IAM_OWNER
|
||||||
|
|
||||||
|
### 3. Générer la clé JSON
|
||||||
|
|
||||||
|
- Profil du service user → Keys → New
|
||||||
|
- Type : **JSON**
|
||||||
|
- Expiration : 1 an (ou jamais selon besoin)
|
||||||
|
- **Download immediately** → fichier `<userId>.json`
|
||||||
|
- ⚠️ La clé n'est jamais re-affichée
|
||||||
|
|
||||||
|
### 4. Transférer sur le serveur
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis le PC (Windows PowerShell ou Linux)
|
||||||
|
scp 375992380685287427.json hedi@<server>:~/furious-to-zitadel/zitadelfuriousimportkey.json
|
||||||
|
|
||||||
|
# Sur le serveur
|
||||||
|
ssh hedi@<server>
|
||||||
|
chmod 600 ~/furious-to-zitadel/zitadelfuriousimportkey.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐍 Environnement Python
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/furious-to-zitadel
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install requests pyjwt cryptography
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Procédure d'import — pas à pas
|
||||||
|
|
||||||
|
### Étape 1 — Lire les users depuis Furious
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/furious-to-zitadel
|
||||||
|
source venv/bin/activate
|
||||||
|
./read-all-users.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Génère `furious-users-raw.json` (29 Ko, 94 users dont 88 actifs).
|
||||||
|
|
||||||
|
### Étape 2 — Test d'authentification Zitadel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test que la clé Zitadel marche
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
data = json.load(open('zitadelfuriousimportkey.json'))
|
||||||
|
print('UserId:', data.get('userId'))
|
||||||
|
print('KeyId:', data.get('keyId'))
|
||||||
|
print('Type:', data.get('type'))
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étape 3 — Dry-run (simulation, aucune écriture)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 import-furious-to-zitadel.py --mode dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Affiche **ce que ferait** le script sans rien créer.
|
||||||
|
|
||||||
|
Doit afficher :
|
||||||
|
```
|
||||||
|
[DRY-RUN] Would create: yves.canauxdebonfils@seizeconsulting.com (Yves Canaux de Bonfils)
|
||||||
|
[DRY-RUN] Would create: oussama.azzabi@seizeconsulting.com (Oussama Azzabi)
|
||||||
|
...
|
||||||
|
Total : 88 users would be created
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étape 4 — Test sur 3 users
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 import-furious-to-zitadel.py --mode test
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Crée **3 users** dans Zitadel (les furious_id 1, 2, 3).
|
||||||
|
|
||||||
|
**Vérification dans Zitadel UI** : Console → Users → vérifier que les 3 users sont créés avec leurs metadata.
|
||||||
|
|
||||||
|
### Étape 5 — Import production
|
||||||
|
|
||||||
|
⚠️ **Étape sensible — ne pas interrompre.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 import-furious-to-zitadel.py --mode prod | tee -a import.log
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Crée les **85 users restants**. Durée : ~2 minutes.
|
||||||
|
|
||||||
|
Logs en temps réel + sauvegarde dans `import.log`.
|
||||||
|
|
||||||
|
### Étape 6 — Vérification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compter les users créés dans Zitadel via API
|
||||||
|
python3 -c "
|
||||||
|
# Voir le script pour récupérer le count
|
||||||
|
"
|
||||||
|
|
||||||
|
# Compter les lignes du CSV de credentials
|
||||||
|
wc -l users-credentials.csv
|
||||||
|
# Doit afficher : 89 (1 header + 88 users)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔀 Gestion des doublons
|
||||||
|
|
||||||
|
### Contexte
|
||||||
|
|
||||||
|
4 anciens admins existaient déjà dans Zitadel avant l'import :
|
||||||
|
- HKA (Hedi Kalboussi) → ID `375119988404518915`
|
||||||
|
- PSP (Philippe Spoljaric) → ID `375120224527056899`
|
||||||
|
- PYT (Pierre-Yves Tachon) → ID `375778940960112643`
|
||||||
|
- Jules (Jules Bataille) → ID `375777592659148803`
|
||||||
|
|
||||||
|
L'import a créé des **doublons** avec leurs emails complets.
|
||||||
|
|
||||||
|
### Stratégie
|
||||||
|
|
||||||
|
Voir [DECISIONS.md D5](../DECISIONS.md). En bref :
|
||||||
|
- **Garder l'ancien** compte (avec MFA configuré)
|
||||||
|
- **Supprimer le nouveau** doublon
|
||||||
|
- **Copier les metadata Furious** du nouveau vers l'ancien (pour pas perdre l'info)
|
||||||
|
|
||||||
|
### Exécution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Éditer le script merge-duplicates.py pour adapter le mapping
|
||||||
|
nano merge-duplicates.py
|
||||||
|
|
||||||
|
# Configurer la variable DUPLICATES :
|
||||||
|
DUPLICATES = [
|
||||||
|
{
|
||||||
|
"keep_user_id": "375119988404518915", # HKA ancien
|
||||||
|
"delete_email": "hedi.kalboussi@seizeconsulting.com",
|
||||||
|
"furious_id": 5,
|
||||||
|
},
|
||||||
|
# ... 3 autres
|
||||||
|
]
|
||||||
|
|
||||||
|
# 2. Lancer
|
||||||
|
python3 merge-duplicates.py
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Pour chaque doublon :
|
||||||
|
1. Lit le user "nouveau" via email
|
||||||
|
2. Copie ses metadata sur l'ancien
|
||||||
|
3. Supprime le nouveau
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 Attribution des rôles RBAC
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
|
||||||
|
Avant d'attribuer des rôles, créer les 8 rôles dans Zitadel (Project Infra Seize).
|
||||||
|
|
||||||
|
Voir [07-rbac-intranet.md](./07-rbac-intranet.md) pour la création des rôles.
|
||||||
|
|
||||||
|
### Mapping job_title → rôle
|
||||||
|
|
||||||
|
Voir `matrices/job-title-to-role.csv` pour la matrice complète. Résumé :
|
||||||
|
|
||||||
|
```python
|
||||||
|
JOB_TITLE_TO_ROLE = {
|
||||||
|
"président": "direction",
|
||||||
|
"directeur": "direction",
|
||||||
|
"directeur associé": "direction",
|
||||||
|
"senior manager": "manager",
|
||||||
|
"sénior manager": "manager",
|
||||||
|
"manager": "manager",
|
||||||
|
"manager bi": "manager",
|
||||||
|
"expert": "expert",
|
||||||
|
"expert n2": "expert",
|
||||||
|
"consultant expert niveau 1": "expert",
|
||||||
|
"consultant sénior": "consultant_senior",
|
||||||
|
"consultant expérimenté": "consultant_senior",
|
||||||
|
"consultant expérimentée": "consultant_senior",
|
||||||
|
"consultant junior": "consultant_junior",
|
||||||
|
"stagiaire": "stagiaire",
|
||||||
|
"assistante": "support",
|
||||||
|
# ... etc
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cas spécial : status="apprenti" → role="alternant"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exécution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Générer le mapping email → role
|
||||||
|
python3 generate-roles-mapping.py
|
||||||
|
|
||||||
|
# 2. Attribuer les rôles dans Zitadel
|
||||||
|
python3 assign-roles.py
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Le script :
|
||||||
|
1. Lit chaque user actif Furious
|
||||||
|
2. Détermine son rôle (via mapping job_title ou status)
|
||||||
|
3. Cherche le user dans Zitadel par email
|
||||||
|
4. Lui attribue le rôle via API `POST /v1/users/{id}/grants`
|
||||||
|
|
||||||
|
**Vérification** : Console Zitadel → Users → 1 user → Authorizations → doit voir le rôle attribué.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Format CSV produit
|
||||||
|
|
||||||
|
### `users-credentials.csv` (sensible, à protéger)
|
||||||
|
|
||||||
|
```csv
|
||||||
|
email,prenom,nom,trigramme,furious_id,zitadel_user_id,mdp_temporaire
|
||||||
|
yves.canauxdebonfils@seizeconsulting.com,Yves,Canaux de Bonfils,YCB,1,375990xxxxxxxxxxx,Y8sR3kp9XmF2
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `distribution-teams.csv` (pour distribution mdp)
|
||||||
|
|
||||||
|
```csv
|
||||||
|
Nom complet,Email,Mot de passe
|
||||||
|
Yves Canaux de Bonfils,yves.canauxdebonfils@seizeconsulting.com,Y8sR3kp9XmF2
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Anti-patterns à éviter
|
||||||
|
|
||||||
|
1. ❌ **Ne PAS sauter le dry-run** : permet de détecter des erreurs avant impact prod
|
||||||
|
2. ❌ **Ne PAS importer avec rôles directement** : importer d'abord, attribuer ensuite (cf. D6)
|
||||||
|
3. ❌ **Ne PAS donner IAM_OWNER au service account** : utiliser IAM_USER_MANAGER
|
||||||
|
4. ❌ **Ne PAS oublier de supprimer le CSV** après distribution des mdps
|
||||||
|
5. ❌ **Ne PAS coller les identifiants Furious** dans une conversation IA
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Synchronisation périodique (à mettre en place)
|
||||||
|
|
||||||
|
Aujourd'hui, l'import est **manuel** (one-shot du 5 juin). À l'avenir :
|
||||||
|
|
||||||
|
### Option recommandée : workflow n8n quotidien
|
||||||
|
|
||||||
|
Workflow n8n à 3h du matin :
|
||||||
|
1. **HTTP Request** vers Furious : récupérer la liste des users
|
||||||
|
2. **HTTP Request** vers Zitadel : récupérer la liste des users actuels
|
||||||
|
3. **Code node** : calculer le diff
|
||||||
|
4. **Switch** :
|
||||||
|
- Nouveau dans Furious → POST Zitadel pour créer
|
||||||
|
- Départ dans Furious (departure_date != "0000-00-00") → PATCH Zitadel pour désactiver
|
||||||
|
- Changement job_title → PATCH metadata + réattribuer rôle
|
||||||
|
5. **Slack/Teams notification** : résumé du sync
|
||||||
|
|
||||||
|
⏱️ Estimation : 1 journée de dev n8n.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting import
|
||||||
|
|
||||||
|
### "Erreur 401 lors de l'auth Zitadel"
|
||||||
|
|
||||||
|
Causes possibles :
|
||||||
|
1. Clé JSON corrompue → re-télécharger depuis Zitadel
|
||||||
|
2. Service account n'a plus le rôle IAM_USER_MANAGER → re-attribuer
|
||||||
|
3. Token JWT mal généré → vérifier que `pyjwt` est bien installé
|
||||||
|
|
||||||
|
### "User déjà existant" lors de l'import
|
||||||
|
|
||||||
|
Normal pour les doublons. Le script `import-furious-to-zitadel.py` skip les users existants par défaut.
|
||||||
|
|
||||||
|
### "Erreur 400 sur certains users"
|
||||||
|
|
||||||
|
Causes courantes :
|
||||||
|
1. **Email invalide** (espace, caractères spéciaux) dans Furious → corriger dans Furious
|
||||||
|
2. **Mot de passe ne respecte pas la policy** → le script génère mdp 16 chars random
|
||||||
|
3. **Username déjà pris** → choisir un autre format (email complet par exemple)
|
||||||
|
|
||||||
|
### "Le script lit les bonnes données mais Zitadel ne les voit pas"
|
||||||
|
|
||||||
|
Vérifier la **réponse HTTP** retournée par chaque création :
|
||||||
|
```bash
|
||||||
|
python3 import-furious-to-zitadel.py --mode test --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Voir aussi
|
||||||
|
|
||||||
|
- [07-rbac-intranet.md](./07-rbac-intranet.md) — Suite de l'import : attribution des rôles RBAC
|
||||||
|
- [03-zitadel-setup.md](./03-zitadel-setup.md) — Création du service account Zitadel
|
||||||
|
- [../matrices/job-title-to-role.csv](../matrices/job-title-to-role.csv) — Matrice de mapping
|
||||||
403
docs/07-rbac-intranet.md
Normal file
403
docs/07-rbac-intranet.md
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
# 07 — RBAC sur l'intranet (Approche A)
|
||||||
|
|
||||||
|
> Comment l'intranet filtre les cartes affichées selon le rôle du user.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Principe
|
||||||
|
|
||||||
|
**Approche A** : le filtrage RBAC se fait **côté intranet uniquement**, pas sur chaque outil.
|
||||||
|
|
||||||
|
### Comment ça marche
|
||||||
|
|
||||||
|
```
|
||||||
|
1. User se logge sur intranet.seizeconsulting.com
|
||||||
|
│
|
||||||
|
│ OAuth2-Proxy fait l'auth Zitadel
|
||||||
|
▼
|
||||||
|
2. nginx reçoit la requête avec header X-Forwarded-Email
|
||||||
|
│
|
||||||
|
│ sub_filter remplace __USER_EMAIL__ par hedi.kalboussi@...
|
||||||
|
▼
|
||||||
|
3. Browser reçoit l'HTML avec const USER_EMAIL = "hedi.kalboussi@..."
|
||||||
|
│
|
||||||
|
│ JavaScript fetch /roles-mapping.json
|
||||||
|
▼
|
||||||
|
4. JS détermine le rôle : "consultant_junior"
|
||||||
|
│
|
||||||
|
│ JS itère sur les cartes <a class="service-card" data-app="...">
|
||||||
|
▼
|
||||||
|
5. JS cache les cartes que ce rôle n'a pas le droit de voir
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
6. User voit uniquement Gitea + Code Server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Limite acceptée
|
||||||
|
|
||||||
|
⚠️ **Un user qui connaît `n8n.seizeconsulting.com` peut bypass le filtre** (l'outil reste accessible via URL directe). C'est acceptable pour Seize (88 employés non-malveillants). Pour un vrai RBAC, voir [DECISIONS.md D7](../DECISIONS.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Structure des fichiers
|
||||||
|
|
||||||
|
```
|
||||||
|
~/docker/intranet/
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── nginx-rbac.conf # ⭐ Config nginx avec sub_filter
|
||||||
|
├── index.html # ⭐ HTML avec data-app + JS RBAC
|
||||||
|
├── roles-mapping.json # ⭐ Mapping email → rôle (généré)
|
||||||
|
└── logo.png
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Config nginx (nginx-rbac.conf)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Désactiver le cache pour l'HTML
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||||
|
|
||||||
|
# ⭐ Substituer __USER_EMAIL__ par la vraie valeur du header HTTP
|
||||||
|
sub_filter '__USER_EMAIL__' '$http_x_forwarded_email';
|
||||||
|
sub_filter_once off;
|
||||||
|
sub_filter_types text/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pareil pour la racine /
|
||||||
|
location = / {
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||||
|
sub_filter '__USER_EMAIL__' '$http_x_forwarded_email';
|
||||||
|
sub_filter_once off;
|
||||||
|
sub_filter_types text/html;
|
||||||
|
try_files /index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Le mapping JSON doit être servi normalement
|
||||||
|
location = /roles-mapping.json {
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tout le reste (logo, favicon, etc.)
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pourquoi sub_filter ?
|
||||||
|
|
||||||
|
- `$http_x_forwarded_email` lit le header HTTP envoyé par OAuth2-Proxy
|
||||||
|
- `sub_filter` substitue `__USER_EMAIL__` par cette valeur dans le HTML servi
|
||||||
|
- `sub_filter_once off` : remplace **toutes les occurrences** (pas juste la 1ère)
|
||||||
|
- `Cache-Control: no-store` : crucial pour que le HTML soit re-généré à chaque requête (sinon un user reçoit l'email d'un autre)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 HTML — Structure de chaque carte
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a class="service-card"
|
||||||
|
data-app="portainer" ← ⭐ ATTRIBUT RBAC
|
||||||
|
data-color="teal"
|
||||||
|
href="https://portainer.seizeconsulting.com"
|
||||||
|
target="_blank" rel="noopener">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon"><!-- SVG --></div>
|
||||||
|
<div class="card-arrow"><!-- SVG --></div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-title">Portainer</div>
|
||||||
|
<div class="card-desc">Console de gestion des conteneurs Docker.</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span class="card-url">portainer.seizeconsulting.com</span>
|
||||||
|
<span class="card-tag">Infra</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Chaque carte a un attribut **`data-app="<nom>"`** que le JavaScript utilise pour la matcher avec la matrice RBAC.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 JavaScript RBAC (à la fin de index.html)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
/* ===== RBAC INTRANET — Filtrage cartes selon rôle Zitadel ===== */
|
||||||
|
(async function() {
|
||||||
|
const USER_EMAIL = "__USER_EMAIL__"; // ← Remplacé par nginx sub_filter
|
||||||
|
|
||||||
|
/* Matrice RBAC : qui voit quelle carte */
|
||||||
|
const ACCESS_MATRIX = {
|
||||||
|
"portainer": ["direction", "manager"],
|
||||||
|
"status": ["direction"],
|
||||||
|
"n8n": ["direction", "manager", "expert"],
|
||||||
|
"gitea": ["direction", "manager", "expert", "consultant_senior", "consultant_junior", "alternant", "stagiaire", "support"],
|
||||||
|
"code": ["direction", "manager", "expert", "consultant_senior", "consultant_junior", "alternant", "stagiaire", "support"],
|
||||||
|
"traefik": ["direction"]
|
||||||
|
};
|
||||||
|
|
||||||
|
let userRole = null;
|
||||||
|
try {
|
||||||
|
const response = await fetch("/roles-mapping.json", { cache: "no-store" });
|
||||||
|
if (response.ok) {
|
||||||
|
const mapping = await response.json();
|
||||||
|
userRole = mapping[USER_EMAIL.toLowerCase()] || null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[RBAC] Erreur chargement mapping:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[RBAC] User:", USER_EMAIL, "| Rôle:", userRole);
|
||||||
|
|
||||||
|
/* Filtrer les cartes selon le rôle */
|
||||||
|
document.querySelectorAll(".service-card[data-app]").forEach(card => {
|
||||||
|
const app = card.dataset.app;
|
||||||
|
const allowedRoles = ACCESS_MATRIX[app] || [];
|
||||||
|
if (!userRole || !allowedRoles.includes(userRole)) {
|
||||||
|
card.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 Création des 8 rôles dans Zitadel
|
||||||
|
|
||||||
|
### Étape 1 — Aller dans le project
|
||||||
|
|
||||||
|
Console Zitadel → Projects → **Infra Seize** → Roles → New
|
||||||
|
|
||||||
|
### Étape 2 — Créer un par un
|
||||||
|
|
||||||
|
| Key | Display Name | Group |
|
||||||
|
|---|---|---|
|
||||||
|
| `direction` | Direction | hierarchie |
|
||||||
|
| `manager` | Manager | hierarchie |
|
||||||
|
| `expert` | Expert | hierarchie |
|
||||||
|
| `consultant_senior` | Consultant Senior | hierarchie |
|
||||||
|
| `consultant_junior` | Consultant Junior | hierarchie |
|
||||||
|
| `alternant` | Alternant | hierarchie |
|
||||||
|
| `stagiaire` | Stagiaire | hierarchie |
|
||||||
|
| `support` | Support | hierarchie |
|
||||||
|
|
||||||
|
### Étape 3 — Activer "Return user roles during authentication"
|
||||||
|
|
||||||
|
Console Zitadel → Projects → **Infra Seize** → General → Settings :
|
||||||
|
- ☑️ **Return user roles during authentication**
|
||||||
|
- ❌ **Only authorized users can authenticate** (à laisser DÉCOCHÉ — cf. [D9](../DECISIONS.md))
|
||||||
|
- ❌ **Authentication restricted to authorized orgs**
|
||||||
|
|
||||||
|
### Étape 4 — Configurer chaque App pour transmettre les rôles
|
||||||
|
|
||||||
|
Pour chaque application (Intranet, n8n, Portainer, etc.) :
|
||||||
|
|
||||||
|
Console Zitadel → Projects → Infra Seize → Applications → `<App>` → Token Settings :
|
||||||
|
- Auth Token Type : **JWT** (pas Bearer Token)
|
||||||
|
- ☑️ User roles inside ID Token
|
||||||
|
- ☑️ Include user's profile info in the ID Token
|
||||||
|
- ☑️ Add user roles to the access token
|
||||||
|
|
||||||
|
**Save**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Matrice RBAC effective
|
||||||
|
|
||||||
|
| Outil | direction | manager | expert | cons_sr | cons_jr | alternant | stagiaire | support |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| **Intranet** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Gitea** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Code Server** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **n8n** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| **Portainer** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| **Uptime Kuma (Status)** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| **Traefik Dashboard** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
|
||||||
|
Voir aussi `matrices/rbac-matrix.md` pour la vue détaillée.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 OAuth2-Proxy — Activer le scope des rôles
|
||||||
|
|
||||||
|
Modifier le `.env` de **chaque** OAuth2-Proxy pour demander le scope des rôles :
|
||||||
|
|
||||||
|
```env
|
||||||
|
OAUTH2_PROXY_SCOPE=openid email profile urn:zitadel:iam:org:project:roles
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis restart :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/docker/oauth2-proxy-<outil>
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎬 Workflow complet : du recrutement au compte fonctionnel
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Nouveau collaborateur arrive
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
2. RH crée la fiche dans Furious Squad (job_title, manager, BU, etc.)
|
||||||
|
│
|
||||||
|
│ Le jour J ou J+1
|
||||||
|
▼
|
||||||
|
3. Sync Furious → Zitadel
|
||||||
|
- Soit manuel : python3 import-furious-to-zitadel.py
|
||||||
|
- Soit auto : workflow n8n (à mettre en place)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
4. Génération du mapping : python3 generate-roles-mapping.py
|
||||||
|
→ roles-mapping.json mis à jour
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
5. Copie vers l'intranet : cp roles-mapping.json ~/docker/intranet/
|
||||||
|
→ nginx ne nécessite pas de restart (volume monté)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
6. Attribution du rôle : python3 assign-roles.py
|
||||||
|
→ Le user reçoit son rôle dans Zitadel
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
7. Distribution du mdp temporaire via Teams
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
8. User se logge → voit les bonnes cartes selon son rôle
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting RBAC
|
||||||
|
|
||||||
|
### "Toutes les cartes sont visibles, le filtrage ne marche pas"
|
||||||
|
|
||||||
|
#### Vérification 1 : le sub_filter nginx fonctionne ?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis le serveur, vérifier que le HTML rendu contient le bon email
|
||||||
|
docker exec intranet sh -c "curl -s -H 'X-Forwarded-Email: test@seizeconsulting.com' http://localhost/ | grep 'USER_EMAIL'"
|
||||||
|
# Doit afficher : const USER_EMAIL = "test@seizeconsulting.com";
|
||||||
|
```
|
||||||
|
|
||||||
|
Si le HTML contient encore `__USER_EMAIL__` :
|
||||||
|
- Vérifier que `nginx-rbac.conf` est bien monté dans le conteneur
|
||||||
|
- Vérifier les logs nginx : `docker logs intranet`
|
||||||
|
|
||||||
|
#### Vérification 2 : roles-mapping.json est chargé ?
|
||||||
|
|
||||||
|
Ouvrir la console JS du browser (F12) :
|
||||||
|
```
|
||||||
|
[RBAC] User: xxx | Rôle: xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
Si Rôle = `null` :
|
||||||
|
- Vérifier que l'email est bien dans roles-mapping.json
|
||||||
|
- Attention à la casse (le script fait `.toLowerCase()`)
|
||||||
|
|
||||||
|
#### Vérification 3 : la matrice JS est correcte ?
|
||||||
|
|
||||||
|
Dans la console :
|
||||||
|
```js
|
||||||
|
document.querySelectorAll('.service-card[data-app]').forEach(c => console.log(c.dataset.app, c.style.display));
|
||||||
|
```
|
||||||
|
|
||||||
|
Doit afficher chaque carte avec son `display: none` ou `display: ""`.
|
||||||
|
|
||||||
|
### "Le user voit __USER_EMAIL__ littéralement"
|
||||||
|
|
||||||
|
Cause : nginx ne fait pas le sub_filter.
|
||||||
|
|
||||||
|
Solutions :
|
||||||
|
1. Vérifier que `nginx-rbac.conf` est bien monté en RO sur `/etc/nginx/nginx.conf` (pas un autre chemin)
|
||||||
|
2. Vérifier que le header `X-Forwarded-Email` arrive bien (debug : tail logs nginx)
|
||||||
|
3. Restart : `docker compose down && docker compose up -d`
|
||||||
|
|
||||||
|
### "Le user voit ses cartes mais quand il clique sur une carte non-autorisée, il y accède quand même"
|
||||||
|
|
||||||
|
C'est **normal** et c'est la limite acceptée de l'Approche A.
|
||||||
|
|
||||||
|
Pour vrai RBAC, voir [DECISIONS.md D7](../DECISIONS.md).
|
||||||
|
|
||||||
|
### "Un user vient d'être ajouté dans Furious mais ne voit aucune carte"
|
||||||
|
|
||||||
|
Causes :
|
||||||
|
1. roles-mapping.json pas à jour → relancer `generate-roles-mapping.py` + copier
|
||||||
|
2. User pas encore dans Zitadel → relancer l'import
|
||||||
|
3. job_title pas dans le mapping → ajouter dans `JOB_TITLE_TO_ROLE` du script
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Mise à jour de la matrice RBAC
|
||||||
|
|
||||||
|
### Cas 1 : Ajouter un nouvel outil
|
||||||
|
|
||||||
|
1. Ajouter la carte HTML dans `index.html` avec `data-app="newapp"`
|
||||||
|
2. Ajouter la ligne dans `ACCESS_MATRIX` :
|
||||||
|
```javascript
|
||||||
|
"newapp": ["direction", "manager"],
|
||||||
|
```
|
||||||
|
3. Restart : `cd ~/docker/intranet && docker compose restart`
|
||||||
|
|
||||||
|
### Cas 2 : Changer les autorisations d'un outil
|
||||||
|
|
||||||
|
1. Modifier la liste de rôles dans `ACCESS_MATRIX` du JS
|
||||||
|
2. Restart : `docker compose restart`
|
||||||
|
3. Tester avec différents profils
|
||||||
|
|
||||||
|
### Cas 3 : Ajouter un nouveau rôle
|
||||||
|
|
||||||
|
1. Créer le rôle dans Zitadel (Projects → Roles → New)
|
||||||
|
2. Mettre à jour le mapping dans `assign-roles.py` (variable `JOB_TITLE_TO_ROLE`)
|
||||||
|
3. Mettre à jour `ACCESS_MATRIX` dans le JS pour préciser ce que ce rôle voit
|
||||||
|
4. Lancer `generate-roles-mapping.py` + `assign-roles.py`
|
||||||
|
5. Copier `roles-mapping.json` vers l'intranet
|
||||||
|
6. Restart intranet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Personnalisation visuelle
|
||||||
|
|
||||||
|
Le CSS de l'intranet est dans `<style>` au début d'`index.html`. Couleurs principales :
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--c-bg-deep: #071C1C; /* Fond sombre */
|
||||||
|
--c-accent: #008A81; /* Vert Seize */
|
||||||
|
--c-accent-bright: #00C9B7;
|
||||||
|
--c-mint: #A9D4D3;
|
||||||
|
--c-lime: #F3FBBA;
|
||||||
|
--c-cream: #FBFBF4;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour changer le logo : remplacer `~/docker/intranet/logo.png`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Voir aussi
|
||||||
|
|
||||||
|
- [06-import-furious.md](./06-import-furious.md) — Import des users (étape préalable)
|
||||||
|
- [04-oauth2-proxy-pattern.md](./04-oauth2-proxy-pattern.md) — Comment OAuth2-Proxy transmet les headers
|
||||||
|
- [DECISIONS.md D7](../DECISIONS.md) — Pourquoi Approche A
|
||||||
|
- [matrices/rbac-matrix.md](../matrices/rbac-matrix.md) — Matrice complète
|
||||||
|
- [matrices/job-title-to-role.csv](../matrices/job-title-to-role.csv) — Mapping Furious → rôle
|
||||||
314
docs/08-secrets-management.md
Normal file
314
docs/08-secrets-management.md
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
# 08 — Gestion des secrets
|
||||||
|
|
||||||
|
> Bonnes pratiques pour les secrets de l'infrastructure : où ils sont, comment les rotater, et comment ne pas les exposer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Inventaire des secrets
|
||||||
|
|
||||||
|
### Secrets stockés sur le serveur
|
||||||
|
|
||||||
|
| Type | Localisation | Criticité | Rotation recommandée |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Mdp Postgres principal | `~/docker/postgres/.env` | 🔴 Critique | 6 mois |
|
||||||
|
| Mdp Postgres Zitadel | `~/docker/zitadel/.env` | 🔴 Critique | 6 mois |
|
||||||
|
| Zitadel MASTERKEY | `~/docker/zitadel/.env` | 🔴⛔ IMMUABLE | Jamais |
|
||||||
|
| Mdp Gitea DB | `~/docker/gitea/.env` | 🟡 Important | 6 mois |
|
||||||
|
| Mdp n8n DB | `~/docker/n8n/.env` | 🟡 Important | 6 mois |
|
||||||
|
| n8n Encryption Key | `~/docker/n8n/.env` | 🔴 Critique | Jamais (perte = perte workflows chiffrés) |
|
||||||
|
| Client Secret OAuth2 (par outil) | `~/docker/oauth2-proxy-*/env` | 🟡 Important | 6 mois |
|
||||||
|
| Cookie Secret OAuth2 | `~/docker/oauth2-proxy-*/env` | 🟢 Mineur | 1 an (déconnecte les users) |
|
||||||
|
| Code Server password (fallback) | `~/docker/code-server/.env` | 🟢 Mineur | 6 mois |
|
||||||
|
| Furious API credentials | `~/furious-to-zitadel/.env` | 🟡 Important | 6 mois |
|
||||||
|
| Zitadel Service Account key | `~/furious-to-zitadel/zitadelfuriousimportkey.json` | 🔴 Critique | 1 an (avec expiration prévue) |
|
||||||
|
| Traefik basic auth | `~/docker/traefik/docker-compose.yml` (label) | 🟡 Important | 6 mois |
|
||||||
|
|
||||||
|
### Secrets stockés ailleurs
|
||||||
|
|
||||||
|
| Secret | Localisation |
|
||||||
|
|---|---|
|
||||||
|
| Mdp utilisateurs Zitadel | Dans la base Postgres Zitadel (hashés) |
|
||||||
|
| Tokens de session OAuth2-Proxy | Dans les cookies HTTP des navigateurs des users |
|
||||||
|
| Certificats Let's Encrypt | `~/docker/traefik/letsencrypt/acme.json` (chmod 600) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Bonnes pratiques
|
||||||
|
|
||||||
|
### 1. Toujours utiliser un gestionnaire de mots de passe
|
||||||
|
|
||||||
|
Pour stocker **tous** les secrets de l'infra :
|
||||||
|
- **Bitwarden** (cloud + self-hosted possible) ← recommandé
|
||||||
|
- **KeePassXC** (offline)
|
||||||
|
- **1Password Business**
|
||||||
|
|
||||||
|
Créer une **collection partagée "Seize Infra"** accessible aux mainteneurs (Hedi, Philippe, futur collègue).
|
||||||
|
|
||||||
|
### 2. Permissions Linux strictes
|
||||||
|
|
||||||
|
Tous les fichiers contenant des secrets doivent avoir `chmod 600` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier toutes les permissions de .env
|
||||||
|
find ~/docker -name ".env" -exec ls -la {} \;
|
||||||
|
# Toutes doivent montrer "-rw-------" (600)
|
||||||
|
|
||||||
|
# Vérifier la clé Zitadel
|
||||||
|
ls -la ~/furious-to-zitadel/zitadelfuriousimportkey.json
|
||||||
|
# Doit être 600
|
||||||
|
```
|
||||||
|
|
||||||
|
Corriger si nécessaire :
|
||||||
|
```bash
|
||||||
|
chmod 600 ~/docker/<service>/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Ne JAMAIS committer de secret sur Git
|
||||||
|
|
||||||
|
Configurer `.gitignore` dans tous les dépôts :
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.p12
|
||||||
|
zitadelfuriousimportkey.json
|
||||||
|
users-credentials.csv
|
||||||
|
distribution-teams.csv
|
||||||
|
roles-mapping.json
|
||||||
|
letsencrypt/acme.json
|
||||||
|
|
||||||
|
# Logs (peuvent contenir des secrets)
|
||||||
|
*.log
|
||||||
|
import.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Ne JAMAIS coller de secret dans une conversation IA
|
||||||
|
|
||||||
|
Si tu colles un token, mdp, ou clé dans Claude / ChatGPT / autre, considère-le **compromis** :
|
||||||
|
- Il peut être logué côté provider
|
||||||
|
- Il peut être utilisé pour entraîner des modèles
|
||||||
|
- L'historique est conservé pendant des mois
|
||||||
|
|
||||||
|
→ **Toujours masquer** les secrets (ex: `XXX_REDACTED_XXX`) avant de coller du contenu.
|
||||||
|
|
||||||
|
⚠️ Voir [HANDOVER.md](../HANDOVER.md) pour la liste des secrets qui ont été exposés pendant le développement et qui doivent être rotatés.
|
||||||
|
|
||||||
|
### 5. Audit des accès aux secrets
|
||||||
|
|
||||||
|
Périodiquement, vérifier :
|
||||||
|
- Qui a accès au gestionnaire de mots de passe ?
|
||||||
|
- Y a-t-il des anciens employés à révoquer ?
|
||||||
|
- Les permissions Linux sont-elles toujours OK ?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Procédures de rotation
|
||||||
|
|
||||||
|
### Rotation Client Secret OAuth2 (par outil)
|
||||||
|
|
||||||
|
⏱️ ~5 min par outil.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Aller dans Zitadel Console
|
||||||
|
# → Projects → Infra Seize → Applications → <outil>
|
||||||
|
# → Actions → Generate Client Secret → copier la nouvelle valeur
|
||||||
|
|
||||||
|
# 2. Mettre à jour le .env d'OAuth2-Proxy
|
||||||
|
nano ~/docker/oauth2-proxy-<outil>/.env
|
||||||
|
# Remplacer OAUTH2_PROXY_CLIENT_SECRET=...
|
||||||
|
|
||||||
|
# 3. Sauvegarder dans le gestionnaire de mdp
|
||||||
|
|
||||||
|
# 4. Restart OAuth2-Proxy
|
||||||
|
cd ~/docker/oauth2-proxy-<outil>
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 5. Tester le SSO en incognito
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rotation Postgres password (Gitea ou n8n)
|
||||||
|
|
||||||
|
⏱️ ~10 min. **Hors heures de bureau** (le service s'arrête brièvement).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Générer nouveau mdp
|
||||||
|
NEW_PWD=$(openssl rand -base64 32 | tr -d '+/=' | head -c 32)
|
||||||
|
echo "Nouveau mdp : $NEW_PWD"
|
||||||
|
# Sauvegarder dans le gestionnaire
|
||||||
|
|
||||||
|
# 2. Changer le mdp dans Postgres
|
||||||
|
docker exec -it postgres psql -U postgres -c "ALTER USER gitea WITH PASSWORD '$NEW_PWD';"
|
||||||
|
|
||||||
|
# 3. Mettre à jour le .env du service
|
||||||
|
nano ~/docker/gitea/.env
|
||||||
|
# Remplacer GITEA_DB_PASSWORD=...
|
||||||
|
|
||||||
|
# 4. Restart
|
||||||
|
cd ~/docker/gitea
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 5. Tester
|
||||||
|
curl -sI https://git.seizeconsulting.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rotation Zitadel Service Account key
|
||||||
|
|
||||||
|
⏱️ ~10 min.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Console Zitadel → Users → furious-import-service → Keys
|
||||||
|
# → Créer une nouvelle clé (Type JSON)
|
||||||
|
# → Télécharger immédiatement
|
||||||
|
# → Supprimer l'ancienne clé (pour l'invalider)
|
||||||
|
|
||||||
|
# 2. Transférer sur le serveur
|
||||||
|
scp <nouvelle_cle>.json admin@<server>:~/furious-to-zitadel/zitadelfuriousimportkey.json
|
||||||
|
|
||||||
|
# 3. Sécuriser
|
||||||
|
ssh admin@<server>
|
||||||
|
chmod 600 ~/furious-to-zitadel/zitadelfuriousimportkey.json
|
||||||
|
|
||||||
|
# 4. Tester les scripts
|
||||||
|
cd ~/furious-to-zitadel
|
||||||
|
source venv/bin/activate
|
||||||
|
python3 assign-roles.py --dry-run # ou un autre script sans effet
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rotation Furious API credentials
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Sur Furious : Configuration → Utilisateurs API
|
||||||
|
# → Supprimer l'ancien compte
|
||||||
|
# → Créer un nouveau avec les mêmes permissions (User > Search uniquement)
|
||||||
|
# → Sauvegarder identifiants dans le gestionnaire de mdp
|
||||||
|
|
||||||
|
# 2. Mettre à jour le .env
|
||||||
|
nano ~/furious-to-zitadel/.env
|
||||||
|
# Remplacer FURIOUS_USERNAME et FURIOUS_PASSWORD
|
||||||
|
|
||||||
|
# 3. Tester
|
||||||
|
cd ~/furious-to-zitadel
|
||||||
|
source venv/bin/activate
|
||||||
|
./test-auth.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⛔ ZITADEL_MASTERKEY — IMMUABLE
|
||||||
|
|
||||||
|
⚠️ **NE JAMAIS CHANGER** le `ZITADEL_MASTERKEY` après le premier démarrage de Zitadel.
|
||||||
|
|
||||||
|
Cette clé est utilisée pour chiffrer/déchiffrer les données sensibles en base. La changer = perdre l'accès à toutes les données.
|
||||||
|
|
||||||
|
Si elle a été exposée → option drastique uniquement :
|
||||||
|
1. Backup complet des données critiques (export users via API)
|
||||||
|
2. Détruire l'instance Zitadel
|
||||||
|
3. Re-déployer from scratch avec une nouvelle MASTERKEY
|
||||||
|
4. Restore les users via les scripts d'import
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📧 Configuration SMTP M365 (à faire)
|
||||||
|
|
||||||
|
⚠️ **Pas encore fait** au moment de la passation.
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
|
||||||
|
- Admin M365 (pas Hedi → solliciter quelqu'un d'autre)
|
||||||
|
- Compte M365 dédié : `noreply@seizeconsulting.com`
|
||||||
|
|
||||||
|
### Étape 1 — Créer le compte M365
|
||||||
|
|
||||||
|
Demander à l'admin M365 :
|
||||||
|
1. Créer le compte `noreply@seizeconsulting.com`
|
||||||
|
2. Activer SMTP AUTH (peut nécessiter ticket Microsoft)
|
||||||
|
3. Désactiver MFA sur ce compte (incompatible avec SMTP)
|
||||||
|
4. Générer un **App Password** pour ce compte
|
||||||
|
|
||||||
|
### Étape 2 — Configurer dans Zitadel
|
||||||
|
|
||||||
|
Console Zitadel → Default Settings → Notifications → SMTP Provider :
|
||||||
|
|
||||||
|
| Paramètre | Valeur |
|
||||||
|
|---|---|
|
||||||
|
| Host | `smtp.office365.com` |
|
||||||
|
| Port | `587` |
|
||||||
|
| User | `noreply@seizeconsulting.com` |
|
||||||
|
| Password | `<App Password généré>` |
|
||||||
|
| From | `noreply@seizeconsulting.com` |
|
||||||
|
| From Name | `Seize Consulting` |
|
||||||
|
| Reply-to | (vide ou `it@seizeconsulting.com`) |
|
||||||
|
| TLS | ✅ Required |
|
||||||
|
|
||||||
|
### Étape 3 — Tester
|
||||||
|
|
||||||
|
Console Zitadel → Default Settings → SMTP → **Test Email**.
|
||||||
|
|
||||||
|
→ Si OK, on peut désormais utiliser :
|
||||||
|
- Welcome emails pour nouveaux users
|
||||||
|
- Reset password
|
||||||
|
- MFA Email codes
|
||||||
|
- Notifications de sécurité
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Audit des secrets exposés
|
||||||
|
|
||||||
|
Pendant le développement (cf. [HANDOVER.md](../HANDOVER.md)), plusieurs secrets ont été exposés dans des conversations IA :
|
||||||
|
|
||||||
|
| Secret | Statut | Action requise |
|
||||||
|
|---|---|---|
|
||||||
|
| Compte API Furious `mick.emmaus.90849` | 🔴 Exposé | **Rotater dans Furious** |
|
||||||
|
| ID API Furious `6a0b0396817841ef2d3c87c8` | 🔴 Exposé | **Rotater dans Furious** |
|
||||||
|
| Token JWT user HKA (1 instance) | 🟡 Expiré | OK (1h de vie) |
|
||||||
|
| Noms + emails + job_titles ~10 collaborateurs | 🟡 Exposé | Pas de rotation possible, vigilance |
|
||||||
|
|
||||||
|
### Procédure post-incident
|
||||||
|
|
||||||
|
1. Rotater immédiatement le secret (procédures ci-dessus)
|
||||||
|
2. Vérifier les **logs d'accès** Zitadel/Furious pour détecter usage suspect
|
||||||
|
3. Documenter dans un fichier sécurité (qui, quand, quoi)
|
||||||
|
4. Si données personnelles exposées → notification CNIL si applicable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Hardening complémentaire
|
||||||
|
|
||||||
|
### Activer fail2ban (recommandé)
|
||||||
|
|
||||||
|
Pour bloquer automatiquement les IPs malveillantes qui scannent SSH ou OAuth2 :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y fail2ban
|
||||||
|
|
||||||
|
# Config par défaut OK pour SSH
|
||||||
|
sudo systemctl enable --now fail2ban
|
||||||
|
|
||||||
|
# Vérifier
|
||||||
|
sudo fail2ban-client status sshd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Activer Crowdsec (alternative moderne)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s https://install.crowdsec.net | sudo sh
|
||||||
|
# Puis configurer les bouncers (cf-firewall-bouncer, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring des tentatives de login échouées Zitadel
|
||||||
|
|
||||||
|
À mettre en place via :
|
||||||
|
- Workflow n8n qui scrape les logs Zitadel toutes les heures
|
||||||
|
- Alert si > 50 échecs en 1h (potentiellement attaque par brute force)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Voir aussi
|
||||||
|
|
||||||
|
- [HANDOVER.md](../HANDOVER.md) — Liste des secrets exposés à rotater en priorité
|
||||||
|
- [03-zitadel-setup.md](./03-zitadel-setup.md) — Configuration du service account
|
||||||
|
- [04-oauth2-proxy-pattern.md](./04-oauth2-proxy-pattern.md) — Rotation des secrets OAuth2
|
||||||
|
- [OPERATIONS.md](../OPERATIONS.md) — Procédures complètes de rotation
|
||||||
486
docs/09-troubleshooting.md
Normal file
486
docs/09-troubleshooting.md
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
# 09 — Troubleshooting
|
||||||
|
|
||||||
|
> Problèmes fréquents rencontrés et leurs solutions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Démarrage de tout diagnostic
|
||||||
|
|
||||||
|
Avant d'essayer n'importe quoi, vérifier :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Conteneurs en route
|
||||||
|
sudo docker ps
|
||||||
|
|
||||||
|
# 2. Espace disque
|
||||||
|
df -h /
|
||||||
|
|
||||||
|
# 3. Mémoire libre
|
||||||
|
free -h
|
||||||
|
|
||||||
|
# 4. Charge système
|
||||||
|
uptime
|
||||||
|
|
||||||
|
# 5. Logs Traefik récents (souvent la source des problèmes)
|
||||||
|
sudo docker logs --tail 50 traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Problèmes critiques
|
||||||
|
|
||||||
|
### "Tout est down, je ne peux plus accéder à rien"
|
||||||
|
|
||||||
|
#### Diagnostic
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH au serveur
|
||||||
|
ssh hedi@<server>
|
||||||
|
|
||||||
|
# Vérifier Docker
|
||||||
|
sudo systemctl status docker
|
||||||
|
sudo docker ps | wc -l
|
||||||
|
# Doit afficher > 20 (24 conteneurs attendus)
|
||||||
|
|
||||||
|
# Vérifier ports 80/443
|
||||||
|
sudo netstat -tlnp | grep -E "80|443"
|
||||||
|
|
||||||
|
# Vérifier les certificats Let's Encrypt
|
||||||
|
sudo ls -la ~/docker/traefik/letsencrypt/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Causes possibles
|
||||||
|
|
||||||
|
**Cause 1 — Docker daemon down** :
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart docker
|
||||||
|
sudo docker compose -f ~/docker/traefik/docker-compose.yml up -d
|
||||||
|
# Démarrer dans l'ordre : Traefik, puis Zitadel, puis les autres
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause 2 — Disque plein** :
|
||||||
|
```bash
|
||||||
|
# Nettoyer Docker
|
||||||
|
sudo docker system prune -a --volumes
|
||||||
|
# Nettoyer logs si > 1 Go
|
||||||
|
sudo du -sh /var/lib/docker/containers/*/
|
||||||
|
sudo truncate -s 0 /var/lib/docker/containers/*/*-json.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause 3 — IP du serveur a changé** :
|
||||||
|
- Vérifier DNS : `dig +short sso.seizeconsulting.com`
|
||||||
|
- Si IP différente → mettre à jour DNS chez le registrar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### "Le SSO ne fonctionne plus depuis 5 minutes"
|
||||||
|
|
||||||
|
Symptôme : tous les outils SSO renvoient une boucle de redirection ou un "Bad Gateway".
|
||||||
|
|
||||||
|
#### Diagnostic — Vérifier dans cet ordre
|
||||||
|
|
||||||
|
**1. Zitadel répond ?**
|
||||||
|
```bash
|
||||||
|
curl -sI https://sso.seizeconsulting.com
|
||||||
|
# Doit retourner 200 ou 302
|
||||||
|
```
|
||||||
|
|
||||||
|
Si non : `docker logs zitadel-zitadel-api-1 --tail 50`
|
||||||
|
|
||||||
|
**2. Traefik ForwardAuth marche ?**
|
||||||
|
```bash
|
||||||
|
grep -A 8 "authRequestHeaders" ~/docker/traefik/dynamic.yml
|
||||||
|
# La ligne "- Cookie" doit être présente
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. OAuth2-Proxy de l'outil tourne ?**
|
||||||
|
```bash
|
||||||
|
sudo docker ps | grep oauth2-proxy
|
||||||
|
# Tous doivent être Up
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Logs OAuth2-Proxy ?**
|
||||||
|
```bash
|
||||||
|
sudo docker logs --tail 50 oauth2-proxy
|
||||||
|
sudo docker logs --tail 50 oauth2-proxy-n8n
|
||||||
|
# Chercher : "error", "unauthorized", "context deadline exceeded"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Solutions courantes
|
||||||
|
|
||||||
|
**Solution 1** : Restart de Traefik
|
||||||
|
```bash
|
||||||
|
cd ~/docker/traefik
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution 2** : Restart d'OAuth2-Proxy de l'outil concerné
|
||||||
|
```bash
|
||||||
|
cd ~/docker/oauth2-proxy-<outil>
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution 3** : Vérifier que les certificats TLS sont OK
|
||||||
|
```bash
|
||||||
|
echo | openssl s_client -connect sso.seizeconsulting.com:443 2>/dev/null | openssl x509 -noout -dates
|
||||||
|
# notAfter doit être dans > 7 jours
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### "Le bug Traefik Cookie est revenu (boucle de redirect)"
|
||||||
|
|
||||||
|
C'est **le** bug historique. Cf. [DECISIONS.md D3](../DECISIONS.md).
|
||||||
|
|
||||||
|
#### Fix immédiat
|
||||||
|
|
||||||
|
Vérifier que `~/docker/traefik/dynamic.yml` contient :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
oauth2-headers:
|
||||||
|
forwardAuth:
|
||||||
|
address: "http://oauth2-proxy:4180/oauth2/auth"
|
||||||
|
authRequestHeaders:
|
||||||
|
- Cookie # ← CRITIQUE
|
||||||
|
- Authorization
|
||||||
|
authResponseHeaders:
|
||||||
|
- X-Auth-Request-User
|
||||||
|
- X-Auth-Request-Email
|
||||||
|
```
|
||||||
|
|
||||||
|
Si la ligne `- Cookie` manque → l'ajouter, puis :
|
||||||
|
```bash
|
||||||
|
cd ~/docker/traefik
|
||||||
|
docker compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 Problèmes courants
|
||||||
|
|
||||||
|
### "Le certificat Let's Encrypt va expirer / a expiré"
|
||||||
|
|
||||||
|
Traefik renouvelle automatiquement. Si pas le cas :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier les logs Let's Encrypt
|
||||||
|
sudo docker logs traefik 2>&1 | grep -i "letsencrypt\|acme" | tail -20
|
||||||
|
|
||||||
|
# Causes possibles :
|
||||||
|
# 1. Rate limit Let's Encrypt (trop de tentatives) → attendre 1h
|
||||||
|
# 2. DNS pas correct → vérifier dig
|
||||||
|
# 3. Port 80 bloqué → vérifier firewall
|
||||||
|
|
||||||
|
# Forcer le renouvellement
|
||||||
|
cd ~/docker/traefik
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ Cas critique : `acme.json` corrompu :
|
||||||
|
```bash
|
||||||
|
sudo cp letsencrypt/acme.json letsencrypt/acme.json.backup
|
||||||
|
sudo rm letsencrypt/acme.json
|
||||||
|
sudo touch letsencrypt/acme.json
|
||||||
|
sudo chmod 600 letsencrypt/acme.json
|
||||||
|
docker compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
→ Recrée tous les certificats (peut prendre 5 min).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### "Un user ne peut pas se logger"
|
||||||
|
|
||||||
|
#### Vérifier dans cet ordre
|
||||||
|
|
||||||
|
**1. Le compte existe-t-il dans Zitadel ?**
|
||||||
|
- Console Zitadel → Users → Search par email
|
||||||
|
- Status doit être "Active" (pas "Inactive" ou "Deleted")
|
||||||
|
|
||||||
|
**2. L'email est-il vérifié ?**
|
||||||
|
- Console → User → General → Email verified ?
|
||||||
|
- Si non : Actions → Resend verification (mais SMTP doit être configuré)
|
||||||
|
- Ou : Set as verified manually
|
||||||
|
|
||||||
|
**3. Le MFA est-il bloqué ?**
|
||||||
|
- Si l'user a perdu son téléphone : Reset MFA
|
||||||
|
- Console → User → Multi-factor authentication → Remove
|
||||||
|
|
||||||
|
**4. Lockout actif ?**
|
||||||
|
- Console → User → Activity → vérifier les failed attempts
|
||||||
|
- Reset : Actions → Unlock
|
||||||
|
|
||||||
|
**5. L'user a-t-il un rôle ?**
|
||||||
|
- Si "Only authorized users" est coché sur le project, il faut un rôle
|
||||||
|
- Console → User → Authorizations → Add role
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### "Le script Python d'import échoue"
|
||||||
|
|
||||||
|
#### Erreur 401 / 403
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/furious-to-zitadel
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Vérifier la clé JSON
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
data = json.load(open('zitadelfuriousimportkey.json'))
|
||||||
|
print('UserId:', data.get('userId'))
|
||||||
|
print('KeyId:', data.get('keyId'))
|
||||||
|
"
|
||||||
|
# Si erreur → la clé est corrompue, en régénérer une
|
||||||
|
|
||||||
|
# Vérifier les permissions du service account
|
||||||
|
# Console Zitadel → Users → furious-import-service → IAM Members
|
||||||
|
# Doit avoir IAM_USER_MANAGER
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Erreur 400 sur certains users
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Voir les détails dans le log
|
||||||
|
tail -100 import.log | grep -B 2 "ERROR"
|
||||||
|
|
||||||
|
# Causes courantes :
|
||||||
|
# - Email invalide dans Furious (espaces, caractères spéciaux)
|
||||||
|
# - Mdp trop court / ne respecte pas la policy
|
||||||
|
# - Username déjà existant
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Erreur de connexion Furious
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test-auth.sh
|
||||||
|
# Doit retourner un JSON valide
|
||||||
|
# Si 401 → identifiants Furious à régénérer
|
||||||
|
# Si timeout → API Furious peut être down, vérifier status
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### "L'intranet ne montre pas les bonnes cartes"
|
||||||
|
|
||||||
|
Voir [07-rbac-intranet.md](./07-rbac-intranet.md) section Troubleshooting.
|
||||||
|
|
||||||
|
Rapide :
|
||||||
|
1. Console JS (F12) → vérifier le log `[RBAC] User: ... | Rôle: ...`
|
||||||
|
2. Si rôle null → `roles-mapping.json` à régénérer
|
||||||
|
3. Si email = `__USER_EMAIL__` → nginx sub_filter pas appliqué
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### "Le conteneur xxx redémarre en boucle"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Voir les logs
|
||||||
|
sudo docker logs <container> --tail 50
|
||||||
|
|
||||||
|
# Voir le statut détaillé
|
||||||
|
sudo docker inspect <container> --format '{{.State.Health}}'
|
||||||
|
|
||||||
|
# Causes courantes :
|
||||||
|
# 1. Healthcheck failed → vérifier la commande de healthcheck
|
||||||
|
# 2. Erreur de config (mauvais env) → vérifier .env
|
||||||
|
# 3. Volume manquant ou permissions → vérifier les paths
|
||||||
|
# 4. Port déjà utilisé → docker compose down + up
|
||||||
|
|
||||||
|
# Voir les events Docker récents
|
||||||
|
sudo docker events --since 10m
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### "Espace disque saturé"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Voir où va l'espace
|
||||||
|
sudo du -sh /var/lib/docker/* | sort -h
|
||||||
|
sudo du -sh /home/*
|
||||||
|
|
||||||
|
# Nettoyer Docker
|
||||||
|
sudo docker system prune -a --volumes
|
||||||
|
# ⚠️ Supprime tout ce qui n'est pas utilisé activement
|
||||||
|
|
||||||
|
# Nettoyer les logs Docker
|
||||||
|
sudo find /var/lib/docker/containers -name "*-json.log" -exec truncate -s 0 {} \;
|
||||||
|
|
||||||
|
# Configurer la rotation des logs (à mettre dans /etc/docker/daemon.json) :
|
||||||
|
{
|
||||||
|
"log-driver": "json-file",
|
||||||
|
"log-opts": {
|
||||||
|
"max-size": "100m",
|
||||||
|
"max-file": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Restart Docker après modif
|
||||||
|
sudo systemctl restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 Problèmes mineurs
|
||||||
|
|
||||||
|
### "L'image d'un conteneur est obsolète"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/docker/<service>
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Comment voir les logs en temps réel ?"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f
|
||||||
|
# Ctrl+C pour quitter
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Comment redémarrer juste un service sans toucher les autres ?"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker restart <container_name>
|
||||||
|
# Plus rapide qu'un docker compose
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Comment exécuter une commande dans un conteneur ?"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker exec -it <container> sh
|
||||||
|
# ou bash si dispo
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Débogage avancé
|
||||||
|
|
||||||
|
### Nginx Diagnostic (debug RBAC)
|
||||||
|
|
||||||
|
⚠️ **Procédure temporaire** — supprimer immédiatement après usage.
|
||||||
|
|
||||||
|
Pour voir ce que les headers HTTP contiennent réellement :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Créer une config nginx diagnostic
|
||||||
|
cat > ~/docker/intranet/nginx-diagnostic.conf << 'EOF'
|
||||||
|
events {}
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
location /debug {
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
return 200 "=== Headers reçus ===
|
||||||
|
X-Forwarded-User: $http_x_forwarded_user
|
||||||
|
X-Forwarded-Email: $http_x_forwarded_email
|
||||||
|
X-Forwarded-Preferred-Username: $http_x_forwarded_preferred_username
|
||||||
|
X-Forwarded-Groups: $http_x_forwarded_groups
|
||||||
|
Authorization: $http_authorization
|
||||||
|
";
|
||||||
|
}
|
||||||
|
location / { try_files $uri /index.html =404; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Modifier docker-compose pour utiliser cette config
|
||||||
|
nano ~/docker/intranet/docker-compose.yml
|
||||||
|
# Ajouter le volume : ./nginx-diagnostic.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Accéder à : https://intranet.seizeconsulting.com/debug
|
||||||
|
# (en étant logué)
|
||||||
|
|
||||||
|
# ⚠️ AVANT DE FERMER : restaurer la config normale !
|
||||||
|
rm ~/docker/intranet/nginx-diagnostic.conf
|
||||||
|
# Remettre nginx-rbac.conf dans docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Décoder un token JWT
|
||||||
|
|
||||||
|
Si tu as un token et veux voir son contenu (claims, exp, etc.) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Méthode 1 : jwt.io (en ligne, ATTENTION ne pas coller de vrais tokens)
|
||||||
|
# Méthode 2 : ligne de commande
|
||||||
|
|
||||||
|
TOKEN="<ton_jwt>"
|
||||||
|
echo $TOKEN | cut -d "." -f2 | base64 -d 2>/dev/null | python3 -m json.tool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérifier la communication réseau entre 2 conteneurs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker exec -it traefik wget -qO- http://intranet:80
|
||||||
|
sudo docker exec -it oauth2-proxy wget -qO- http://intranet:80
|
||||||
|
```
|
||||||
|
|
||||||
|
Si erreur → vérifier que les conteneurs partagent un réseau Docker commun.
|
||||||
|
|
||||||
|
### Voir les connexions actives
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ss -tunlp | grep -E "80|443"
|
||||||
|
|
||||||
|
# Connexions en cours
|
||||||
|
sudo netstat -anp | grep ESTABLISHED | grep ":443" | wc -l
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Checklist quand quelque chose ne va pas
|
||||||
|
|
||||||
|
- [ ] Tous les conteneurs `Up` ? → `sudo docker ps`
|
||||||
|
- [ ] DNS OK ? → `dig +short <domain>`
|
||||||
|
- [ ] HTTPS valide ? → `curl -sI https://<domain>`
|
||||||
|
- [ ] Logs Traefik sans erreur ? → `docker logs --tail 50 traefik | grep -i error`
|
||||||
|
- [ ] Logs OAuth2-Proxy sans erreur ? → `docker logs --tail 50 oauth2-proxy*`
|
||||||
|
- [ ] Espace disque OK ? → `df -h /`
|
||||||
|
- [ ] Mémoire OK ? → `free -h`
|
||||||
|
- [ ] Authentification Zitadel OK depuis incognito ?
|
||||||
|
- [ ] Vérifier que rien n'a été modifié récemment (fichier .yml ou .env)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Quand escalader
|
||||||
|
|
||||||
|
### Vers Philippe (sponsor)
|
||||||
|
|
||||||
|
- Service critique down > 30 min
|
||||||
|
- Demande de modification d'architecture
|
||||||
|
- Validation d'une décision business
|
||||||
|
|
||||||
|
### Vers le support Scaleway
|
||||||
|
|
||||||
|
- Serveur inaccessible (panne réseau ou hardware)
|
||||||
|
- Quota dépassé sur le compte
|
||||||
|
- Problème de facturation
|
||||||
|
|
||||||
|
### Vers le support Furious
|
||||||
|
|
||||||
|
- API Furious down ou très lente
|
||||||
|
- Permissions API qui ne fonctionnent pas malgré une bonne config
|
||||||
|
- Problèmes de données RH (départs mal renseignés, etc.)
|
||||||
|
|
||||||
|
### Vers la communauté Zitadel
|
||||||
|
|
||||||
|
- Discord Zitadel : https://discord.gg/zitadel
|
||||||
|
- Issues GitHub : https://github.com/zitadel/zitadel
|
||||||
|
- Forum : https://community.zitadel.com/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Liens vers les docs internes
|
||||||
|
|
||||||
|
- [INSTALL.md](../INSTALL.md) — Procédure d'install from scratch
|
||||||
|
- [OPERATIONS.md](../OPERATIONS.md) — Maintenance et opérations courantes
|
||||||
|
- [HANDOVER.md](../HANDOVER.md) — Actions urgentes à mener
|
||||||
|
- [DECISIONS.md](../DECISIONS.md) — Anti-patterns à éviter
|
||||||
468
install.sh
Executable file
468
install.sh
Executable file
@ -0,0 +1,468 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# install.sh — Déploiement infrastructure Seize from scratch
|
||||||
|
# ============================================================================
|
||||||
|
# Usage : ./install.sh [étape]
|
||||||
|
# Sans argument : affiche le menu
|
||||||
|
# Avec étape : lance directement cette étape
|
||||||
|
#
|
||||||
|
# Étapes :
|
||||||
|
# 1) check - Vérifications pré-install (DNS, Docker, etc.)
|
||||||
|
# 2) networks - Créer les réseaux Docker
|
||||||
|
# 3) traefik - Déployer Traefik
|
||||||
|
# 4) zitadel - Déployer Zitadel
|
||||||
|
# 5) postgres - Déployer Postgres principal
|
||||||
|
# 6) gitea - Déployer Gitea
|
||||||
|
# 7) intranet - Déployer l'intranet
|
||||||
|
# 8) oauth2-proxy - Déployer un OAuth2-Proxy (interactif)
|
||||||
|
# 9) apps - Déployer les autres apps (n8n, portainer, etc.)
|
||||||
|
# all - Tout déployer dans l'ordre (interactif)
|
||||||
|
#
|
||||||
|
# Variables : voir .env (à créer depuis .env.example)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ===== COULEURS =====
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# ===== CHARGEMENT .env =====
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
if [ -f "$SCRIPT_DIR/.env" ]; then
|
||||||
|
set -a
|
||||||
|
source "$SCRIPT_DIR/.env"
|
||||||
|
set +a
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Fichier .env manquant. Copier .env.example vers .env et adapter.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===== FONCTIONS UTILITAIRES =====
|
||||||
|
|
||||||
|
log() { echo -e "${BLUE}[$(date +%H:%M:%S)]${NC} $1"; }
|
||||||
|
ok() { echo -e "${GREEN}✅ $1${NC}"; }
|
||||||
|
warn(){ echo -e "${YELLOW}⚠️ $1${NC}"; }
|
||||||
|
err() { echo -e "${RED}❌ $1${NC}"; }
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
read -p "$1 (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
[[ $REPLY =~ ^[Yy]$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===== ÉTAPES =====
|
||||||
|
|
||||||
|
step_check() {
|
||||||
|
log "===== ÉTAPE 1 : Vérifications pré-install ====="
|
||||||
|
|
||||||
|
# Vérifier l'OS
|
||||||
|
if [ -f /etc/os-release ]; then
|
||||||
|
. /etc/os-release
|
||||||
|
log "OS : $PRETTY_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier Docker
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
err "Docker n'est pas installé. Voir INSTALL.md étape 2."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
ok "Docker installé : $(docker --version)"
|
||||||
|
|
||||||
|
# Vérifier docker compose
|
||||||
|
if ! docker compose version &> /dev/null; then
|
||||||
|
err "Docker Compose v2 n'est pas installé."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
ok "Docker Compose installé : $(docker compose version | head -1)"
|
||||||
|
|
||||||
|
# Vérifier DNS pour chaque sous-domaine
|
||||||
|
log "Vérification DNS..."
|
||||||
|
for sub in sso intranet git n8n portainer code status traefik; do
|
||||||
|
DNS_IP=$(dig +short "$sub.$DOMAIN" 2>/dev/null | head -1)
|
||||||
|
if [ "$DNS_IP" == "$SERVER_IP" ]; then
|
||||||
|
ok "DNS $sub.$DOMAIN → $DNS_IP"
|
||||||
|
else
|
||||||
|
warn "DNS $sub.$DOMAIN → '$DNS_IP' (attendu : $SERVER_IP)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier les ports 80/443 disponibles
|
||||||
|
if sudo netstat -tlnp 2>/dev/null | grep -q ":80 "; then
|
||||||
|
warn "Port 80 déjà utilisé (vérifier que c'est Traefik et pas autre chose)"
|
||||||
|
fi
|
||||||
|
if sudo netstat -tlnp 2>/dev/null | grep -q ":443 "; then
|
||||||
|
warn "Port 443 déjà utilisé (vérifier que c'est Traefik et pas autre chose)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier l'espace disque
|
||||||
|
DISK_FREE=$(df -h / | awk 'NR==2 {print $4}')
|
||||||
|
log "Espace libre disque : $DISK_FREE"
|
||||||
|
|
||||||
|
ok "Vérifications terminées."
|
||||||
|
}
|
||||||
|
|
||||||
|
step_networks() {
|
||||||
|
log "===== ÉTAPE 2 : Création des réseaux Docker ====="
|
||||||
|
|
||||||
|
for net in web backend; do
|
||||||
|
if docker network inspect $net &> /dev/null; then
|
||||||
|
warn "Réseau '$net' existe déjà"
|
||||||
|
else
|
||||||
|
docker network create $net
|
||||||
|
ok "Réseau '$net' créé"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
step_traefik() {
|
||||||
|
log "===== ÉTAPE 3 : Déploiement Traefik ====="
|
||||||
|
|
||||||
|
mkdir -p ~/docker/traefik/letsencrypt
|
||||||
|
cd ~/docker/traefik
|
||||||
|
|
||||||
|
# Copier les fichiers depuis configs/
|
||||||
|
if [ ! -f docker-compose.yml ]; then
|
||||||
|
cp $SCRIPT_DIR/configs/traefik/docker-compose.yml.template docker-compose.yml
|
||||||
|
warn "docker-compose.yml créé depuis le template. À adapter si besoin."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f traefik.yml ]; then
|
||||||
|
cp $SCRIPT_DIR/configs/traefik/traefik.yml.template traefik.yml
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f dynamic.yml ]; then
|
||||||
|
cp $SCRIPT_DIR/configs/traefik/dynamic.yml.template dynamic.yml
|
||||||
|
warn "⚠️ Penser à mettre à jour LETSENCRYPT_EMAIL et TRAEFIK_DASHBOARD_AUTH dans dynamic.yml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Permissions
|
||||||
|
touch letsencrypt/acme.json
|
||||||
|
chmod 600 letsencrypt/acme.json
|
||||||
|
|
||||||
|
# Lancement
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
if docker ps | grep -q traefik; then
|
||||||
|
ok "Traefik démarré"
|
||||||
|
else
|
||||||
|
err "Traefik n'a pas démarré. Logs :"
|
||||||
|
docker compose logs --tail 50
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
step_postgres() {
|
||||||
|
log "===== ÉTAPE 4 : Déploiement Postgres principal ====="
|
||||||
|
|
||||||
|
mkdir -p ~/docker/postgres/data
|
||||||
|
cd ~/docker/postgres
|
||||||
|
|
||||||
|
if [ ! -f docker-compose.yml ]; then
|
||||||
|
cp $SCRIPT_DIR/configs/postgres/docker-compose.yml.template docker-compose.yml
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
cat > .env << EOF
|
||||||
|
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
EOF
|
||||||
|
chmod 600 .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
log "Attente démarrage Postgres..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
if docker exec postgres pg_isready -U postgres &> /dev/null; then
|
||||||
|
ok "Postgres prêt"
|
||||||
|
else
|
||||||
|
err "Postgres pas prêt"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Créer les databases dédiées
|
||||||
|
log "Création des databases gitea et n8n..."
|
||||||
|
docker exec postgres psql -U postgres << EOF
|
||||||
|
CREATE USER gitea WITH PASSWORD '${GITEA_DB_PASSWORD}';
|
||||||
|
CREATE DATABASE gitea OWNER gitea;
|
||||||
|
CREATE USER n8n WITH PASSWORD '${N8N_DB_PASSWORD}';
|
||||||
|
CREATE DATABASE n8n OWNER n8n;
|
||||||
|
EOF
|
||||||
|
ok "Databases créées"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_zitadel() {
|
||||||
|
log "===== ÉTAPE 5 : Déploiement Zitadel ====="
|
||||||
|
warn "⚠️ Zitadel est un déploiement complexe. Voir docs/03-zitadel-setup.md."
|
||||||
|
warn "Cette étape lance le docker-compose Zitadel uniquement."
|
||||||
|
warn "La création du compte zitadel-admin se fait MANUELLEMENT via la console après."
|
||||||
|
|
||||||
|
if ! confirm "Continuer ?"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ~/docker/zitadel/machinekey
|
||||||
|
cd ~/docker/zitadel
|
||||||
|
|
||||||
|
if [ ! -f docker-compose.yml ]; then
|
||||||
|
cp $SCRIPT_DIR/configs/zitadel/docker-compose.yml.template docker-compose.yml
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
cp $SCRIPT_DIR/configs/zitadel/.env.template .env
|
||||||
|
warn "Adapter le .env avant de lancer (ZITADEL_MASTER_KEY notamment)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
log "Attente démarrage Zitadel (peut prendre 1-2 min)..."
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
ok "Zitadel démarré. Aller sur https://sso.${DOMAIN}/ui/console pour finaliser."
|
||||||
|
}
|
||||||
|
|
||||||
|
step_intranet() {
|
||||||
|
log "===== ÉTAPE 6 : Déploiement Intranet ====="
|
||||||
|
|
||||||
|
mkdir -p ~/docker/intranet
|
||||||
|
cd ~/docker/intranet
|
||||||
|
|
||||||
|
# Copier les fichiers depuis configs/
|
||||||
|
if [ ! -f docker-compose.yml ]; then
|
||||||
|
cp $SCRIPT_DIR/configs/intranet/docker-compose.yml.template docker-compose.yml
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f nginx-rbac.conf ]; then
|
||||||
|
cp $SCRIPT_DIR/configs/intranet/nginx-rbac.conf.template nginx-rbac.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f index.html ]; then
|
||||||
|
cp $SCRIPT_DIR/scripts/intranet/index.html.template index.html
|
||||||
|
warn "index.html créé depuis le template. À personnaliser (logo, couleurs, etc.)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f roles-mapping.json ]; then
|
||||||
|
echo '{}' > roles-mapping.json
|
||||||
|
warn "roles-mapping.json vide créé. Sera généré par generate-roles-mapping.py après import."
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
if docker ps | grep -q intranet; then
|
||||||
|
ok "Intranet démarré"
|
||||||
|
else
|
||||||
|
err "Intranet n'a pas démarré. Logs :"
|
||||||
|
docker compose logs --tail 30
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
step_gitea() {
|
||||||
|
log "===== ÉTAPE 7 : Déploiement Gitea ====="
|
||||||
|
warn "⚠️ La config SSO Zitadel pour Gitea se fait MANUELLEMENT via l'UI Gitea après ce script."
|
||||||
|
|
||||||
|
mkdir -p ~/docker/gitea/data
|
||||||
|
cd ~/docker/gitea
|
||||||
|
|
||||||
|
if [ ! -f docker-compose.yml ]; then
|
||||||
|
cp $SCRIPT_DIR/configs/gitea/docker-compose.yml.template docker-compose.yml
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
cat > .env << EOF
|
||||||
|
GITEA_DB_PASSWORD=${GITEA_DB_PASSWORD}
|
||||||
|
EOF
|
||||||
|
chmod 600 .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
ok "Gitea démarré. Aller sur https://git.${DOMAIN} pour setup initial."
|
||||||
|
}
|
||||||
|
|
||||||
|
step_oauth2_proxy() {
|
||||||
|
log "===== ÉTAPE 8 : Déploiement OAuth2-Proxy (générique) ====="
|
||||||
|
warn "Cette étape déploie 1 OAuth2-Proxy pour 1 outil."
|
||||||
|
|
||||||
|
read -p "Nom de l'outil (intranet, n8n, portainer, code, status) : " TOOL
|
||||||
|
read -p "Host (ex: intranet.seizeconsulting.com) : " TOOL_HOST
|
||||||
|
read -p "Upstream URL (ex: http://intranet:80) : " TOOL_UPSTREAM
|
||||||
|
read -p "Client ID Zitadel : " ZITADEL_CLIENT_ID
|
||||||
|
read -s -p "Client Secret Zitadel : " ZITADEL_CLIENT_SECRET
|
||||||
|
echo
|
||||||
|
|
||||||
|
COOKIE_SECRET=$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')
|
||||||
|
|
||||||
|
DIR=~/docker/oauth2-proxy-${TOOL}
|
||||||
|
if [ "$TOOL" == "intranet" ]; then
|
||||||
|
DIR=~/docker/oauth2-proxy
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p $DIR
|
||||||
|
cd $DIR
|
||||||
|
|
||||||
|
# docker-compose.yml
|
||||||
|
cat > docker-compose.yml << EOF
|
||||||
|
services:
|
||||||
|
oauth2-proxy-${TOOL}:
|
||||||
|
image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
|
||||||
|
container_name: oauth2-proxy-${TOOL}
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.${TOOL}-sso.rule=Host(\`${TOOL_HOST}\`)"
|
||||||
|
- "traefik.http.routers.${TOOL}-sso.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.${TOOL}-sso.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.routers.${TOOL}-sso.service=${TOOL}-sso"
|
||||||
|
- "traefik.http.routers.${TOOL}-sso.priority=300"
|
||||||
|
- "traefik.http.services.${TOOL}-sso.loadbalancer.server.port=4180"
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# .env
|
||||||
|
cat > .env << EOF
|
||||||
|
OAUTH2_PROXY_PROVIDER=oidc
|
||||||
|
OAUTH2_PROXY_CLIENT_ID=${ZITADEL_CLIENT_ID}
|
||||||
|
OAUTH2_PROXY_CLIENT_SECRET=${ZITADEL_CLIENT_SECRET}
|
||||||
|
OAUTH2_PROXY_COOKIE_SECRET=${COOKIE_SECRET}
|
||||||
|
OAUTH2_PROXY_OIDC_ISSUER_URL=https://sso.${DOMAIN}
|
||||||
|
OAUTH2_PROXY_REDIRECT_URL=https://${TOOL_HOST}/oauth2/callback
|
||||||
|
OAUTH2_PROXY_UPSTREAMS=${TOOL_UPSTREAM}
|
||||||
|
OAUTH2_PROXY_SCOPE=openid email profile urn:zitadel:iam:org:project:roles
|
||||||
|
OAUTH2_PROXY_EMAIL_DOMAINS=*
|
||||||
|
OAUTH2_PROXY_COOKIE_DOMAINS=.${DOMAIN}
|
||||||
|
OAUTH2_PROXY_COOKIE_SECURE=true
|
||||||
|
OAUTH2_PROXY_COOKIE_HTTPONLY=true
|
||||||
|
OAUTH2_PROXY_COOKIE_SAMESITE=lax
|
||||||
|
OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
|
||||||
|
OAUTH2_PROXY_REVERSE_PROXY=true
|
||||||
|
OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER=true
|
||||||
|
OAUTH2_PROXY_PASS_ACCESS_TOKEN=true
|
||||||
|
OAUTH2_PROXY_SET_AUTHORIZATION_HEADER=true
|
||||||
|
OAUTH2_PROXY_SET_XAUTHREQUEST=true
|
||||||
|
OAUTH2_PROXY_WHITELIST_DOMAINS=.${DOMAIN}
|
||||||
|
OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 600 .env
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
if docker ps | grep -q "oauth2-proxy-${TOOL}"; then
|
||||||
|
ok "OAuth2-Proxy ${TOOL} démarré"
|
||||||
|
else
|
||||||
|
err "Échec démarrage. Logs :"
|
||||||
|
docker compose logs --tail 30
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
step_apps() {
|
||||||
|
log "===== ÉTAPE 9 : Déploiement des apps tierces ====="
|
||||||
|
warn "Chaque app nécessite sa propre config. Cette étape ne fait que créer la structure."
|
||||||
|
|
||||||
|
for app in n8n portainer code-server uptime-kuma; do
|
||||||
|
if confirm "Déployer $app ?"; then
|
||||||
|
mkdir -p ~/docker/$app
|
||||||
|
cd ~/docker/$app
|
||||||
|
|
||||||
|
if [ -f $SCRIPT_DIR/configs/$app/docker-compose.yml.template ]; then
|
||||||
|
cp $SCRIPT_DIR/configs/$app/docker-compose.yml.template docker-compose.yml
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f $SCRIPT_DIR/configs/$app/.env.template ]; then
|
||||||
|
cp $SCRIPT_DIR/configs/$app/.env.template .env
|
||||||
|
warn "Adapter $app/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
ok "$app déployé"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
step_all() {
|
||||||
|
log "===== INSTALL COMPLET (mode interactif) ====="
|
||||||
|
step_check && \
|
||||||
|
step_networks && \
|
||||||
|
step_traefik && \
|
||||||
|
step_postgres && \
|
||||||
|
step_zitadel && \
|
||||||
|
step_intranet && \
|
||||||
|
step_gitea && \
|
||||||
|
step_apps
|
||||||
|
|
||||||
|
warn "Reste manuellement :"
|
||||||
|
warn " - Setup Zitadel via UI (créer admin, configurer policies, créer apps)"
|
||||||
|
warn " - Setup Gitea via UI (premier admin)"
|
||||||
|
warn " - OAuth2-Proxy par outil (étape 8, à répéter pour chaque)"
|
||||||
|
warn " - Import users Furious (voir scripts/furious-to-zitadel/)"
|
||||||
|
warn " - Attribution des rôles RBAC"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===== MENU PRINCIPAL =====
|
||||||
|
|
||||||
|
show_menu() {
|
||||||
|
echo "============================================================"
|
||||||
|
echo " Install Infrastructure Seize Consulting"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
echo "Étapes disponibles :"
|
||||||
|
echo " 1) check - Vérifications pré-install"
|
||||||
|
echo " 2) networks - Créer les réseaux Docker"
|
||||||
|
echo " 3) traefik - Déployer Traefik"
|
||||||
|
echo " 4) postgres - Déployer Postgres principal"
|
||||||
|
echo " 5) zitadel - Déployer Zitadel"
|
||||||
|
echo " 6) intranet - Déployer l'intranet"
|
||||||
|
echo " 7) gitea - Déployer Gitea"
|
||||||
|
echo " 8) oauth2-proxy - Déployer 1 OAuth2-Proxy (interactif)"
|
||||||
|
echo " 9) apps - Déployer n8n, portainer, code, uptime"
|
||||||
|
echo " all - Tout déployer (interactif)"
|
||||||
|
echo " q - Quitter"
|
||||||
|
echo ""
|
||||||
|
read -p "Choix : " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1|check) step_check ;;
|
||||||
|
2|networks) step_networks ;;
|
||||||
|
3|traefik) step_traefik ;;
|
||||||
|
4|postgres) step_postgres ;;
|
||||||
|
5|zitadel) step_zitadel ;;
|
||||||
|
6|intranet) step_intranet ;;
|
||||||
|
7|gitea) step_gitea ;;
|
||||||
|
8|oauth2-proxy) step_oauth2_proxy ;;
|
||||||
|
9|apps) step_apps ;;
|
||||||
|
all) step_all ;;
|
||||||
|
q|Q) exit 0 ;;
|
||||||
|
*) err "Choix invalide" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===== ENTRY POINT =====
|
||||||
|
|
||||||
|
if [ -n "$1" ]; then
|
||||||
|
case $1 in
|
||||||
|
check) step_check ;;
|
||||||
|
networks) step_networks ;;
|
||||||
|
traefik) step_traefik ;;
|
||||||
|
postgres) step_postgres ;;
|
||||||
|
zitadel) step_zitadel ;;
|
||||||
|
intranet) step_intranet ;;
|
||||||
|
gitea) step_gitea ;;
|
||||||
|
oauth2-proxy) step_oauth2_proxy ;;
|
||||||
|
apps) step_apps ;;
|
||||||
|
all) step_all ;;
|
||||||
|
*) err "Étape inconnue : $1" ; exit 1 ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
show_menu
|
||||||
|
fi
|
||||||
21
matrices/job-title-to-role.csv
Normal file
21
matrices/job-title-to-role.csv
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
job_title_furious,role_zitadel,group,notes
|
||||||
|
Président,direction,hierarchie,Top management
|
||||||
|
Directeur,direction,hierarchie,Top management
|
||||||
|
Directeur Associé,direction,hierarchie,Top management
|
||||||
|
Senior Manager,manager,hierarchie,
|
||||||
|
Sénior Manager,manager,hierarchie,Variante orthographique de Senior Manager
|
||||||
|
Manager,manager,hierarchie,
|
||||||
|
Manager BI,manager,hierarchie,Manager spécialité BI
|
||||||
|
Expert,expert,hierarchie,
|
||||||
|
Expert N2,expert,hierarchie,Expert niveau 2
|
||||||
|
Consultant Expert niveau 1,expert,hierarchie,Variante de naming
|
||||||
|
Consultant Sénior,consultant_senior,hierarchie,
|
||||||
|
Consultant Expérimenté,consultant_senior,hierarchie,Équivalent à Consultant Sénior
|
||||||
|
Consultant Expérimentée,consultant_senior,hierarchie,Variante féminine
|
||||||
|
Consultant Junior,consultant_junior,hierarchie,
|
||||||
|
Stagiaire,stagiaire,hierarchie,Distinct des apprentis
|
||||||
|
Assistante,support,hierarchie,Assistante de direction ou autre
|
||||||
|
Assistante de direction,support,hierarchie,
|
||||||
|
Chargé de recrutement,support,hierarchie,Recrutement RH
|
||||||
|
Marketing,support,hierarchie,Équipe marketing
|
||||||
|
,,(status=apprenti → alternant),Cas spécial : tous les users avec status=apprenti sont mappés en alternant indépendamment du job_title
|
||||||
|
131
matrices/rbac-matrix.md
Normal file
131
matrices/rbac-matrix.md
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
# Matrice RBAC — Qui voit quoi sur l'intranet
|
||||||
|
|
||||||
|
> Décision tranchée par Hedi Kalboussi le 5 juin 2026 (validée verbalement par Philippe Spoljaric, à valider en workshop formel).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Matrice principale
|
||||||
|
|
||||||
|
| Application | direction | manager | expert | consultant_senior | consultant_junior | alternant | stagiaire | support |
|
||||||
|
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
|
| **Intranet (page d'accueil)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Gitea (Git)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **Code Server (IDE)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **n8n (Workflows)** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| **Portainer (Docker)** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| **Uptime Kuma (Monitoring)** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| **Traefik Dashboard** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 Définition des rôles
|
||||||
|
|
||||||
|
### `direction`
|
||||||
|
**Périmètre** : Top management, accès complet à toute l'infrastructure.
|
||||||
|
|
||||||
|
Inclut : Président, Directeur, Directeur Associé.
|
||||||
|
|
||||||
|
**Profil type** : Pierre-Yves Tachon (Président), Philippe Spoljaric (Admin Infra).
|
||||||
|
|
||||||
|
### `manager`
|
||||||
|
**Périmètre** : Managers opérationnels, gèrent des équipes.
|
||||||
|
|
||||||
|
Inclut : Senior Manager, Manager, Manager BI.
|
||||||
|
|
||||||
|
### `expert`
|
||||||
|
**Périmètre** : Experts techniques senior, peuvent intervenir sur des outils dev (n8n).
|
||||||
|
|
||||||
|
Inclut : Expert, Expert N2, Consultant Expert niveau 1.
|
||||||
|
|
||||||
|
### `consultant_senior`
|
||||||
|
**Périmètre** : Consultants expérimentés (3+ ans). Accès dev (Gitea + Code).
|
||||||
|
|
||||||
|
Inclut : Consultant Sénior, Consultant Expérimenté(e).
|
||||||
|
|
||||||
|
### `consultant_junior`
|
||||||
|
**Périmètre** : Consultants juniors, accès dev limité.
|
||||||
|
|
||||||
|
### `alternant`
|
||||||
|
**Périmètre** : Apprentis en alternance, accès dev limité.
|
||||||
|
|
||||||
|
⚠️ Tous les users avec `status: apprenti` dans Furious sont mappés en `alternant`.
|
||||||
|
|
||||||
|
### `stagiaire`
|
||||||
|
**Périmètre** : Stagiaires, accès dev limité.
|
||||||
|
|
||||||
|
### `support`
|
||||||
|
**Périmètre** : Support administratif et marketing. Accès aux outils collaboratifs.
|
||||||
|
|
||||||
|
Inclut : Assistante, Assistante de direction, Chargé de recrutement, Marketing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Distribution effective (au 5 juin 2026)
|
||||||
|
|
||||||
|
| Rôle | Nombre d'users |
|
||||||
|
|---|---|
|
||||||
|
| consultant_senior | 27 |
|
||||||
|
| consultant_junior | 16 |
|
||||||
|
| manager | 14 |
|
||||||
|
| alternant | 13 |
|
||||||
|
| expert | 8 |
|
||||||
|
| direction | 6 |
|
||||||
|
| support | 3 |
|
||||||
|
| **TOTAL** | **87** (sur 88, 1 vide à corriger) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Limites acceptées (Approche A)
|
||||||
|
|
||||||
|
L'Approche A filtre les cartes **côté intranet** mais **ne bloque pas l'accès direct** à un outil si l'user connaît l'URL.
|
||||||
|
|
||||||
|
Exemple : un `consultant_junior` ne voit pas la carte Portainer sur l'intranet, mais s'il tape `portainer.seizeconsulting.com` dans son navigateur, il sera authentifié et accédera à Portainer (car le SSO ne discrimine pas par rôle).
|
||||||
|
|
||||||
|
→ Acceptable pour Seize Consulting (88 collaborateurs non-malveillants).
|
||||||
|
|
||||||
|
Pour un **vrai RBAC** sur chaque outil, voir [DECISIONS.md D7](../DECISIONS.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Modification de la matrice
|
||||||
|
|
||||||
|
### Pour ajouter ou modifier les autorisations
|
||||||
|
|
||||||
|
1. Éditer le fichier `~/docker/intranet/index.html`
|
||||||
|
2. Trouver le bloc JavaScript `ACCESS_MATRIX`
|
||||||
|
3. Modifier les listes de rôles
|
||||||
|
4. Restart de l'intranet :
|
||||||
|
```bash
|
||||||
|
cd ~/docker/intranet
|
||||||
|
docker compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pour ajouter un nouveau rôle
|
||||||
|
|
||||||
|
1. Créer le rôle dans Zitadel (Project Infra Seize → Roles → New)
|
||||||
|
2. Mettre à jour `JOB_TITLE_TO_ROLE` dans `generate-roles-mapping.py` et `assign-roles.py`
|
||||||
|
3. Mettre à jour `ACCESS_MATRIX` dans `index.html` pour préciser ce que ce rôle voit
|
||||||
|
4. Lancer :
|
||||||
|
```bash
|
||||||
|
python3 generate-roles-mapping.py
|
||||||
|
python3 assign-roles.py
|
||||||
|
cp roles-mapping.json ~/docker/intranet/
|
||||||
|
cd ~/docker/intranet && docker compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗓️ Historique des modifications
|
||||||
|
|
||||||
|
| Date | Action | Auteur |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-06-05 | Création initiale (8 rôles, 6 outils filtrés) | Hedi Kalboussi |
|
||||||
|
| | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Voir aussi
|
||||||
|
|
||||||
|
- [job-title-to-role.csv](./job-title-to-role.csv) — Mapping détaillé Furious → rôle
|
||||||
|
- [docs/07-rbac-intranet.md](../docs/07-rbac-intranet.md) — Implémentation technique
|
||||||
|
- [DECISIONS.md D7](../DECISIONS.md) — Pourquoi Approche A
|
||||||
125
scripts/furious-to-zitadel/README.md
Normal file
125
scripts/furious-to-zitadel/README.md
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
# Scripts d'import Furious → Zitadel
|
||||||
|
|
||||||
|
> Scripts Python/Shell pour l'import automatisé des collaborateurs Seize depuis Furious Squad vers Zitadel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Liste des scripts
|
||||||
|
|
||||||
|
| Fichier | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `read-all-users.sh` | Shell | Récupère les users depuis Furious API → JSON |
|
||||||
|
| `test-auth.sh` | Shell | Teste l'auth Furious |
|
||||||
|
| `import-furious-to-zitadel.py` | Python | ⭐ Script principal d'import |
|
||||||
|
| `merge-duplicates.py` | Python | Fusion des doublons après import |
|
||||||
|
| `generate-roles-mapping.py` | Python | Génère le mapping email → rôle |
|
||||||
|
| `assign-roles.py` | Python | Attribue les rôles RBAC dans Zitadel |
|
||||||
|
| `extract-distribution-csv.py` | Python | Génère le CSV pour distribution Teams |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Setup initial
|
||||||
|
|
||||||
|
### 1. Environnement Python
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/furious-to-zitadel
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install requests pyjwt cryptography
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Fichier `.env`
|
||||||
|
|
||||||
|
```env
|
||||||
|
FURIOUS_USERNAME=__creer_dans_furious_config__
|
||||||
|
FURIOUS_PASSWORD=__creer_dans_furious_config__
|
||||||
|
FURIOUS_API_URL=https://seize-consulting.furious-squad.com/api/v2
|
||||||
|
```
|
||||||
|
|
||||||
|
`chmod 600 .env`
|
||||||
|
|
||||||
|
### 3. Clé service account Zitadel
|
||||||
|
|
||||||
|
Voir [docs/06-import-furious.md](../../docs/06-import-furious.md).
|
||||||
|
|
||||||
|
Placer le fichier dans le dossier sous le nom `zitadelfuriousimportkey.json` avec `chmod 600`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Procédure complète
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/furious-to-zitadel
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# 1. Lire les users Furious
|
||||||
|
./read-all-users.sh
|
||||||
|
|
||||||
|
# 2. Test dry-run
|
||||||
|
python3 import-furious-to-zitadel.py --mode dry-run
|
||||||
|
|
||||||
|
# 3. Test sur 3 users
|
||||||
|
python3 import-furious-to-zitadel.py --mode test
|
||||||
|
|
||||||
|
# 4. Vérification dans Zitadel UI
|
||||||
|
|
||||||
|
# 5. Import production (les 85 restants)
|
||||||
|
python3 import-furious-to-zitadel.py --mode prod
|
||||||
|
|
||||||
|
# 6. Gérer les doublons (adapter DUPLICATES dans le script avant)
|
||||||
|
nano merge-duplicates.py
|
||||||
|
python3 merge-duplicates.py
|
||||||
|
|
||||||
|
# 7. Extraire CSV de distribution
|
||||||
|
python3 extract-distribution-csv.py
|
||||||
|
|
||||||
|
# 8. Générer le mapping pour RBAC intranet
|
||||||
|
python3 generate-roles-mapping.py
|
||||||
|
|
||||||
|
# 9. Attribuer les rôles RBAC
|
||||||
|
python3 assign-roles.py
|
||||||
|
|
||||||
|
# 10. Copier le mapping vers l'intranet
|
||||||
|
cp roles-mapping.json ~/docker/intranet/
|
||||||
|
cd ~/docker/intranet
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Fichiers générés (sensibles)
|
||||||
|
|
||||||
|
Ces fichiers contiennent des données sensibles, à **protéger** :
|
||||||
|
|
||||||
|
| Fichier | Contenu | Action |
|
||||||
|
|---|---|---|
|
||||||
|
| `furious-users-raw.json` | Toutes les infos Furious des collaborateurs | chmod 600, ne pas commit |
|
||||||
|
| `users-credentials.csv` | 89 lignes avec mdp temporaires | chmod 600, supprimer après distribution |
|
||||||
|
| `distribution-teams.csv` | 88 lignes (Nom, Email, Mdp) pour Teams | chmod 600, supprimer après distribution |
|
||||||
|
| `roles-mapping.json` | Mapping email → rôle | OK à partager avec le mainteneur |
|
||||||
|
| `zitadelfuriousimportkey.json` | Clé privée RSA service account | chmod 600, ne JAMAIS partager |
|
||||||
|
| `import.log` | Logs de l'import (peut contenir des emails) | chmod 600 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Synchronisation continue (à mettre en place)
|
||||||
|
|
||||||
|
Voir [docs/06-import-furious.md](../../docs/06-import-furious.md) section "Synchronisation périodique".
|
||||||
|
|
||||||
|
Idée : workflow n8n quotidien à 3h du matin qui :
|
||||||
|
1. Compare la liste Furious vs Zitadel
|
||||||
|
2. Crée les nouveaux
|
||||||
|
3. Désactive les départs
|
||||||
|
4. Met à jour les rôles
|
||||||
|
|
||||||
|
⏱️ ~1 journée de dev.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Voir aussi
|
||||||
|
|
||||||
|
- [docs/06-import-furious.md](../../docs/06-import-furious.md) — Doc complète de l'import
|
||||||
|
- [docs/07-rbac-intranet.md](../../docs/07-rbac-intranet.md) — Suite : attribution des rôles
|
||||||
|
- [matrices/job-title-to-role.csv](../../matrices/job-title-to-role.csv) — Matrice de mapping
|
||||||
179
scripts/furious-to-zitadel/assign-roles.py
Executable file
179
scripts/furious-to-zitadel/assign-roles.py
Executable file
@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
assign-roles.py — Attribue les rôles RBAC aux users dans Zitadel.
|
||||||
|
|
||||||
|
Pour chaque user actif Furious :
|
||||||
|
1. Détermine son rôle (via mapping job_title)
|
||||||
|
2. Cherche son compte Zitadel par email
|
||||||
|
3. Crée le UserGrant avec le rôle dans le project "Infra Seize"
|
||||||
|
|
||||||
|
Prérequis :
|
||||||
|
- 88 users déjà importés dans Zitadel (cf. import-furious-to-zitadel.py)
|
||||||
|
- 8 rôles déjà créés dans le project Zitadel "Infra Seize"
|
||||||
|
- Clé service account avec rôle IAM_USER_MANAGER
|
||||||
|
|
||||||
|
Auteur : Hedi Kalboussi
|
||||||
|
Date : 5 juin 2026
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
import requests
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
|
# Réutiliser le même mapping
|
||||||
|
from generate_roles_mapping import JOB_TITLE_TO_ROLE, determine_role # noqa
|
||||||
|
# Note : adapter selon disposition réelle des fichiers
|
||||||
|
# Si import échoue, copier le mapping ici directement
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).parent
|
||||||
|
KEY_FILE = SCRIPT_DIR / "zitadelfuriousimportkey.json"
|
||||||
|
RAW_DATA_FILE = SCRIPT_DIR / "furious-users-raw.json"
|
||||||
|
|
||||||
|
ZITADEL_DOMAIN = "https://sso.seizeconsulting.com"
|
||||||
|
PROJECT_ID = "375123185571463171" # ⚠️ ID du project "Infra Seize" — adapter si différent
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# AUTHENTIFICATION ZITADEL (identique au script d'import)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def get_zitadel_token():
|
||||||
|
with open(KEY_FILE) as f:
|
||||||
|
key_data = json.load(f)
|
||||||
|
user_id = key_data["userId"]
|
||||||
|
key_id = key_data["keyId"]
|
||||||
|
private_key_pem = key_data["key"]
|
||||||
|
private_key = serialization.load_pem_private_key(
|
||||||
|
private_key_pem.encode(), password=None, backend=default_backend()
|
||||||
|
)
|
||||||
|
now = int(time.time())
|
||||||
|
payload = {"iss": user_id, "sub": user_id, "aud": ZITADEL_DOMAIN, "iat": now, "exp": now + 3600}
|
||||||
|
assertion = jwt.encode(payload, private_key, algorithm="RS256", headers={"kid": key_id})
|
||||||
|
response = requests.post(
|
||||||
|
f"{ZITADEL_DOMAIN}/oauth/v2/token",
|
||||||
|
data={
|
||||||
|
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||||
|
"scope": "openid profile urn:zitadel:iam:org:project:id:zitadel:aud",
|
||||||
|
"assertion": assertion,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()["access_token"]
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# API ZITADEL — Search user by email + create user grant
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def find_user_by_email(email, token):
|
||||||
|
"""Cherche un user Zitadel par email. Retourne l'userId."""
|
||||||
|
response = requests.post(
|
||||||
|
f"{ZITADEL_DOMAIN}/v2/users",
|
||||||
|
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||||
|
json={
|
||||||
|
"queries": [
|
||||||
|
{"emailQuery": {"emailAddress": email, "method": "TEXT_QUERY_METHOD_EQUALS"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if not response.ok:
|
||||||
|
log.error(f"Erreur recherche {email} : {response.status_code}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
users = result.get("result", [])
|
||||||
|
if not users:
|
||||||
|
return None
|
||||||
|
return users[0].get("userId")
|
||||||
|
|
||||||
|
|
||||||
|
def assign_role_to_user(user_id, role_key, token):
|
||||||
|
"""Attribue un rôle au user dans le project Infra Seize."""
|
||||||
|
response = requests.post(
|
||||||
|
f"{ZITADEL_DOMAIN}/management/v1/users/{user_id}/grants",
|
||||||
|
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||||
|
json={
|
||||||
|
"projectId": PROJECT_ID,
|
||||||
|
"roleKeys": [role_key],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 409:
|
||||||
|
log.info(f" Grant déjà existant pour user {user_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not response.ok:
|
||||||
|
log.error(f"Échec attribution rôle pour {user_id} : {response.status_code} {response.text[:200]}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MAIN
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
log.info("===== Attribution des rôles RBAC =====")
|
||||||
|
|
||||||
|
with open(RAW_DATA_FILE) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
actifs = [
|
||||||
|
u for u in data.get("data", {}).get("User", [])
|
||||||
|
if u.get("departure_date") == "0000-00-00"
|
||||||
|
]
|
||||||
|
log.info(f"Users actifs : {len(actifs)}")
|
||||||
|
|
||||||
|
token = get_zitadel_token()
|
||||||
|
log.info("✅ Token Zitadel OK")
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
no_role_count = 0
|
||||||
|
not_found_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
for i, user in enumerate(actifs, 1):
|
||||||
|
email = user.get("email", "").lower()
|
||||||
|
role = determine_role(user)
|
||||||
|
|
||||||
|
log.info(f"[{i}/{len(actifs)}] {email} → rôle: {role}")
|
||||||
|
|
||||||
|
if not role:
|
||||||
|
log.warning(f" Pas de rôle déterminé (job_title='{user.get('job_title')}')")
|
||||||
|
no_role_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
user_id = find_user_by_email(email, token)
|
||||||
|
if not user_id:
|
||||||
|
log.error(f" User Zitadel introuvable pour {email}")
|
||||||
|
not_found_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if assign_role_to_user(user_id, role, token):
|
||||||
|
success_count += 1
|
||||||
|
else:
|
||||||
|
error_count += 1
|
||||||
|
|
||||||
|
time.sleep(0.15) # Anti rate-limit
|
||||||
|
|
||||||
|
log.info(f"===== FIN =====")
|
||||||
|
log.info(f" ✅ Succès : {success_count}")
|
||||||
|
log.info(f" ⚠️ Sans rôle : {no_role_count}")
|
||||||
|
log.info(f" ❌ Non trouvés : {not_found_count}")
|
||||||
|
log.info(f" 💥 Erreurs : {error_count}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
36
scripts/furious-to-zitadel/extract-distribution-csv.py
Executable file
36
scripts/furious-to-zitadel/extract-distribution-csv.py
Executable file
@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
extract-distribution-csv.py — Extrait les colonnes pour distribution Teams.
|
||||||
|
|
||||||
|
Input : users-credentials.csv (sortie de import-furious-to-zitadel.py)
|
||||||
|
Output : distribution-teams.csv (3 colonnes : Nom complet, Email, Mot de passe)
|
||||||
|
|
||||||
|
Auteur : Hedi Kalboussi
|
||||||
|
Date : 5 juin 2026
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).parent
|
||||||
|
INPUT = SCRIPT_DIR / "users-credentials.csv"
|
||||||
|
OUTPUT = SCRIPT_DIR / "distribution-teams.csv"
|
||||||
|
|
||||||
|
if not INPUT.exists():
|
||||||
|
print(f"❌ Fichier {INPUT} manquant")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
with open(INPUT, newline="", encoding="utf-8") as fin, \
|
||||||
|
open(OUTPUT, "w", newline="", encoding="utf-8") as fout:
|
||||||
|
reader = csv.DictReader(fin)
|
||||||
|
writer = csv.writer(fout)
|
||||||
|
writer.writerow(["Nom complet", "Email", "Mot de passe"])
|
||||||
|
for row in reader:
|
||||||
|
full_name = f"{row['first_name']} {row['last_name']}"
|
||||||
|
writer.writerow([full_name, row["email"], row["password"]])
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
os.chmod(OUTPUT, 0o600)
|
||||||
|
print(f"✅ {count} users écrits dans {OUTPUT}")
|
||||||
101
scripts/furious-to-zitadel/generate-roles-mapping.py
Executable file
101
scripts/furious-to-zitadel/generate-roles-mapping.py
Executable file
@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
generate-roles-mapping.py — Génère le fichier JSON mappant email → rôle.
|
||||||
|
|
||||||
|
Output : roles-mapping.json (utilisé par l'intranet pour le filtrage RBAC côté JS).
|
||||||
|
|
||||||
|
Mapping :
|
||||||
|
- status == "apprenti" → "alternant"
|
||||||
|
- sinon, lookup dans JOB_TITLE_TO_ROLE par job_title
|
||||||
|
|
||||||
|
Stagiaires : ceux avec status="stagiaire" devraient être mappés en "stagiaire" mais
|
||||||
|
attention, certains apparaissent comme "apprenti" dans Furious (à corriger source).
|
||||||
|
|
||||||
|
Auteur : Hedi Kalboussi
|
||||||
|
Date : 5 juin 2026
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).parent
|
||||||
|
RAW_DATA_FILE = SCRIPT_DIR / "furious-users-raw.json"
|
||||||
|
OUTPUT_FILE = SCRIPT_DIR / "roles-mapping.json"
|
||||||
|
|
||||||
|
# Mapping job_title (Furious) → role (Zitadel)
|
||||||
|
JOB_TITLE_TO_ROLE = {
|
||||||
|
# DIRECTION
|
||||||
|
"président": "direction",
|
||||||
|
"directeur": "direction",
|
||||||
|
"directeur associé": "direction",
|
||||||
|
# MANAGER
|
||||||
|
"senior manager": "manager",
|
||||||
|
"sénior manager": "manager",
|
||||||
|
"manager": "manager",
|
||||||
|
"manager bi": "manager",
|
||||||
|
# EXPERT
|
||||||
|
"expert": "expert",
|
||||||
|
"expert n2": "expert",
|
||||||
|
"consultant expert niveau 1": "expert",
|
||||||
|
# CONSULTANT SENIOR
|
||||||
|
"consultant sénior": "consultant_senior",
|
||||||
|
"consultant expérimenté": "consultant_senior",
|
||||||
|
"consultant expérimentée": "consultant_senior",
|
||||||
|
# CONSULTANT JUNIOR
|
||||||
|
"consultant junior": "consultant_junior",
|
||||||
|
# STAGIAIRE
|
||||||
|
"stagiaire": "stagiaire",
|
||||||
|
# SUPPORT
|
||||||
|
"assistante": "support",
|
||||||
|
"assistante de direction": "support",
|
||||||
|
"chargé de recrutement": "support",
|
||||||
|
"marketing": "support",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def determine_role(user):
|
||||||
|
"""Détermine le rôle d'un user selon son status (apprenti) ou job_title."""
|
||||||
|
# Cas spécial : apprenti = alternant
|
||||||
|
if user.get("status") == "apprenti":
|
||||||
|
return "alternant"
|
||||||
|
|
||||||
|
job_title = (user.get("job_title") or "").strip().lower()
|
||||||
|
if not job_title:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return JOB_TITLE_TO_ROLE.get(job_title)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with open(RAW_DATA_FILE) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
actifs = [
|
||||||
|
u for u in data.get("data", {}).get("User", [])
|
||||||
|
if u.get("departure_date") == "0000-00-00"
|
||||||
|
]
|
||||||
|
|
||||||
|
mapping = {}
|
||||||
|
for u in actifs:
|
||||||
|
email = u.get("email", "").lower()
|
||||||
|
role = determine_role(u)
|
||||||
|
if email and role:
|
||||||
|
mapping[email] = role
|
||||||
|
|
||||||
|
# Sauvegarder
|
||||||
|
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(mapping, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Statistiques
|
||||||
|
role_stats = Counter(mapping.values())
|
||||||
|
print(f"✅ Fichier généré : {OUTPUT_FILE}")
|
||||||
|
print(f" Total : {len(mapping)} users mappés")
|
||||||
|
print(f" Sans rôle : {len(actifs) - len(mapping)}")
|
||||||
|
print(f"\nRépartition par rôle :")
|
||||||
|
for role, count in role_stats.most_common():
|
||||||
|
print(f" {role:<20} : {count}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
314
scripts/furious-to-zitadel/import-furious-to-zitadel.py
Executable file
314
scripts/furious-to-zitadel/import-furious-to-zitadel.py
Executable file
@ -0,0 +1,314 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
import-furious-to-zitadel.py — Import des users Furious Squad vers Zitadel.
|
||||||
|
|
||||||
|
Modes :
|
||||||
|
--mode dry-run : simulation sans écriture (par défaut)
|
||||||
|
--mode test : crée uniquement les 3 premiers users (Furious IDs 1, 2, 3)
|
||||||
|
--mode prod : crée tous les users actifs restants
|
||||||
|
|
||||||
|
Génère :
|
||||||
|
- users-credentials.csv : 1 ligne par user avec mdp temporaire
|
||||||
|
- import.log : trace de l'exécution
|
||||||
|
|
||||||
|
Auteur : Hedi Kalboussi
|
||||||
|
Date : 5 juin 2026
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
import requests
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).parent
|
||||||
|
ENV_FILE = SCRIPT_DIR / ".env"
|
||||||
|
KEY_FILE = SCRIPT_DIR / "zitadelfuriousimportkey.json"
|
||||||
|
RAW_DATA_FILE = SCRIPT_DIR / "furious-users-raw.json"
|
||||||
|
CREDENTIALS_CSV = SCRIPT_DIR / "users-credentials.csv"
|
||||||
|
LOG_FILE = SCRIPT_DIR / "import.log"
|
||||||
|
|
||||||
|
ZITADEL_DOMAIN = "https://sso.seizeconsulting.com"
|
||||||
|
|
||||||
|
# IDs Furious pour le mode test (3 premiers)
|
||||||
|
TEST_USER_IDS = [1, 2, 3]
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LOGGING
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(LOG_FILE),
|
||||||
|
logging.StreamHandler(sys.stdout),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# AUTHENTIFICATION ZITADEL (JWT signé)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def get_zitadel_token():
|
||||||
|
"""Génère un JWT signé avec la clé du service account et l'échange contre un access token."""
|
||||||
|
with open(KEY_FILE) as f:
|
||||||
|
key_data = json.load(f)
|
||||||
|
|
||||||
|
user_id = key_data["userId"]
|
||||||
|
key_id = key_data["keyId"]
|
||||||
|
private_key_pem = key_data["key"]
|
||||||
|
|
||||||
|
# Charger la clé privée RSA
|
||||||
|
private_key = serialization.load_pem_private_key(
|
||||||
|
private_key_pem.encode(),
|
||||||
|
password=None,
|
||||||
|
backend=default_backend(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Créer le JWT
|
||||||
|
now = int(time.time())
|
||||||
|
payload = {
|
||||||
|
"iss": user_id,
|
||||||
|
"sub": user_id,
|
||||||
|
"aud": ZITADEL_DOMAIN,
|
||||||
|
"iat": now,
|
||||||
|
"exp": now + 3600, # 1h
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {"kid": key_id}
|
||||||
|
|
||||||
|
assertion = jwt.encode(payload, private_key, algorithm="RS256", headers=headers)
|
||||||
|
|
||||||
|
# Échange contre access token
|
||||||
|
response = requests.post(
|
||||||
|
f"{ZITADEL_DOMAIN}/oauth/v2/token",
|
||||||
|
data={
|
||||||
|
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||||
|
"scope": "openid profile urn:zitadel:iam:org:project:id:zitadel:aud",
|
||||||
|
"assertion": assertion,
|
||||||
|
},
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()["access_token"]
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# FONCTIONS UTILITAIRES
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def generate_password(length=16):
|
||||||
|
"""Génère un mdp temporaire respectant la policy Zitadel (14+ chars, complexe)."""
|
||||||
|
chars = string.ascii_letters + string.digits + "@#$%&*"
|
||||||
|
# Assure au moins 1 maj, 1 min, 1 chiffre, 1 symbole
|
||||||
|
pwd = (
|
||||||
|
random.choice(string.ascii_uppercase) +
|
||||||
|
random.choice(string.ascii_lowercase) +
|
||||||
|
random.choice(string.digits) +
|
||||||
|
random.choice("@#$%&*") +
|
||||||
|
"".join(random.choices(chars, k=length - 4))
|
||||||
|
)
|
||||||
|
return "".join(random.sample(pwd, len(pwd)))
|
||||||
|
|
||||||
|
def determine_username(user):
|
||||||
|
"""Username = email complet (simple et unique)."""
|
||||||
|
return user.get("email", "").strip().lower()
|
||||||
|
|
||||||
|
def build_metadata(user):
|
||||||
|
"""Construit le dict de metadata à attacher à chaque user Zitadel."""
|
||||||
|
return {
|
||||||
|
"furious_id": str(user.get("id", "")),
|
||||||
|
"pseudo": user.get("pseudo", ""),
|
||||||
|
"job_title": user.get("job_title", ""),
|
||||||
|
"bu_name": user.get("bu_name", ""),
|
||||||
|
"manager": user.get("manager_pseudo", ""),
|
||||||
|
"status": user.get("status", ""),
|
||||||
|
"arrival_date": user.get("arrival_date", ""),
|
||||||
|
"entity": user.get("entity_name", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CRÉATION D'UN USER ZITADEL
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def create_user_zitadel(user, token, dry_run=False):
|
||||||
|
"""Crée un user dans Zitadel via l'API v2."""
|
||||||
|
email = user.get("email", "").strip().lower()
|
||||||
|
given_name = user.get("first_name", "").strip()
|
||||||
|
family_name = user.get("last_name", "").strip()
|
||||||
|
|
||||||
|
if not email or not given_name or not family_name:
|
||||||
|
log.warning(f"Données incomplètes pour user Furious id={user.get('id')}, skip")
|
||||||
|
return None
|
||||||
|
|
||||||
|
username = determine_username(user)
|
||||||
|
password = generate_password()
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"username": username,
|
||||||
|
"profile": {
|
||||||
|
"givenName": given_name,
|
||||||
|
"familyName": family_name,
|
||||||
|
"displayName": f"{given_name} {family_name}",
|
||||||
|
"preferredLanguage": "fr",
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"email": email,
|
||||||
|
"isVerified": True,
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"password": password,
|
||||||
|
"changeRequired": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
log.info(f"[DRY-RUN] Would create: {email} ({given_name} {family_name})")
|
||||||
|
return {
|
||||||
|
"userId": "DRY_RUN_ID",
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{ZITADEL_DOMAIN}/v2/users/human",
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 409:
|
||||||
|
log.warning(f"User déjà existant : {email}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not response.ok:
|
||||||
|
log.error(f"Erreur création {email} : {response.status_code} - {response.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
user_id = result.get("userId")
|
||||||
|
log.info(f"✅ Créé : {email} → Zitadel ID {user_id}")
|
||||||
|
|
||||||
|
# Attacher les metadata
|
||||||
|
metadata = build_metadata(user)
|
||||||
|
for key, value in metadata.items():
|
||||||
|
if value:
|
||||||
|
attach_metadata(user_id, key, value, token)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"userId": user_id,
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
|
|
||||||
|
def attach_metadata(user_id, key, value, token):
|
||||||
|
"""Attache une metadata custom à un user."""
|
||||||
|
import base64
|
||||||
|
value_b64 = base64.b64encode(str(value).encode()).decode()
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{ZITADEL_DOMAIN}/management/v1/users/{user_id}/metadata/{key}",
|
||||||
|
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||||
|
json={"value": value_b64},
|
||||||
|
)
|
||||||
|
if not response.ok:
|
||||||
|
log.warning(f" Metadata {key} échouée : {response.status_code}")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MAIN
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
"--mode",
|
||||||
|
choices=["dry-run", "test", "prod"],
|
||||||
|
default="dry-run",
|
||||||
|
help="Mode d'exécution",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
log.info(f"===== Import Furious → Zitadel (mode: {args.mode}) =====")
|
||||||
|
|
||||||
|
# 1. Charger les données Furious
|
||||||
|
if not RAW_DATA_FILE.exists():
|
||||||
|
log.error(f"Fichier {RAW_DATA_FILE} manquant. Lancer read-all-users.sh d'abord.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(RAW_DATA_FILE) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
all_users = data.get("data", {}).get("User", [])
|
||||||
|
|
||||||
|
# Filtrer les actifs (departure_date == "0000-00-00")
|
||||||
|
actifs = [u for u in all_users if u.get("departure_date") == "0000-00-00"]
|
||||||
|
log.info(f"Users actifs dans Furious : {len(actifs)} sur {len(all_users)} total")
|
||||||
|
|
||||||
|
# Filtrer selon le mode
|
||||||
|
if args.mode == "test":
|
||||||
|
users_to_import = [u for u in actifs if u.get("id") in TEST_USER_IDS]
|
||||||
|
else:
|
||||||
|
users_to_import = actifs
|
||||||
|
|
||||||
|
log.info(f"Users à importer : {len(users_to_import)}")
|
||||||
|
|
||||||
|
# 2. Obtenir le token Zitadel (si pas dry-run)
|
||||||
|
token = None
|
||||||
|
if args.mode != "dry-run":
|
||||||
|
log.info("Authentification Zitadel...")
|
||||||
|
token = get_zitadel_token()
|
||||||
|
log.info("✅ Token Zitadel OK")
|
||||||
|
|
||||||
|
# 3. Boucler sur les users
|
||||||
|
results = []
|
||||||
|
for i, user in enumerate(users_to_import, 1):
|
||||||
|
log.info(f"[{i}/{len(users_to_import)}] {user.get('email')}")
|
||||||
|
result = create_user_zitadel(user, token, dry_run=(args.mode == "dry-run"))
|
||||||
|
if result:
|
||||||
|
results.append({
|
||||||
|
"email": user.get("email"),
|
||||||
|
"first_name": user.get("first_name"),
|
||||||
|
"last_name": user.get("last_name"),
|
||||||
|
"pseudo": user.get("pseudo"),
|
||||||
|
"furious_id": user.get("id"),
|
||||||
|
"zitadel_user_id": result["userId"],
|
||||||
|
"password": result["password"],
|
||||||
|
})
|
||||||
|
# Pause anti-rate-limit
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# 4. Sauvegarder le CSV
|
||||||
|
if results and args.mode != "dry-run":
|
||||||
|
with open(CREDENTIALS_CSV, "w", newline="", encoding="utf-8") as f:
|
||||||
|
writer = csv.DictWriter(
|
||||||
|
f,
|
||||||
|
fieldnames=["email", "first_name", "last_name", "pseudo", "furious_id", "zitadel_user_id", "password"],
|
||||||
|
)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(results)
|
||||||
|
os.chmod(CREDENTIALS_CSV, 0o600)
|
||||||
|
log.info(f"✅ CSV sauvegardé : {CREDENTIALS_CSV} ({len(results)} lignes)")
|
||||||
|
|
||||||
|
log.info(f"===== FIN : {len(results)} users importés =====")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
177
scripts/furious-to-zitadel/merge-duplicates.py
Executable file
177
scripts/furious-to-zitadel/merge-duplicates.py
Executable file
@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
merge-duplicates.py — Fusionne les doublons après import.
|
||||||
|
|
||||||
|
Pour chaque doublon :
|
||||||
|
1. Lit les metadata Furious sur le compte "nouveau"
|
||||||
|
2. Les copie sur le compte "ancien" (à garder)
|
||||||
|
3. Supprime le compte "nouveau"
|
||||||
|
|
||||||
|
⚠️ Adapter la variable DUPLICATES pour ton cas.
|
||||||
|
|
||||||
|
Auteur : Hedi Kalboussi
|
||||||
|
Date : 5 juin 2026
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
import requests
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CONFIGURATION — À ADAPTER
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Liste des doublons à fusionner
|
||||||
|
# Format : { "keep_user_id": <id_ancien>, "delete_email": <email_du_nouveau>, "furious_id": <id_furious> }
|
||||||
|
DUPLICATES = [
|
||||||
|
# Exemple :
|
||||||
|
# {
|
||||||
|
# "keep_user_id": "375119988404518915",
|
||||||
|
# "delete_email": "hedi.kalboussi@seizeconsulting.com",
|
||||||
|
# "furious_id": 5,
|
||||||
|
# },
|
||||||
|
]
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).parent
|
||||||
|
KEY_FILE = SCRIPT_DIR / "zitadelfuriousimportkey.json"
|
||||||
|
RAW_DATA_FILE = SCRIPT_DIR / "furious-users-raw.json"
|
||||||
|
|
||||||
|
ZITADEL_DOMAIN = "https://sso.seizeconsulting.com"
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# AUTH
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def get_zitadel_token():
|
||||||
|
with open(KEY_FILE) as f:
|
||||||
|
key_data = json.load(f)
|
||||||
|
private_key = serialization.load_pem_private_key(
|
||||||
|
key_data["key"].encode(), password=None, backend=default_backend()
|
||||||
|
)
|
||||||
|
now = int(time.time())
|
||||||
|
payload = {
|
||||||
|
"iss": key_data["userId"],
|
||||||
|
"sub": key_data["userId"],
|
||||||
|
"aud": ZITADEL_DOMAIN,
|
||||||
|
"iat": now,
|
||||||
|
"exp": now + 3600,
|
||||||
|
}
|
||||||
|
assertion = jwt.encode(
|
||||||
|
payload, private_key, algorithm="RS256", headers={"kid": key_data["keyId"]}
|
||||||
|
)
|
||||||
|
response = requests.post(
|
||||||
|
f"{ZITADEL_DOMAIN}/oauth/v2/token",
|
||||||
|
data={
|
||||||
|
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||||
|
"scope": "openid profile urn:zitadel:iam:org:project:id:zitadel:aud",
|
||||||
|
"assertion": assertion,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()["access_token"]
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# OPÉRATIONS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def find_user_by_email(email, token):
|
||||||
|
response = requests.post(
|
||||||
|
f"{ZITADEL_DOMAIN}/v2/users",
|
||||||
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
|
json={
|
||||||
|
"queries": [
|
||||||
|
{"emailQuery": {"emailAddress": email, "method": "TEXT_QUERY_METHOD_EQUALS"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
users = response.json().get("result", [])
|
||||||
|
return users[0].get("userId") if users else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_metadata(user_id, token):
|
||||||
|
"""Récupère toutes les metadata d'un user."""
|
||||||
|
response = requests.get(
|
||||||
|
f"{ZITADEL_DOMAIN}/management/v1/users/{user_id}/metadata/_search",
|
||||||
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
|
)
|
||||||
|
if not response.ok:
|
||||||
|
return {}
|
||||||
|
result = response.json().get("result", [])
|
||||||
|
return {
|
||||||
|
m["key"]: base64.b64decode(m["value"]).decode()
|
||||||
|
for m in result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_metadata(user_id, key, value, token):
|
||||||
|
value_b64 = base64.b64encode(str(value).encode()).decode()
|
||||||
|
response = requests.post(
|
||||||
|
f"{ZITADEL_DOMAIN}/management/v1/users/{user_id}/metadata/{key}",
|
||||||
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
|
json={"value": value_b64},
|
||||||
|
)
|
||||||
|
return response.ok
|
||||||
|
|
||||||
|
|
||||||
|
def delete_user(user_id, token):
|
||||||
|
response = requests.delete(
|
||||||
|
f"{ZITADEL_DOMAIN}/v2/users/{user_id}",
|
||||||
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
|
)
|
||||||
|
return response.ok
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MAIN
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not DUPLICATES:
|
||||||
|
log.error("Variable DUPLICATES vide. Adapter le script avant exécution.")
|
||||||
|
return
|
||||||
|
|
||||||
|
token = get_zitadel_token()
|
||||||
|
log.info("✅ Token Zitadel OK")
|
||||||
|
|
||||||
|
for dup in DUPLICATES:
|
||||||
|
keep_id = dup["keep_user_id"]
|
||||||
|
delete_email = dup["delete_email"]
|
||||||
|
|
||||||
|
log.info(f"\n=== Doublon : keep={keep_id}, delete={delete_email} ===")
|
||||||
|
|
||||||
|
# 1. Trouver le user "nouveau" à supprimer
|
||||||
|
delete_id = find_user_by_email(delete_email, token)
|
||||||
|
if not delete_id:
|
||||||
|
log.warning(f"User {delete_email} pas trouvé (peut-être déjà fusionné)")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2. Récupérer ses metadata
|
||||||
|
metadata = get_user_metadata(delete_id, token)
|
||||||
|
log.info(f" Metadata récupérées : {list(metadata.keys())}")
|
||||||
|
|
||||||
|
# 3. Les copier sur l'ancien
|
||||||
|
for key, value in metadata.items():
|
||||||
|
if value and set_user_metadata(keep_id, key, value, token):
|
||||||
|
log.info(f" → Copié {key}={value[:30]}...")
|
||||||
|
|
||||||
|
# 4. Supprimer le nouveau
|
||||||
|
if delete_user(delete_id, token):
|
||||||
|
log.info(f" ✅ Supprimé : {delete_email}")
|
||||||
|
else:
|
||||||
|
log.error(f" 💥 Échec suppression : {delete_email}")
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
49
scripts/furious-to-zitadel/read-all-users.sh
Executable file
49
scripts/furious-to-zitadel/read-all-users.sh
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# read-all-users.sh — Récupère la liste des users depuis l'API Furious Squad.
|
||||||
|
# ============================================================================
|
||||||
|
# Output : furious-users-raw.json
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ENV_FILE="$SCRIPT_DIR/.env"
|
||||||
|
OUTPUT="$SCRIPT_DIR/furious-users-raw.json"
|
||||||
|
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
echo "❌ Fichier .env manquant"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
source "$ENV_FILE"
|
||||||
|
|
||||||
|
if [ -z "$FURIOUS_USERNAME" ] || [ -z "$FURIOUS_PASSWORD" ] || [ -z "$FURIOUS_API_URL" ]; then
|
||||||
|
echo "❌ Variables manquantes dans .env"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔄 Récupération des users Furious..."
|
||||||
|
|
||||||
|
# Appel API : User > Search (retourne tous les users actifs et inactifs)
|
||||||
|
curl -s -u "$FURIOUS_USERNAME:$FURIOUS_PASSWORD" \
|
||||||
|
-X POST "$FURIOUS_API_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"User": {
|
||||||
|
"Search": {
|
||||||
|
"fields": ["id", "first_name", "last_name", "email", "pseudo", "job_title", "bu_name", "manager_pseudo", "status", "arrival_date", "departure_date", "entity_name"],
|
||||||
|
"limit": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' > "$OUTPUT"
|
||||||
|
|
||||||
|
# Vérifier qu'on a bien des données
|
||||||
|
TOTAL=$(python3 -c "import json; data=json.load(open('$OUTPUT')); print(len(data.get('data', {}).get('User', [])))")
|
||||||
|
ACTIFS=$(python3 -c "import json; data=json.load(open('$OUTPUT')); print(sum(1 for u in data.get('data', {}).get('User', []) if u.get('departure_date') == '0000-00-00'))")
|
||||||
|
|
||||||
|
echo "✅ Fichier généré : $OUTPUT"
|
||||||
|
echo " Total : $TOTAL users"
|
||||||
|
echo " Actifs : $ACTIFS users"
|
||||||
|
|
||||||
|
chmod 600 "$OUTPUT"
|
||||||
27
scripts/furious-to-zitadel/test-auth.sh
Executable file
27
scripts/furious-to-zitadel/test-auth.sh
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# test-auth.sh — Teste l'authentification à l'API Furious.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/.env"
|
||||||
|
|
||||||
|
echo "🔍 Test auth Furious..."
|
||||||
|
echo "URL : $FURIOUS_API_URL"
|
||||||
|
echo "User : $FURIOUS_USERNAME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test simple : lecture d'un user (id=1)
|
||||||
|
curl -s -u "$FURIOUS_USERNAME:$FURIOUS_PASSWORD" \
|
||||||
|
-X POST "$FURIOUS_API_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"User": {
|
||||||
|
"Search": {
|
||||||
|
"fields": ["id", "first_name"],
|
||||||
|
"limit": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' | python3 -m json.tool
|
||||||
239
scripts/intranet/index.html.template
Normal file
239
scripts/intranet/index.html.template
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Seize Consulting — Intranet</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter+Tight:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
============================================================================
|
||||||
|
TEMPLATE INTRANET SEIZE CONSULTING
|
||||||
|
============================================================================
|
||||||
|
Ce fichier est un template. Le vrai fichier en production est sur le serveur
|
||||||
|
sur ~/docker/intranet/index.html.
|
||||||
|
|
||||||
|
Points clés :
|
||||||
|
- __USER_EMAIL__ est un placeholder substitué par nginx (sub_filter)
|
||||||
|
- Chaque carte service-card a un attribut data-app="<nom>" pour le RBAC
|
||||||
|
- Le JavaScript en bas fait le filtrage selon le rôle
|
||||||
|
============================================================================
|
||||||
|
-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--c-bg-deep: #071C1C;
|
||||||
|
--c-surface: rgba(8, 57, 52, 0.6);
|
||||||
|
--c-surface-2: rgba(169, 212, 211, 0.04);
|
||||||
|
--c-accent: #008A81;
|
||||||
|
--c-accent-bright: #00C9B7;
|
||||||
|
--c-mint: #A9D4D3;
|
||||||
|
--c-lime: #F3FBBA;
|
||||||
|
--c-cream: #FBFBF4;
|
||||||
|
--c-text-dim: rgba(251, 251, 244, 0.55);
|
||||||
|
--c-text-muted: rgba(251, 251, 244, 0.4);
|
||||||
|
--c-border: rgba(169, 212, 211, 0.12);
|
||||||
|
--c-border-bright: rgba(169, 212, 211, 0.3);
|
||||||
|
--font-display: 'Space Grotesk', sans-serif;
|
||||||
|
--font-body: 'Inter Tight', sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
html, body {
|
||||||
|
background: var(--c-bg-deep);
|
||||||
|
color: var(--c-cream);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ⚠️ Les styles complets (orbs, animations, etc.) sont dans le fichier réel.
|
||||||
|
Ici on garde le minimum pour ne pas surcharger le template. */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative; z-index: 10;
|
||||||
|
max-width: 1400px; margin: 0 auto; padding: 60px 40px;
|
||||||
|
}
|
||||||
|
.brand {
|
||||||
|
display: flex; align-items: center; gap: 16px; margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
.brand-logo img { height: 50px; }
|
||||||
|
.brand-title {
|
||||||
|
font-family: var(--font-display); font-size: 28px; font-weight: 700;
|
||||||
|
}
|
||||||
|
.intro {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
.intro h1 {
|
||||||
|
font-family: var(--font-display); font-size: 48px; font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.services-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.service-card {
|
||||||
|
background: var(--c-surface);
|
||||||
|
border: 1px solid var(--c-border);
|
||||||
|
border-radius: 16px; padding: 24px;
|
||||||
|
display: flex; flex-direction: column; gap: 12px;
|
||||||
|
text-decoration: none; color: inherit;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.service-card:hover {
|
||||||
|
border-color: var(--c-border-bright);
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-family: var(--font-display); font-size: 22px; font-weight: 600;
|
||||||
|
}
|
||||||
|
.card-desc {
|
||||||
|
color: var(--c-text-dim); font-size: 14px; line-height: 1.5;
|
||||||
|
}
|
||||||
|
.card-meta {
|
||||||
|
display: flex; gap: 8px; flex-wrap: wrap; font-family: var(--font-mono); font-size: 11px;
|
||||||
|
}
|
||||||
|
.card-url { color: var(--c-mint); }
|
||||||
|
.card-tag {
|
||||||
|
background: var(--c-surface-2); padding: 4px 10px; border-radius: 12px;
|
||||||
|
color: var(--c-accent-bright);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="brand-logo"><img src="/logo.png" alt="Seize"></div>
|
||||||
|
<div class="brand-title">Seize Consulting</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="intro">
|
||||||
|
<h1>Tableau de bord</h1>
|
||||||
|
<p>Accédez à vos outils internes</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="services-grid">
|
||||||
|
|
||||||
|
<!-- ⚠️ Chaque carte a un attribut data-app="<nom>" pour le RBAC -->
|
||||||
|
<!-- Le JavaScript en bas du fichier filtre ces cartes selon le rôle -->
|
||||||
|
|
||||||
|
<!-- PORTAINER -->
|
||||||
|
<a class="service-card" data-app="portainer" href="https://portainer.seizeconsulting.com" target="_blank" rel="noopener">
|
||||||
|
<div class="card-title">Portainer</div>
|
||||||
|
<div class="card-desc">Console de gestion des conteneurs Docker.</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span class="card-url">portainer.seizeconsulting.com</span>
|
||||||
|
<span class="card-tag">Infra</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- UPTIME KUMA / STATUS -->
|
||||||
|
<a class="service-card" data-app="status" href="https://status.seizeconsulting.com" target="_blank" rel="noopener">
|
||||||
|
<div class="card-title">Status</div>
|
||||||
|
<div class="card-desc">Surveillance de la disponibilité des services.</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span class="card-url">status.seizeconsulting.com</span>
|
||||||
|
<span class="card-tag">Monitoring</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- N8N -->
|
||||||
|
<a class="service-card" data-app="n8n" href="https://n8n.seizeconsulting.com" target="_blank" rel="noopener">
|
||||||
|
<div class="card-title">n8n</div>
|
||||||
|
<div class="card-desc">Plateforme d'automatisation de workflows.</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span class="card-url">n8n.seizeconsulting.com</span>
|
||||||
|
<span class="card-tag">Automation</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- GITEA -->
|
||||||
|
<a class="service-card" data-app="gitea" href="https://git.seizeconsulting.com" target="_blank" rel="noopener">
|
||||||
|
<div class="card-title">Gitea</div>
|
||||||
|
<div class="card-desc">Dépôts Git et gestion collaborative du code.</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span class="card-url">git.seizeconsulting.com</span>
|
||||||
|
<span class="card-tag">Code</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- CODE SERVER -->
|
||||||
|
<a class="service-card" data-app="code" href="https://code.seizeconsulting.com" target="_blank" rel="noopener">
|
||||||
|
<div class="card-title">Code Server</div>
|
||||||
|
<div class="card-desc">Environnement VS Code dans le navigateur.</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span class="card-url">code.seizeconsulting.com</span>
|
||||||
|
<span class="card-tag">IDE</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- TRAEFIK -->
|
||||||
|
<a class="service-card" data-app="traefik" href="https://traefik.seizeconsulting.com" target="_blank" rel="noopener">
|
||||||
|
<div class="card-title">Traefik</div>
|
||||||
|
<div class="card-desc">Reverse proxy et routage du trafic.</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span class="card-url">traefik.seizeconsulting.com</span>
|
||||||
|
<span class="card-tag">Réseau</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div>SEIZE CONSULTING · © 2026</div>
|
||||||
|
<div>Hébergé en France · Made with care</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* ============================================================================
|
||||||
|
RBAC INTRANET — Filtrage des cartes selon rôle Zitadel
|
||||||
|
============================================================================
|
||||||
|
- __USER_EMAIL__ est remplacé par nginx (sub_filter) par la valeur du
|
||||||
|
header X-Forwarded-Email (envoyé par OAuth2-Proxy après auth Zitadel)
|
||||||
|
- On fetch /roles-mapping.json (généré par generate-roles-mapping.py)
|
||||||
|
- Pour chaque carte service-card[data-app], on cache si rôle pas autorisé
|
||||||
|
============================================================================ */
|
||||||
|
(async function() {
|
||||||
|
const USER_EMAIL = "__USER_EMAIL__"; // ← Substitué par nginx sub_filter
|
||||||
|
|
||||||
|
/* Matrice RBAC : qui voit quelle carte */
|
||||||
|
const ACCESS_MATRIX = {
|
||||||
|
"portainer": ["direction", "manager"],
|
||||||
|
"status": ["direction"],
|
||||||
|
"n8n": ["direction", "manager", "expert"],
|
||||||
|
"gitea": ["direction", "manager", "expert", "consultant_senior", "consultant_junior", "alternant", "stagiaire", "support"],
|
||||||
|
"code": ["direction", "manager", "expert", "consultant_senior", "consultant_junior", "alternant", "stagiaire", "support"],
|
||||||
|
"traefik": ["direction"]
|
||||||
|
};
|
||||||
|
|
||||||
|
let userRole = null;
|
||||||
|
try {
|
||||||
|
const response = await fetch("/roles-mapping.json", { cache: "no-store" });
|
||||||
|
if (response.ok) {
|
||||||
|
const mapping = await response.json();
|
||||||
|
userRole = mapping[USER_EMAIL.toLowerCase()] || null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[RBAC] Erreur chargement mapping:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[RBAC] User:", USER_EMAIL, "| Rôle:", userRole);
|
||||||
|
|
||||||
|
/* Filtrer les cartes selon le rôle */
|
||||||
|
document.querySelectorAll(".service-card[data-app]").forEach(card => {
|
||||||
|
const app = card.dataset.app;
|
||||||
|
const allowedRoles = ACCESS_MATRIX[app] || [];
|
||||||
|
if (!userRole || !allowedRoles.includes(userRole)) {
|
||||||
|
card.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user