#!/bin/bash # GENERE par Set-OPS (role serveur_keycloak). Ne pas editer a la main. # # SONDE « identite » — le realm sert sa decouverte OIDC, et Keycloak ATTEINT l'annuaire # # Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. # Mise en defaut PAR PARAMETRE : `serveur_keycloak_sonde_realm` (realm inexistant) pour la # premiere etape, `serveur_keycloak_sonde_federation` (federation inexistante) pour la seconde. # # LA FEDERATION, DEPUIS LE 2026-09-28. Cette sonde s'arretait a la decouverte OIDC. Or les # comptes vivent dans LDAP, pas dans Keycloak : un lien rompu vers l'annuaire (certificat # renouvele mais non recharge, mot de passe de liaison tourne d'un seul cote, slapd tombe) # laisse la decouverte PARFAITE et refuse chaque connexion. On demande donc a Keycloak de # s'authentifier aupres de l'annuaire AVEC SA PROPRE configuration — celle de la federation # enregistree (`componentId`, secret masque) : c'est le meme chemin qu'une vraie connexion. # # LE MOT DE PASSE ADMIN EST LU LA OU IL EST, pas recopie : `/etc/keycloak/keycloak.env`, # 0640 root:keycloak. La sonde tourne en root ; il n'est ni affiche, ni passe en argument. set -uo pipefail URL={{ serveur_keycloak_sonde_url }} REALM={{ serveur_keycloak_sonde_realm }} FEDERATION="{{ serveur_keycloak_sonde_federation | trim }}" ENV=/etc/keycloak/keycloak.env s=$(curl -s --max-time 10 "${URL}/realms/${REALM}/.well-known/openid-configuration" 2>/dev/null) [[ -n "${s}" ]] || { echo "Keycloak ne repond pas sur ${URL}."; exit 2; } printf '%s' "${s}" | grep -q '"issuer"' \ || { echo "Le realm « ${REALM} » ne sert pas sa decouverte OIDC : ${s:0:70}"; exit 2; } # LES POINTS D'ENTREE QUE LES CLIENTS UTILISENT VRAIMENT. Un document de decouverte # tronque se lit comme du JSON valide et casse chaque client a la premiere redirection. for cle in authorization_endpoint token_endpoint jwks_uri; do printf '%s' "${s}" | grep -q "\"${cle}\"" \ || { echo "Decouverte OIDC incomplete pour « ${REALM} » : ${cle} absent."; exit 2; } done if [[ -z "${FEDERATION}" ]]; then echo "Realm « ${REALM} » : decouverte OIDC complete (pas de federation a verifier)." exit 0 fi KC_UTIL=$(grep -hoP '^KC_BOOTSTRAP_ADMIN_USERNAME=\K\S+' "${ENV}" 2>/dev/null | head -1) KC_MDP=$(grep -hoP '^KC_BOOTSTRAP_ADMIN_PASSWORD=\K\S+' "${ENV}" 2>/dev/null | head -1) if [[ -z "${KC_UTIL}" || -z "${KC_MDP}" ]]; then echo "Realm « ${REALM} » : decouverte OIDC complete, mais federation NON verifiee (identifiants admin illisibles dans ${ENV})." exit 1 fi KC_UTIL="${KC_UTIL}" KC_MDP="${KC_MDP}" python3 - "${URL}" "${REALM}" "${FEDERATION}" <<'PY' import json, os, sys, urllib.parse, urllib.request url, realm, nom = sys.argv[1:4] def appel(chemin, data=None, jeton=None, form=False): h = {"Authorization": "Bearer " + jeton} if jeton else {} if data is not None: if form: data = urllib.parse.urlencode(data).encode() h["Content-Type"] = "application/x-www-form-urlencoded" else: data = json.dumps(data).encode() h["Content-Type"] = "application/json" try: with urllib.request.urlopen(urllib.request.Request(url + chemin, data=data, headers=h), timeout=8) as r: b = r.read() return r.status, (json.loads(b) if b else None) except urllib.error.HTTPError as e: b = e.read() try: return e.code, json.loads(b) except ValueError: return e.code, {"errorMessage": f"HTTP {e.code}"} except OSError as e: return 0, {"errorMessage": str(e)[:80]} c, t = appel("/realms/master/protocol/openid-connect/token", {"grant_type": "password", "client_id": "admin-cli", "username": os.environ["KC_UTIL"], "password": os.environ["KC_MDP"]}, form=True) if c != 200 or not isinstance(t, dict) or "access_token" not in t: print(f"Realm « {realm} » : decouverte OIDC complete, mais federation NON verifiee " f"(jeton admin refuse, HTTP {c}).") sys.exit(1) jeton = t["access_token"] c, comps = appel(f"/admin/realms/{realm}/components?type=org.keycloak.storage.UserStorageProvider", jeton=jeton) if c != 200 or not isinstance(comps, list): print(f"Realm « {realm} » : la liste des federations est illisible (HTTP {c}).") sys.exit(1) fed = next((x for x in comps if x.get("name") == nom), None) if fed is None: print(f"Realm « {realm} » : la federation « {nom} » N'EXISTE PAS " f"({', '.join(x.get('name', '?') for x in comps) or 'aucune'}) — les comptes de l'annuaire ne peuvent pas se connecter.") sys.exit(2) cfg = fed.get("config", {}) val = lambda k, d="": (cfg.get(k) or [d])[0] if val("enabled", "true") != "true": print(f"Realm « {realm} » : la federation « {nom} » est DESACTIVEE — les comptes de l'annuaire ne peuvent pas se connecter.") sys.exit(2) # `**********` : Keycloak substitue le secret ENREGISTRE pour ce `componentId`. On teste donc # la liaison telle qu'une vraie connexion la ferait, sans jamais manipuler le secret LDAP. corps = {"action": "testAuthentication", "componentId": fed["id"], "bindCredential": "**********", "connectionUrl": val("connectionUrl"), "bindDn": val("bindDn"), "authType": val("authType", "simple"), "startTls": val("startTls", "false"), "useTruststoreSpi": val("useTruststoreSpi", "always"), "connectionTimeout": val("connectionTimeout")} c, r = appel(f"/admin/realms/{realm}/testLDAPConnection", corps, jeton=jeton) if c != 204: motif = (r or {}).get("errorMessage", f"HTTP {c}") if isinstance(r, dict) else f"HTTP {c}" print(f"Realm « {realm} » : decouverte OIDC complete, mais Keycloak N'ATTEINT PAS l'annuaire " f"({val('connectionUrl')} : {motif}) — toute connexion echoue.") sys.exit(2) print(f"Realm « {realm} » : decouverte OIDC complete, federation « {nom} » authentifiee aupres de {val('connectionUrl')}.") PY