21 KiB
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
- Contexte
- Prérequis
- Architecture concernée
- Étape 1 — Obtenir un token admin
- Étape 2 — Créer le client api-admin dans chaque realm
- Étape 3 — Récupérer les secrets des clients
- Étape 4 — Attribuer les rôles au service account
- Étape 5 — Injecter les secrets dans Kubernetes
- Étape 6 — Redémarrer les pods
- Étape 7 — Vérifier le fonctionnement
- Procédure via l'interface web Keycloak
- Rotation des secrets
- Dépannage
- 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 :
- Valider les tokens JWT des utilisateurs qui appellent l'API.
- 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.
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.
# Variables à adapter
KEYCLOAK_URL="https://auth.rber.com"
ADMIN_USER="admin"
ADMIN_PASS="<mot_de_passe_admin_keycloak>"
# 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.
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 :
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.
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.
# 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="<secret_realm_uac>" \
--from-literal=UNA_KC_SECRET="<secret_realm_una>" \
--from-literal=UP_KC_SECRET="<secret_realm_univ-parakou>" \
--from-literal=UNSTIM_KC_SECRET="<secret_realm_unstim>" \
-n ldap-api \
--dry-run=client -o yaml | kubectl apply -f -
Vérification :
# 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.
# 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
curl -s https://uac.api.rber.bj/health | python3 -m json.tool
Résultat attendu — tous les composants healthy :
{
"status": "healthy",
"components": {
"database": "healthy",
"redis": "healthy",
"ldap": "healthy",
"keycloak": "healthy"
}
}
10.2 Obtenir un token de test
KEYCLOAK_URL="https://auth.rber.com"
REALM="uac"
CLIENT_SECRET="<secret_realm_uac>"
# 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
# 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
# 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
- Connecte-toi à
https://auth.rber.com/admin - Sélectionne le realm dans le menu déroulant en haut à gauche (ex :
uac) - Menu latéral → Clients → bouton Create client
- 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. |
- Clique Next
- 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) |
- Clique Next puis Save
11.2 Récupérer le secret
- Dans la page du client
api-admin, onglet Credentials - Le champ Client secret contient la valeur à noter
- Note cette valeur — tu en auras besoin pour l'injection Kubernetes
11.3 Attribuer les rôles au service account
- Dans la page du client
api-admin, onglet Service account roles - Clique Assign role
- Dans le filtre, sélectionne Filter by clients
- Cherche
realm-management - Coche les rôles suivants :
| Rôle | Coché |
|---|---|
manage-users |
✅ |
query-users |
✅ |
view-users |
✅ |
query-groups |
✅ |
manage-clients |
✅ |
- 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 :
# 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="<garder_ancien_si_pas_changé>" \
--from-literal=UP_KC_SECRET="<garder_ancien_si_pas_changé>" \
--from-literal=UNSTIM_KC_SECRET="<garder_ancien_si_pas_changé>" \
-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 :
# 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 |