Set-OPS-Public/roles/serveur_keycloak/templates/sonde-identite.sh.j2
Daniel Allaire 9e6f33d1cf sonde identite : Keycloak doit atteindre l annuaire ; l archive Keycloak ne voyage plus pour rien
La sonde demande a Keycloak de s authentifier aupres de LDAP avec la federation
enregistree (testLDAPConnection, secret masque). Eprouvee sur les deux locataires ;
mise en defaut par parametre. Le repere d archive deja posee est le marqueur de
construction de la version, pas /tmp : changed=0 au redeploiement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:17:46 -04:00

114 lines
5.8 KiB
Django/Jinja

#!/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