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>
This commit is contained in:
Daniel Allaire 2026-09-28 20:17:46 -04:00
parent 6b6a56ab3f
commit 9e6f33d1cf
6 changed files with 136 additions and 8 deletions

View file

@ -1,5 +1,32 @@
# CHANGELOG — Set-OPS
## 2026-09-28 (26) — `identite` vérifie que Keycloak atteint l'annuaire ; l'archive Keycloak cesse de voyager pour rien
Troisième des cinq sondes « répond » de la revue (25).
**`identite` (Keycloak).** S'arrêtait à la découverte OIDC du realm. Or les comptes vivent dans
LDAP : un lien rompu vers l'annuaire (certificat, mot de passe de liaison tourné d'un seul côté,
slapd tombé) laisse la découverte parfaite et refuse chaque connexion. La sonde demande
désormais à Keycloak de s'authentifier auprès de l'annuaire avec SA PROPRE configuration
(`testLDAPConnection`, `testAuthentication`, `componentId` de la fédération enregistrée, secret
masqué : Keycloak substitue le secret stocké — la sonde ne manipule jamais le secret LDAP). Le
jeton admin est pris avec les identifiants de `/etc/keycloak/keycloak.env` (0640, lus en root,
jamais affichés ni passés en argument). Éprouvé avant d'écrire : 204 sur la vraie fédération,
400 `AuthenticationFailure` avec un faux bindDn. Chez les deux locataires : fédération
`openldap` authentifiée, reçue par Icinga (0,12 s). Mise en défaut par paramètre
(`serveur_keycloak_sonde_federation`) : critique, « la fédération n'existe pas » ; fichier
d'identifiants illisible : avertissement, « fédération non vérifiée ». Sans fédération
(`serveur_keycloak_ldap_federation: false`), la sonde s'en tient à la découverte.
**Au passage : l'archive Keycloak repartait à chaque déploiement.** « Déjà posée ? » regardait
`/tmp/keycloak-<version>.tar.gz`, que le système vide : l'archive repartait du contrôleur à
chaque passage (`changed` sur les deux locataires). Le repère est désormais le marqueur
`.kc-built-<version>` : cette version est en place, rien à apporter. Redéployé : `changed=0`
sur les deux.
Reste de la revue : `console` (Icinga Web lit-il sa base ?), `passerelle` (oauth2-proxy
atteint-il Keycloak ?).
## 2026-09-28 (25) — Deux sondes qui disaient « répond » apprennent à dire « sert » ; l'edge refuse les noms inconnus
Revue de toutes les sondes, à la recherche de celles qui resteraient vertes pendant que le

View file

@ -228,3 +228,8 @@ serveur_keycloak_signature_url: "{{ serveur_keycloak_url }}.asc"
# installation derriere un proxy (verifie le 2026-09-10 : 8080, pas 8443).
serveur_keycloak_sonde_url: "http://127.0.0.1:8080"
serveur_keycloak_sonde_realm: "{{ serveur_keycloak_realm }}"
# La federation que la sonde fait s'AUTHENTIFIER aupres de l'annuaire (2026-09-28). Vide =
# pas de federation a verifier (`serveur_keycloak_ldap_federation: false`). Mise en defaut
# PAR PARAMETRE : un nom de federation qui n'existe pas.
serveur_keycloak_sonde_federation: >-
{{ serveur_keycloak_ldap_nom if serveur_keycloak_ldap_federation | bool else '' }}

View file

@ -3,6 +3,7 @@
sondes:
- nom: identite
ttl: 5400
raison: Le realm sert-il encore sa decouverte OIDC ? C'est la porte par laquelle tout le SSO de l'ecosysteme
entre. Un Keycloak debout dont le realm n'a pas charge repond sur son port et refuse toute connexion
— sans rien dire.
raison: Le realm sert-il sa decouverte OIDC, et Keycloak ATTEINT-IL l'annuaire ? La decouverte est la
porte par laquelle tout le SSO entre ; la federation LDAP est ce qui la rend utile. Un Keycloak dont
le lien a l'annuaire est rompu (certificat, mot de passe de liaison, slapd tombe) publie une decouverte
parfaite et refuse chaque connexion — sans rien dire.

View file

@ -84,9 +84,14 @@
# Sans garde, `get_url` recontacte le serveur distant a CHAQUE deploiement — et un
# serveur tiers lent suffit alors a faire tomber un deploiement de flotte. Constate le
# 2026-08-09 sur le binaire Forgejo, meme motif ici.
# LE REPERE EST LA CONSTRUCTION DE CETTE VERSION, PAS L'ARCHIVE (2026-09-28). On regardait
# `/tmp/keycloak-<version>.tar.gz` : /tmp se vide, et l'archive repartait du controleur a
# CHAQUE deploiement — `changed` a tous les coups sur les deux locataires, pour un Keycloak
# deja extrait et construit. Le marqueur `.kc-built-<version>` dit exactement ce qu'on veut
# savoir : cette version est en place, rien a apporter.
- name: L artefact de cette version est-il deja pose ?
ansible.builtin.stat:
path: "/tmp/keycloak-{{ serveur_keycloak_version }}.tar.gz"
path: "{{ serveur_keycloak_home }}/.kc-built-{{ serveur_keycloak_version }}"
register: serveur_keycloak_archive_present
# Le controleur telecharge, puis pousse par SSH — la cible ne tire jamais d'Internet.

View file

@ -1,14 +1,27 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_keycloak). Ne pas editer a la main.
#
# SONDE « identite » — le realm sert sa decouverte OIDC
# 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).
# 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; }
@ -21,4 +34,81 @@ 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
echo "Realm « ${REALM} » : decouverte OIDC complete."
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

View file

@ -22,7 +22,7 @@ graph LR
| sonde | TTL | ce qu'elle voit |
|---|---|---|
| `identite` | 5400 s | Le realm sert-il encore sa decouverte OIDC ? C'est la porte par laquelle tout le SSO de l'ecosysteme entre. Un Keycloak debout dont le realm n'a pas charge repond sur son port et refuse toute connexion — sans rien dire. |
| `identite` | 5400 s | Le realm sert-il sa decouverte OIDC, et Keycloak ATTEINT-IL l'annuaire ? La decouverte est la porte par laquelle tout le SSO entre ; la federation LDAP est ce qui la rend utile. Un Keycloak dont le lien a l'annuaire est rompu (certificat, mot de passe de liaison, slapd tombe) publie une decouverte parfaite et refuse chaque connexion — sans rien dire. |
## Ce qu'il expose en séries