# Keycloak IAM - Universités du Bénin ## Documentation Technique Complète --- **Version :** 1.0 **Date :** 5 Janvier 2026 **Projet :** IAM Multi-Universités (UAC, UNA, UNSTIM, UP) **Infrastructure :** RKE2 / Kubernetes --- # Table des matières 1. [Architecture Générale](#1-architecture-générale) 2. [Document d'Installation](#2-document-dinstallation) 3. [Document d'Administration Générale](#3-document-dadministration-générale) 4. [Administration par Realm](#4-administration-par-realm) 5. [Procédures Opérationnelles](#5-procédures-opérationnelles) 6. [Dépannage](#6-dépannage) --- # 1. Architecture Générale ## 1.1 Vue d'ensemble ``` ┌─────────────────────────────────────────────────────────────────────┐ │ INTERNET │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────┐ │ 102.222.216.6 │ │ (NAT/DNS) │ └────────┬────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ HAProxy Externe │ │ (rber-ldb-int-01) │ │ 10.29.113.21 │ │ - Terminaison SNI (port 443) │ │ - Routage par domaine │ │ - Health checks │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ (auth.rber.bj → bk_k8s_ingress_https) ┌─────────────────────────────────────────────────────────────────────┐ │ HAProxy Ingress Controller │ │ 10.29.113.201:443 │ │ (Kubernetes Ingress) │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Keycloak │ │ Namespace: keycloak │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ keycloak-0 │ │ keycloak-1 │ (2 réplicas) │ │ │ Port 8080 │ │ Port 8080 │ │ │ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │ │ └──────────┬───────────┘ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ PostgreSQL HA │ │ │ │ (CloudNativePG) │ │ │ │ keycloak-pg-1/2/3 │ │ │ └─────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ (LDAP Federation) ┌─────────────────────────────────────────────────────────────────────┐ │ Annuaires LDAP Universités │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ UAC │ │ UNA │ │ UNSTIM │ │ UP │ │ │ │10.24.112.33 │ │10.20.112.33 │ │10.21.112.33 │ │10.25.112.33 │ │ │ │ Port 389 │ │ Port 389 │ │ Port 389 │ │ Port 389 │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ``` ## 1.2 Composants | Composant | Version | Rôle | |-----------|---------|------| | Keycloak | 26.0 | Serveur IAM | | PostgreSQL | CloudNativePG | Base de données HA | | HAProxy | 2.8.5 | Load Balancer externe | | HAProxy Ingress | - | Ingress Controller K8s | | RKE2 | - | Distribution Kubernetes | ## 1.3 Realms configurés | Realm | Université | LDAP | Base DN | |-------|-----------|------|---------| | master | Administration | - | - | | uac | Université d'Abomey-Calavi | 10.24.112.33 | DC=uac,DC=bj | | una | Université Nationale d'Agriculture | 10.20.112.33 | DC=una,DC=bj | | unstim | Université des Sciences, Technologies, Ingénierie et Mathématiques | 10.21.112.33 | DC=unstim,DC=bj | | up | Université de Parakou | 10.25.112.33 | DC=univ-parakou,DC=bj | ## 1.4 URLs d'accès | Service | URL | |---------|-----| | Console Admin | https://auth.rber.bj/admin | | Realm UAC | https://auth.rber.bj/realms/uac | | Realm UNA | https://auth.rber.bj/realms/una | | Realm UNSTIM | https://auth.rber.bj/realms/unstim | | Realm UP | https://auth.rber.bj/realms/up | --- # 2. Document d'Installation ## 2.1 Prérequis ### Infrastructure - Cluster RKE2 opérationnel - Namespace `keycloak` créé - HAProxy Ingress Controller déployé (10.29.113.201) - Certificat SSL pour auth.rber.bj ### Réseau - Connectivité vers les LDAP des universités (ports 389) - DNS configuré : auth.rber.bj → 102.222.216.6 ## 2.2 Installation PostgreSQL (CloudNativePG) ### 2.2.1 Créer le cluster PostgreSQL ```yaml # keycloak-pg-cluster.yaml apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: keycloak-pg namespace: keycloak spec: instances: 3 primaryUpdateStrategy: unsupervised storage: size: 10Gi bootstrap: initdb: database: keycloak owner: keycloak backup: barmanObjectStore: # Configurer selon votre stockage retentionPolicy: "30d" ``` ```bash kubectl apply -f keycloak-pg-cluster.yaml ``` ### 2.2.2 Vérifier le déploiement ```bash kubectl get pods -n keycloak # Attendu: keycloak-pg-1, keycloak-pg-2, keycloak-pg-3 en Running ``` ## 2.3 Installation Keycloak ### 2.3.1 Créer le secret admin ```bash kubectl create secret generic keycloak-admin-secret -n keycloak \ --from-literal=admin-password='VotreMotDePasseSecurise' ``` ### 2.3.2 Déployer Keycloak ```yaml # keycloak-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: keycloak namespace: keycloak labels: app: keycloak spec: replicas: 2 selector: matchLabels: app: keycloak template: metadata: labels: app: keycloak spec: containers: - name: keycloak image: quay.io/keycloak/keycloak:26.0 args: ["start"] env: - name: KC_HOSTNAME value: "auth.rber.bj" - name: KC_PROXY_HEADERS value: "xforwarded" - name: KC_HTTP_ENABLED value: "true" - name: KC_HEALTH_ENABLED value: "true" - name: KC_HTTP_MANAGEMENT_PORT value: "9000" - name: KC_DB value: "postgres" - name: KC_DB_URL_HOST value: "keycloak-pg-rw" - name: KC_DB_URL_PORT value: "5432" - name: KC_DB_URL_DATABASE value: "keycloak" - name: KC_DB_USERNAME valueFrom: secretKeyRef: name: keycloak-pg-app key: username - name: KC_DB_PASSWORD valueFrom: secretKeyRef: name: keycloak-pg-app key: password - name: KEYCLOAK_ADMIN value: "admin" - name: KEYCLOAK_ADMIN_PASSWORD valueFrom: secretKeyRef: name: keycloak-admin-secret key: admin-password ports: - name: http containerPort: 8080 - name: management containerPort: 9000 readinessProbe: httpGet: path: /health/ready port: 9000 initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 5 livenessProbe: httpGet: path: /health/live port: 9000 initialDelaySeconds: 120 periodSeconds: 10 timeoutSeconds: 5 startupProbe: httpGet: path: /health/started port: 9000 initialDelaySeconds: 30 periodSeconds: 5 failureThreshold: 30 resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "1" --- apiVersion: v1 kind: Service metadata: name: keycloak namespace: keycloak labels: app: keycloak spec: ports: - name: http port: 8080 targetPort: 8080 selector: app: keycloak --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: keycloak namespace: keycloak annotations: haproxy.org/cookie-persistence: "SERVERID" haproxy.org/timeout-server: "120s" spec: ingressClassName: haproxy rules: - host: auth.rber.bj http: paths: - path: / pathType: Prefix backend: service: name: keycloak port: number: 8080 ``` ```bash kubectl apply -f keycloak-deployment.yaml ``` ### 2.3.3 Vérifier le déploiement ```bash # Pods kubectl get pods -n keycloak # Logs kubectl logs -n keycloak -l app=keycloak --tail=50 # Test health kubectl exec -it -n keycloak deploy/keycloak -- curl -s localhost:9000/health/ready ``` ## 2.4 Configuration HAProxy ### 2.4.1 ACL SNI pour Keycloak Ajouter dans `/etc/haproxy/haproxy.cfg` section `frontend https_frontend` : ``` # ACL Keycloak acl sni_keycloak req_ssl_sni -i auth.rber.bj # Routage (ajouter sni_keycloak à la règle existante) use_backend bk_k8s_ingress_https if sni_unstim or sni_uac or sni_una or sni_up or sni_test_unstim or sni_keycloak ``` ### 2.4.2 ACL HTTP pour Keycloak Ajouter dans `frontend http_frontend` : ``` acl is_keycloak hdr(host) -i auth.rber.bj # Ajouter à la règle use_backend bk_k8s_ingress use_backend bk_k8s_ingress if ... or is_keycloak ``` ### 2.4.3 Recharger HAProxy ```bash haproxy -c -f /etc/haproxy/haproxy.cfg systemctl reload haproxy ``` ## 2.5 Création des Realms ### 2.5.1 Script de création ```bash #!/bin/bash # create-realms.sh export KEYCLOAK_URL="https://auth.rber.bj" export ADMIN_PASSWORD=$(kubectl get secret keycloak-admin-secret -n keycloak -o jsonpath='{.data.admin-password}' | base64 -d) # Obtenir token export TOKEN=$(curl -sk -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=admin" \ -d "password=$ADMIN_PASSWORD" \ -d "grant_type=password" \ -d "client_id=admin-cli" | jq -r '.access_token') # Créer les realms declare -A REALMS=( ["uac"]="Université d Abomey-Calavi" ["una"]="Université Nationale d Agriculture" ["unstim"]="Université des Sciences Technologies Ingénierie et Mathématiques" ["up"]="Université de Parakou" ) for REALM in "${!REALMS[@]}"; do curl -sk -X POST "$KEYCLOAK_URL/admin/realms" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"realm\":\"$REALM\",\"enabled\":true,\"displayName\":\"${REALMS[$REALM]}\"}" echo "Realm $REALM créé" done ``` ## 2.6 Configuration des Fédérations LDAP ### 2.6.1 Script de configuration LDAP ```bash #!/bin/bash # configure-ldap.sh export KEYCLOAK_URL="https://auth.rber.bj" export ADMIN_PASSWORD=$(kubectl get secret keycloak-admin-secret -n keycloak -o jsonpath='{.data.admin-password}' | base64 -d) # Obtenir token export TOKEN=$(curl -sk -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=admin" \ -d "password=$ADMIN_PASSWORD" \ -d "grant_type=password" \ -d "client_id=admin-cli" | jq -r '.access_token') # Configuration LDAP par université configure_ldap() { local REALM=$1 local LDAP_URL=$2 local BIND_DN=$3 local BIND_PASSWORD=$4 local USERS_DN=$5 curl -sk -X POST "$KEYCLOAK_URL/admin/realms/$REALM/components" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"ldap-$REALM\", \"providerId\": \"ldap\", \"providerType\": \"org.keycloak.storage.UserStorageProvider\", \"config\": { \"vendor\": [\"ad\"], \"connectionUrl\": [\"$LDAP_URL\"], \"bindDn\": [\"$BIND_DN\"], \"bindCredential\": [\"$BIND_PASSWORD\"], \"usersDn\": [\"$USERS_DN\"], \"usernameLDAPAttribute\": [\"sAMAccountName\"], \"rdnLDAPAttribute\": [\"cn\"], \"uuidLDAPAttribute\": [\"objectGUID\"], \"userObjectClasses\": [\"person, organizationalPerson, user\"], \"editMode\": [\"READ_ONLY\"], \"importEnabled\": [\"true\"], \"enabled\": [\"true\"] } }" echo "LDAP configuré pour $REALM" } # Appliquer les configurations configure_ldap "uac" "ldap://10.24.112.33:389" "Administrator@UAC.BJ" "MOT_DE_PASSE" "CN=Users,DC=uac,DC=bj" configure_ldap "una" "ldap://10.20.112.33:389" "Administrator@UNA.BJ" "MOT_DE_PASSE" "CN=Users,DC=una,DC=bj" configure_ldap "unstim" "ldap://10.21.112.33:389" "Administrator@UNSTIM.BJ" "MOT_DE_PASSE" "CN=Users,DC=unstim,DC=bj" configure_ldap "up" "ldap://10.25.112.33:389" "Administrator@UNIV-PARAKOU.BJ" "MOT_DE_PASSE" "CN=Users,DC=univ-parakou,DC=bj" ``` --- # 3. Document d'Administration Générale ## 3.1 Accès à la console d'administration ### URL ``` https://auth.rber.bj/admin ``` ### Récupérer le mot de passe admin ```bash kubectl get secret keycloak-admin-secret -n keycloak -o jsonpath='{.data.admin-password}' | base64 -d && echo ``` ## 3.2 Gestion des tokens API ### Obtenir un token d'administration ```bash export KEYCLOAK_URL="https://auth.rber.bj" export ADMIN_PASSWORD="votre_mot_de_passe" export TOKEN=$(curl -sk -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=admin" \ -d "password=$ADMIN_PASSWORD" \ -d "grant_type=password" \ -d "client_id=admin-cli" | jq -r '.access_token') ``` **Note :** Le token expire après 1 minute par défaut. ## 3.3 Commandes de base Kubernetes ### Voir les pods Keycloak ```bash kubectl get pods -n keycloak ``` ### Logs Keycloak ```bash kubectl logs -n keycloak -l app=keycloak -f ``` ### Redémarrer Keycloak ```bash kubectl rollout restart deployment/keycloak -n keycloak ``` ### Scaler les réplicas ```bash kubectl scale deployment/keycloak -n keycloak --replicas=3 ``` ## 3.4 Gestion de la base de données ### Se connecter à PostgreSQL ```bash kubectl exec -it keycloak-pg-1 -n keycloak -- psql -U keycloak -d keycloak ``` ### Vérifier l'état du cluster PostgreSQL ```bash kubectl get cluster -n keycloak kubectl describe cluster keycloak-pg -n keycloak ``` ## 3.5 Synchronisation LDAP ### Via API ```bash # Obtenir l'ID du composant LDAP LDAP_ID=$(curl -sk "$KEYCLOAK_URL/admin/realms/uac/components?type=org.keycloak.storage.UserStorageProvider" \ -H "Authorization: Bearer $TOKEN" | jq -r '.[0].id') # Synchronisation complète curl -sk -X POST "$KEYCLOAK_URL/admin/realms/uac/user-storage/$LDAP_ID/sync?action=triggerFullSync" \ -H "Authorization: Bearer $TOKEN" # Synchronisation des modifications curl -sk -X POST "$KEYCLOAK_URL/admin/realms/uac/user-storage/$LDAP_ID/sync?action=triggerChangedUsersSync" \ -H "Authorization: Bearer $TOKEN" ``` ### Via Console 1. Aller dans le realm concerné 2. User Federation → ldap-xxx 3. Cliquer sur "Synchronize all users" ou "Synchronize changed users" ## 3.6 Sauvegarde et restauration ### Exporter un realm ```bash # Via API curl -sk "$KEYCLOAK_URL/admin/realms/uac" \ -H "Authorization: Bearer $TOKEN" > realm-uac-export.json # Via pod (export complet avec utilisateurs) kubectl exec -it deploy/keycloak -n keycloak -- /opt/keycloak/bin/kc.sh export \ --dir /tmp/export --realm uac kubectl cp keycloak/keycloak-xxx:/tmp/export ./keycloak-export ``` ### Importer un realm ```bash curl -sk -X POST "$KEYCLOAK_URL/admin/realms" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @realm-uac-export.json ``` --- # 4. Administration par Realm ## 4.1 Realm UAC (Université d'Abomey-Calavi) ### Informations | Paramètre | Valeur | |-----------|--------| | Nom | uac | | Display Name | Université d'Abomey-Calavi | | URL | https://auth.rber.bj/realms/uac | | LDAP | 10.24.112.33:389 | | Base DN | DC=uac,DC=bj | | Users DN | CN=Users,DC=uac,DC=bj | | Bind DN | Administrator@UAC.BJ | ### Endpoints OIDC ``` Well-Known: https://auth.rber.bj/realms/uac/.well-known/openid-configuration Authorization: https://auth.rber.bj/realms/uac/protocol/openid-connect/auth Token: https://auth.rber.bj/realms/uac/protocol/openid-connect/token UserInfo: https://auth.rber.bj/realms/uac/protocol/openid-connect/userinfo ``` --- ## 4.2 Realm UNA (Université Nationale d'Agriculture) ### Informations | Paramètre | Valeur | |-----------|--------| | Nom | una | | Display Name | Université Nationale d'Agriculture | | URL | https://auth.rber.bj/realms/una | | LDAP | 10.20.112.33:389 | | Base DN | DC=una,DC=bj | | Users DN | CN=Users,DC=una,DC=bj | | Bind DN | Administrator@UNA.BJ | ### Endpoints OIDC ``` Well-Known: https://auth.rber.bj/realms/una/.well-known/openid-configuration Authorization: https://auth.rber.bj/realms/una/protocol/openid-connect/auth Token: https://auth.rber.bj/realms/una/protocol/openid-connect/token UserInfo: https://auth.rber.bj/realms/una/protocol/openid-connect/userinfo ``` --- ## 4.3 Realm UNSTIM ### Informations | Paramètre | Valeur | |-----------|--------| | Nom | unstim | | Display Name | Université des Sciences, Technologies, Ingénierie et Mathématiques | | URL | https://auth.rber.bj/realms/unstim | | LDAP | 10.21.112.33:389 | | Base DN | DC=unstim,DC=bj | | Users DN | CN=Users,DC=unstim,DC=bj | | Bind DN | Administrator@UNSTIM.BJ | ### Endpoints OIDC ``` Well-Known: https://auth.rber.bj/realms/unstim/.well-known/openid-configuration Authorization: https://auth.rber.bj/realms/unstim/protocol/openid-connect/auth Token: https://auth.rber.bj/realms/unstim/protocol/openid-connect/token UserInfo: https://auth.rber.bj/realms/unstim/protocol/openid-connect/userinfo ``` --- ## 4.4 Realm UP (Université de Parakou) ### Informations | Paramètre | Valeur | |-----------|--------| | Nom | up | | Display Name | Université de Parakou | | URL | https://auth.rber.bj/realms/up | | LDAP | 10.25.112.33:389 | | Base DN | DC=univ-parakou,DC=bj | | Users DN | CN=Users,DC=univ-parakou,DC=bj | | Bind DN | Administrator@UNIV-PARAKOU.BJ | ### Endpoints OIDC ``` Well-Known: https://auth.rber.bj/realms/up/.well-known/openid-configuration Authorization: https://auth.rber.bj/realms/up/protocol/openid-connect/auth Token: https://auth.rber.bj/realms/up/protocol/openid-connect/token UserInfo: https://auth.rber.bj/realms/up/protocol/openid-connect/userinfo ``` --- # 5. Procédures Opérationnelles ## 5.1 Créer un client OIDC (exemple Moodle) ### Via Console 1. Realm concerné → Clients → Create client 2. Client ID: `moodle-unstim` 3. Client Protocol: `openid-connect` 4. Root URL: `https://elearning.unstim.bj` 5. Valid Redirect URIs: `https://elearning.unstim.bj/*` 6. Client authentication: ON 7. Sauvegarder → Onglet Credentials → Copier le secret ### Via API ```bash curl -sk -X POST "$KEYCLOAK_URL/admin/realms/unstim/clients" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "clientId": "moodle-unstim", "enabled": true, "protocol": "openid-connect", "rootUrl": "https://elearning.unstim.bj", "redirectUris": ["https://elearning.unstim.bj/*"], "publicClient": false, "clientAuthenticatorType": "client-secret" }' ``` ## 5.2 Créer un groupe ```bash curl -sk -X POST "$KEYCLOAK_URL/admin/realms/uac/groups" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "enseignants"}' ``` ## 5.3 Créer un rôle ```bash curl -sk -X POST "$KEYCLOAK_URL/admin/realms/uac/roles" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "professeur", "description": "Rôle enseignant"}' ``` ## 5.4 Mapper les groupes LDAP Dans User Federation → ldap-xxx → Mappers → Add mapper : - Name: `group-mapper` - Mapper type: `group-ldap-mapper` - LDAP Groups DN: `CN=Users,DC=uac,DC=bj` - Group Object Classes: `group` --- # 6. Dépannage ## 6.1 Keycloak ne démarre pas ### Vérifier les logs ```bash kubectl logs -n keycloak -l app=keycloak --tail=100 ``` ### Problèmes courants | Erreur | Cause | Solution | |--------|-------|----------| | Connection refused DB | PostgreSQL non accessible | Vérifier keycloak-pg-rw service | | health/ready timeout | Démarrage lent | Augmenter initialDelaySeconds | | OutOfMemory | Mémoire insuffisante | Augmenter limits.memory | ## 6.2 LDAP ne se connecte pas ### Tester la connectivité ```bash kubectl run ldap-test --rm -it --image=alpine --restart=Never -- sh -c " apk add --no-cache openldap-clients && ldapsearch -x -H ldap://10.24.112.33:389 -D 'Administrator@UAC.BJ' -W -b 'DC=uac,DC=bj' '(objectClass=person)' dn | head -10 " ``` ### Problèmes courants | Erreur | Cause | Solution | |--------|-------|----------| | Can't contact LDAP server | Réseau/Firewall | Vérifier connectivité port 389 | | Invalid credentials | Mauvais mot de passe | Vérifier bindCredential | | No such object | Base DN incorrect | Vérifier usersDn | ## 6.3 Erreur 503 sur HAProxy ### Vérifier le backend ```bash echo "show stat" | socat stdio /run/haproxy/admin.sock | grep k8s_ingress ``` ### Vérifier l'Ingress ```bash kubectl get ingress -n keycloak curl -sk -H "Host: auth.rber.bj" https://10.29.113.201/ ``` ## 6.4 Problème de session / cookies Si les utilisateurs sont déconnectés aléatoirement : 1. Vérifier que la persistence de session est activée dans HAProxy 2. Vérifier que les cookies SERVERID sont transmis ```bash curl -sk -c cookies.txt -b cookies.txt https://auth.rber.bj/realms/uac/account cat cookies.txt ``` --- # Annexes ## A. Fichiers de configuration ### keycloak-deployment.yaml Voir section 2.3.2 ### haproxy.cfg (extrait Keycloak) Voir section 2.4 ## B. Mots de passe (À SÉCURISER) **ATTENTION : Ces mots de passe doivent être stockés dans un coffre-fort sécurisé (Vault, etc.)** | Service | Compte | Secret | |---------|--------|--------| | Keycloak Admin | admin | (dans secret k8s keycloak-admin-secret) | | PostgreSQL | keycloak | (dans secret k8s keycloak-pg-app) | | LDAP UAC | Administrator@UAC.BJ | *** | | LDAP UNA | Administrator@UNA.BJ | *** | | LDAP UNSTIM | Administrator@UNSTIM.BJ | *** | | LDAP UP | Administrator@UNIV-PARAKOU.BJ | *** | ## C. Contacts | Rôle | Contact | |------|---------| | Admin Infrastructure | À définir | | Support Keycloak | À définir | --- **Document généré le 5 janvier 2026** **Prochaine mise à jour prévue : Après configuration Identity Linking**