From 9e6f33d1cfaef7e84d790648295fda50e896aabb Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Mon, 28 Sep 2026 20:17:46 -0400 Subject: [PATCH] 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 --- CHANGELOG.md | 27 ++++++ roles/serveur_keycloak/defaults/main.yml | 5 + roles/serveur_keycloak/meta/supervision.yml | 7 +- roles/serveur_keycloak/tasks/main.yml | 7 +- .../templates/sonde-identite.sh.j2 | 96 ++++++++++++++++++- wiki/Rôle-serveur_keycloak.md | 2 +- 6 files changed, 136 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a261f38..c36283b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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-.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-` : 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 diff --git a/roles/serveur_keycloak/defaults/main.yml b/roles/serveur_keycloak/defaults/main.yml index ea1ca6b..27cb9fa 100644 --- a/roles/serveur_keycloak/defaults/main.yml +++ b/roles/serveur_keycloak/defaults/main.yml @@ -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 '' }} diff --git a/roles/serveur_keycloak/meta/supervision.yml b/roles/serveur_keycloak/meta/supervision.yml index 318d61c..5ded295 100644 --- a/roles/serveur_keycloak/meta/supervision.yml +++ b/roles/serveur_keycloak/meta/supervision.yml @@ -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. diff --git a/roles/serveur_keycloak/tasks/main.yml b/roles/serveur_keycloak/tasks/main.yml index 42d3b39..10c02c2 100644 --- a/roles/serveur_keycloak/tasks/main.yml +++ b/roles/serveur_keycloak/tasks/main.yml @@ -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-.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-` 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. diff --git a/roles/serveur_keycloak/templates/sonde-identite.sh.j2 b/roles/serveur_keycloak/templates/sonde-identite.sh.j2 index e1f322b..09d4b96 100644 --- a/roles/serveur_keycloak/templates/sonde-identite.sh.j2 +++ b/roles/serveur_keycloak/templates/sonde-identite.sh.j2 @@ -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 diff --git a/wiki/Rôle-serveur_keycloak.md b/wiki/Rôle-serveur_keycloak.md index d8d2027..9933804 100644 --- a/wiki/Rôle-serveur_keycloak.md +++ b/wiki/Rôle-serveur_keycloak.md @@ -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