# Procédure de configuration Keycloak pour l'API LDAP+ | Info | Valeur | |------|--------| | **Projet** | API LDAP+ — Universités Béninoises | | **Référence** | `docs/keycloak-setup.md` | | **Version** | 1.0 | | **Date** | Mars 2026 | | **Auteur** | IDADU TECH | | **Destinataire** | Administrateur Keycloak (auth.rber.com) | --- ## Sommaire 1. [Contexte](#1-contexte) 2. [Prérequis](#2-prérequis) 3. [Architecture concernée](#3-architecture-concernée) 4. [Étape 1 — Obtenir un token admin](#4-étape-1--obtenir-un-token-admin) 5. [Étape 2 — Créer le client api-admin dans chaque realm](#5-étape-2--créer-le-client-api-admin-dans-chaque-realm) 6. [Étape 3 — Récupérer les secrets des clients](#6-étape-3--récupérer-les-secrets-des-clients) 7. [Étape 4 — Attribuer les rôles au service account](#7-étape-4--attribuer-les-rôles-au-service-account) 8. [Étape 5 — Injecter les secrets dans Kubernetes](#8-étape-5--injecter-les-secrets-dans-kubernetes) 9. [Étape 6 — Redémarrer les pods](#9-étape-6--redémarrer-les-pods) 10. [Étape 7 — Vérifier le fonctionnement](#10-étape-7--vérifier-le-fonctionnement) 11. [Procédure via l'interface web Keycloak](#11-procédure-via-linterface-web-keycloak) 12. [Rotation des secrets](#12-rotation-des-secrets) 13. [Dépannage](#13-dépannage) 14. [Annexe — Résumé des rôles et accès](#14-annexe--résumé-des-rôles-et-accès) --- ## 1. Contexte L'API LDAP+ est un service REST déployé sur le cluster RKE2 de RBER. Elle gère la structure académique (facultés, filières, groupes d'étudiants) des universités béninoises et synchronise ces groupes vers Keycloak. L'API a besoin d'un **client Keycloak de type confidentiel** dans chaque realm universitaire pour : 1. **Valider les tokens JWT** des utilisateurs qui appellent l'API. 2. **Appeler l'Admin API Keycloak** pour créer/modifier des groupes et y affecter des utilisateurs (synchronisation). Ce client s'appelle `api-admin`. Il doit être créé dans les 4 realms suivants : | Realm | Université | |-------|-----------| | `uac` | Université d'Abomey-Calavi | | `una` | Université Nationale d'Agriculture | | `univ-parakou` | Université de Parakou | | `unstim` | UNSTIM | --- ## 2. Prérequis | Élément | Détail | |---------|--------| | Accès admin Keycloak | Compte avec droits admin sur `https://auth.rber.com` (realm `master` ou droits admin sur les 4 realms) | | curl | Installé sur la machine depuis laquelle tu exécutes les commandes | | python3 | Pour parser les réponses JSON (installé par défaut sur la plupart des Linux) | | kubectl | Accès au cluster RKE2 avec droits sur le namespace `ldap-api` (pour l'injection des secrets) | > **Note :** si tu préfères utiliser l'interface web de Keycloak plutôt que les commandes curl, va directement à la [section 11](#11-procédure-via-linterface-web-keycloak). --- ## 3. Architecture concernée ``` Applications universitaires │ │ JWT (émis par Keycloak) ▼ ┌──────────────────────────────────┐ │ API LDAP+ (api.rber.bj) │ │ │ │ 1. Valide le JWT │──── Keycloak (auth.rber.com) │ (clés publiques du realm) │ ← c'est ici qu'on crée le client │ │ │ 2. Sync groupes → Keycloak │──── Keycloak Admin API │ (via client api-admin) │ ← c'est ici qu'on a besoin du secret └──────────────────────────────────┘ ``` --- ## 4. Étape 1 — Obtenir un token admin Ce token permet d'appeler l'Admin API de Keycloak pour créer les clients. ```bash # Variables à adapter KEYCLOAK_URL="https://auth.rber.com" ADMIN_USER="admin" ADMIN_PASS="" # Obtenir le token TOKEN=$(curl -s -X POST \ "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \ -d "client_id=admin-cli" \ -d "username=${ADMIN_USER}" \ -d "password=${ADMIN_PASS}" \ -d "grant_type=password" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") # Vérifier que le token est récupéré if [ -z "$TOKEN" ]; then echo "ERREUR : impossible d'obtenir le token admin." echo "Vérifie le nom d'utilisateur, le mot de passe et l'URL." exit 1 fi echo "Token admin obtenu (${#TOKEN} caractères)" ``` > **Durée de validité :** le token admin expire au bout de 60 secondes par défaut. Si les étapes suivantes échouent avec une erreur 401, relance cette commande pour en obtenir un nouveau. --- ## 5. Étape 2 — Créer le client api-admin dans chaque realm Le script suivant crée un client `api-admin` dans les 4 realms. ```bash for REALM in uac una univ-parakou unstim; do echo "" echo "=== Realm : ${REALM} ===" HTTP_CODE=$(curl -s -o /tmp/kc_response_${REALM}.json -w "%{http_code}" \ -X POST "${KEYCLOAK_URL}/admin/realms/${REALM}/clients" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "clientId": "api-admin", "name": "API LDAP+ Administration", "description": "Client utilisé par l'\''API LDAP+ pour valider les JWT et synchroniser les groupes académiques.", "enabled": true, "protocol": "openid-connect", "publicClient": false, "serviceAccountsEnabled": true, "authorizationServicesEnabled": false, "directAccessGrantsEnabled": false, "standardFlowEnabled": false, "clientAuthenticatorType": "client-secret", "defaultClientScopes": ["email", "profile", "roles"], "attributes": { "use.refresh.tokens": "false" } }') case "$HTTP_CODE" in 201) echo " SUCCÈS — client api-admin créé" ;; 409) echo " EXISTE DÉJÀ — aucune action nécessaire" ;; 401) echo " ERREUR 401 — token expiré, relance l'étape 1" ;; *) echo " ERREUR HTTP ${HTTP_CODE} :" cat /tmp/kc_response_${REALM}.json echo "" ;; esac done ``` ### Explication des paramètres du client | Paramètre | Valeur | Rôle | |-----------|--------|------| | `publicClient` | `false` | Client confidentiel : un `client_secret` est généré. Sans ça, pas de secret. | | `serviceAccountsEnabled` | `true` | Active un compte de service. L'API utilise ce compte pour appeler l'Admin API Keycloak (sync des groupes). | | `standardFlowEnabled` | `false` | Désactive le flux de login navigateur. L'API ne redirige pas des utilisateurs vers Keycloak, elle valide des tokens déjà émis. | | `directAccessGrantsEnabled` | `false` | Désactive le Resource Owner Password Grant. L'API ne collecte jamais de mots de passe. | | `clientAuthenticatorType` | `client-secret` | Méthode d'authentification du client auprès de Keycloak. | --- ## 6. Étape 3 — Récupérer les secrets des clients Après la création, chaque client a un `client_secret` généré automatiquement. Ce script le récupère : ```bash echo "" echo "============================================" echo " SECRETS CLIENT api-admin PAR REALM" echo "============================================" echo "" echo " CONSERVE CES VALEURS EN LIEU SÛR." echo " NE LES PARTAGE PAS PAR EMAIL OU CHAT." echo "" echo "--------------------------------------------" for REALM in uac una univ-parakou unstim; do # Trouver l'ID interne du client (UUID Keycloak, différent du clientId) CLIENT_UUID=$(curl -s \ "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=api-admin" \ -H "Authorization: Bearer ${TOKEN}" \ | python3 -c "import sys,json; data=json.load(sys.stdin); print(data[0]['id'] if data else 'NOT_FOUND')") if [ "$CLIENT_UUID" = "NOT_FOUND" ]; then echo " ${REALM}: ERREUR — client api-admin introuvable dans ce realm" continue fi # Récupérer le secret SECRET=$(curl -s \ "${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/client-secret" \ -H "Authorization: Bearer ${TOKEN}" \ | python3 -c "import sys,json; print(json.load(sys.stdin).get('value', 'ERREUR'))") echo " ${REALM}: ${SECRET}" done echo "" echo "--------------------------------------------" ``` **Sortie attendue :** ``` uac: a1b2c3d4-e5f6-7890-abcd-ef1234567890 una: b2c3d4e5-f6a7-8901-bcde-f12345678901 univ-parakou: c3d4e5f6-a7b8-9012-cdef-123456789012 unstim: d4e5f6a7-b8c9-0123-defa-234567890123 ``` > **Note ces valeurs.** Tu en auras besoin pour l'étape 5. --- ## 7. Étape 4 — Attribuer les rôles au service account Le compte de service du client `api-admin` a besoin de droits pour gérer les groupes et lire les utilisateurs dans chaque realm. Sans ces rôles, la synchronisation des groupes académiques échouera. ```bash for REALM in uac una univ-parakou unstim; do echo "" echo "=== Attribution des rôles — realm ${REALM} ===" # ID du client api-admin CLIENT_UUID=$(curl -s \ "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=api-admin" \ -H "Authorization: Bearer ${TOKEN}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])") # ID du client realm-management (client interne qui porte les rôles d'admin) REALM_MGMT_UUID=$(curl -s \ "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=realm-management" \ -H "Authorization: Bearer ${TOKEN}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])") # ID du service account (l'utilisateur technique créé automatiquement) SA_USER_ID=$(curl -s \ "${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/service-account-user" \ -H "Authorization: Bearer ${TOKEN}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") # Rôles nécessaires : # manage-users → créer/modifier/supprimer des utilisateurs dans les groupes # query-users → lister les utilisateurs # query-groups → lister les groupes # manage-clients → (optionnel) gérer les scopes si nécessaire # view-users → voir le détail des utilisateurs for ROLE_NAME in manage-users query-groups query-users manage-clients view-users; do # Récupérer la définition JSON du rôle ROLE_JSON=$(curl -s \ "${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${REALM_MGMT_UUID}/roles/${ROLE_NAME}" \ -H "Authorization: Bearer ${TOKEN}") # Vérifier que le rôle existe ROLE_CHECK=$(echo "$ROLE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('name','ERREUR'))" 2>/dev/null) if [ "$ROLE_CHECK" = "ERREUR" ] || [ -z "$ROLE_CHECK" ]; then echo " ATTENTION : rôle ${ROLE_NAME} introuvable dans realm-management de ${REALM}" continue fi # Attribuer le rôle au service account HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ -X POST "${KEYCLOAK_URL}/admin/realms/${REALM}/users/${SA_USER_ID}/role-mappings/clients/${REALM_MGMT_UUID}" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/json" \ -d "[${ROLE_JSON}]") if [ "$HTTP_CODE" = "204" ]; then echo " ${ROLE_NAME} → attribué" else echo " ${ROLE_NAME} → ERREUR HTTP ${HTTP_CODE}" fi done done ``` ### Récapitulatif des rôles attribués | Rôle realm-management | Usage par l'API LDAP+ | |-----------------------|-----------------------| | `manage-users` | Affecter/retirer des utilisateurs des groupes Keycloak | | `query-users` | Lister les utilisateurs d'un realm | | `view-users` | Voir le détail d'un utilisateur | | `query-groups` | Lister les groupes existants dans un realm | | `manage-clients` | Optionnel — gestion des scopes si nécessaire | --- ## 8. Étape 5 — Injecter les secrets dans Kubernetes Cette étape est exécutée par la personne ayant accès au cluster RKE2. ```bash # Remplacer les valeurs ci-dessous par les vrais secrets récupérés à l'étape 3 kubectl create secret generic keycloak-secrets \ --from-literal=UAC_KC_SECRET="" \ --from-literal=UNA_KC_SECRET="" \ --from-literal=UP_KC_SECRET="" \ --from-literal=UNSTIM_KC_SECRET="" \ -n ldap-api \ --dry-run=client -o yaml | kubectl apply -f - ``` **Vérification :** ```bash # Le secret doit contenir 4 clés kubectl get secret keycloak-secrets -n ldap-api -o jsonpath='{.data}' | python3 -c " import sys,json data = json.load(sys.stdin) for key in sorted(data.keys()): print(f' {key}: (encodé, {len(data[key])} chars)') " ``` --- ## 9. Étape 6 — Redémarrer les pods Les pods doivent redémarrer pour charger les nouveaux secrets depuis les variables d'environnement. ```bash # Redémarrer les pods API kubectl rollout restart deployment/ldap-api -n ldap-api # Redémarrer les workers Celery (sync Keycloak) kubectl rollout restart deployment/celery-worker -n ldap-workers kubectl rollout restart deployment/celery-beat -n ldap-workers # Surveiller le redémarrage kubectl get pods -n ldap-api -w kubectl get pods -n ldap-workers -w ``` **Résultat attendu :** tous les pods en état `Running` avec `READY 1/1`. --- ## 10. Étape 7 — Vérifier le fonctionnement ### 10.1 Health check ```bash curl -s https://uac.api.rber.bj/health | python3 -m json.tool ``` Résultat attendu — tous les composants `healthy` : ```json { "status": "healthy", "components": { "database": "healthy", "redis": "healthy", "ldap": "healthy", "keycloak": "healthy" } } ``` ### 10.2 Obtenir un token de test ```bash KEYCLOAK_URL="https://auth.rber.com" REALM="uac" CLIENT_SECRET="" # Token via client credentials (service account) TOKEN=$(curl -s -X POST \ "${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token" \ -d "client_id=api-admin" \ -d "client_secret=${CLIENT_SECRET}" \ -d "grant_type=client_credentials" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") echo "Token obtenu (${#TOKEN} caractères)" ``` ### 10.3 Tester les endpoints protégés ```bash # Lister les facultés (doit retourner une liste, même vide) curl -s https://uac.api.rber.bj/faculties \ -H "Authorization: Bearer ${TOKEN}" | python3 -m json.tool # Lister les utilisateurs LDAP (5 premiers) curl -s "https://uac.api.rber.bj/users?limit=5" \ -H "Authorization: Bearer ${TOKEN}" | python3 -m json.tool # Lister les groupes curl -s https://uac.api.rber.bj/groups \ -H "Authorization: Bearer ${TOKEN}" | python3 -m json.tool # Statut de la synchronisation curl -s https://uac.api.rber.bj/sync/status \ -H "Authorization: Bearer ${TOKEN}" | python3 -m json.tool ``` ### 10.4 Tester la synchronisation Keycloak ```bash # Déclencher une sync manuelle curl -s -X POST https://uac.api.rber.bj/sync/keycloak \ -H "Authorization: Bearer ${TOKEN}" | python3 -m json.tool ``` ### 10.5 Vérifier dans Keycloak Après une sync réussie, connecte-toi à `https://auth.rber.com`, va dans le realm `uac` → Groups. Tu dois voir apparaître l'arborescence : ``` /facultes /fast /informatique /L1-2024-2025 /L2-2024-2025 ... ``` --- ## 11. Procédure via l'interface web Keycloak Si tu préfères utiliser l'interface graphique plutôt que les commandes curl, voici la marche à suivre. **Répète cette procédure pour chaque realm** (uac, una, univ-parakou, unstim). ### 11.1 Créer le client 1. Connecte-toi à `https://auth.rber.com/admin` 2. Sélectionne le realm dans le menu déroulant en haut à gauche (ex : `uac`) 3. Menu latéral → **Clients** → bouton **Create client** 4. Remplis les champs : | Champ | Valeur | |-------|--------| | Client type | OpenID Connect | | Client ID | `api-admin` | | Name | `API LDAP+ Administration` | | Description | `Client utilisé par l'API LDAP+ pour valider les JWT et synchroniser les groupes académiques.` | 5. Clique **Next** 6. Configuration d'authentification : | Champ | Valeur | |-------|--------| | Client authentication | **ON** (active le mode confidentiel → génère un secret) | | Authorization | OFF | | Standard flow | **OFF** | | Direct access grants | **OFF** | | Service accounts roles | **ON** (nécessaire pour la sync) | 7. Clique **Next** puis **Save** ### 11.2 Récupérer le secret 1. Dans la page du client `api-admin`, onglet **Credentials** 2. Le champ **Client secret** contient la valeur à noter 3. **Note cette valeur** — tu en auras besoin pour l'injection Kubernetes ### 11.3 Attribuer les rôles au service account 1. Dans la page du client `api-admin`, onglet **Service account roles** 2. Clique **Assign role** 3. Dans le filtre, sélectionne **Filter by clients** 4. Cherche `realm-management` 5. Coche les rôles suivants : | Rôle | Coché | |------|-------| | `manage-users` | ✅ | | `query-users` | ✅ | | `view-users` | ✅ | | `query-groups` | ✅ | | `manage-clients` | ✅ | 6. Clique **Assign** ### 11.4 Répéter Répète les sections 11.1 à 11.3 pour les realms `una`, `univ-parakou` et `unstim`. --- ## 12. Rotation des secrets Si un secret est compromis ou si la politique de sécurité exige une rotation périodique : ```bash # 1. Régénérer le secret dans Keycloak (exemple pour le realm uac) REALM="uac" CLIENT_UUID=$(curl -s \ "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=api-admin" \ -H "Authorization: Bearer ${TOKEN}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])") # Générer un nouveau secret NEW_SECRET=$(curl -s -X POST \ "${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/client-secret" \ -H "Authorization: Bearer ${TOKEN}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['value'])") echo "Nouveau secret pour ${REALM}: ${NEW_SECRET}" # 2. Mettre à jour le secret Kubernetes kubectl create secret generic keycloak-secrets \ --from-literal=UAC_KC_SECRET="${NEW_SECRET}" \ --from-literal=UNA_KC_SECRET="" \ --from-literal=UP_KC_SECRET="" \ --from-literal=UNSTIM_KC_SECRET="" \ -n ldap-api \ --dry-run=client -o yaml | kubectl apply -f - # 3. Redémarrer les pods kubectl rollout restart deployment/ldap-api -n ldap-api kubectl rollout restart deployment/celery-worker -n ldap-workers ``` --- ## 13. Dépannage ### Le token admin ne s'obtient pas (étape 1) | Symptôme | Cause probable | Solution | |----------|---------------|----------| | `{"error":"invalid_grant"}` | Mauvais identifiant ou mot de passe | Vérifie les credentials admin | | `curl: (7) Failed to connect` | Keycloak inaccessible | Vérifie que `auth.rber.com` est joignable depuis ta machine | | `{"error":"unauthorized_client"}` | Le client `admin-cli` est désactivé | Active-le dans le realm `master` → Clients → `admin-cli` → Enabled: ON | ### La création du client échoue (étape 2) | Code HTTP | Cause | Solution | |-----------|-------|----------| | 401 | Token expiré | Relance l'étape 1 | | 403 | Pas les droits admin sur le realm | Vérifie que ton compte a le rôle `admin` dans le realm `master` | | 409 | Le client `api-admin` existe déjà | Passe à l'étape 3 | ### Le health check retourne `keycloak: unhealthy` (étape 7) | Cause probable | Solution | |---------------|----------| | Secret incorrect dans Kubernetes | Relance l'étape 3 pour récupérer les vrais secrets, puis étape 5 | | Pods pas redémarrés | Relance l'étape 6 | | Keycloak inaccessible depuis le cluster | Vérifier que les pods peuvent atteindre `auth.rber.com:443` | ### Les endpoints protégés retournent 401 | Cause probable | Solution | |---------------|----------| | Token expiré | Récupère un nouveau token (étape 7.2) | | Client `api-admin` désactivé | Vérifie dans Keycloak → Clients → api-admin → Enabled: ON | | Secret ne correspond pas | Régénère le secret (section 12) | ### La sync Keycloak échoue | Cause probable | Solution | |---------------|----------| | Rôles service account manquants | Relance l'étape 4 | | `403 Forbidden` dans les logs | Le service account n'a pas `manage-users` — voir étape 4 | | Pas de groupes dans Keycloak après sync | Vérifie qu'il y a des groupes dans PostgreSQL (`GET /groups`) | Consulter les logs des pods : ```bash # Logs API kubectl logs -l app=ldap-api -n ldap-api --tail=50 # Logs Celery Worker (sync) kubectl logs -l app=celery-worker -n ldap-workers --tail=50 ``` --- ## 14. Annexe — Résumé des rôles et accès ### Ce que le client api-admin peut faire | Action | Realm concerné | Utilisé pour | |--------|---------------|-------------| | Valider des tokens JWT | Chaque realm | Authentification des requêtes API | | Lister les utilisateurs | Chaque realm | Vérification d'existence (sync) | | Créer/modifier/supprimer des groupes | Chaque realm | Synchronisation de la structure académique | | Affecter des utilisateurs à des groupes | Chaque realm | Synchronisation des membres | ### Ce que le client api-admin ne peut PAS faire | Action | Raison | |--------|--------| | Connecter des utilisateurs (login) | `standardFlowEnabled: false` | | Collecter des mots de passe | `directAccessGrantsEnabled: false` | | Modifier la configuration du realm | Pas de rôle `realm-admin` | | Accéder aux données d'un autre realm | Chaque client est limité à son realm | ### Variables d'environnement attendues par l'API | Variable | Secret Kubernetes | Contenu | |----------|-------------------|---------| | `UAC_KC_SECRET` | `keycloak-secrets` | Client secret du realm `uac` | | `UNA_KC_SECRET` | `keycloak-secrets` | Client secret du realm `una` | | `UP_KC_SECRET` | `keycloak-secrets` | Client secret du realm `univ-parakou` | | `UNSTIM_KC_SECRET` | `keycloak-secrets` | Client secret du realm `unstim` |