Files
document/RBER Connect/keycloak-setup.md

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

  1. Contexte
  2. Prérequis
  3. Architecture concernée
  4. Étape 1 — Obtenir un token admin
  5. Étape 2 — Créer le client api-admin dans chaque realm
  6. Étape 3 — Récupérer les secrets des clients
  7. Étape 4 — Attribuer les rôles au service account
  8. Étape 5 — Injecter les secrets dans Kubernetes
  9. Étape 6 — Redémarrer les pods
  10. Étape 7 — Vérifier le fonctionnement
  11. Procédure via l'interface web Keycloak
  12. Rotation des secrets
  13. Dépannage
  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.


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

  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.
  1. Clique Next
  2. 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)
  1. 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
  1. 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