rotation : vault_openldap_admin, quatre consommateurs et deux pieges

`ldappasswd` ne peut pas le changer : `cn=admin` n'est pas une entree de la base
mais le rootDN declare dans cn=config. Le mot de passe vit dans `olcRootPW` et se
modifie par un bind EXTERNAL. L'echec etait sans degat — l'ancien fonctionnait
toujours, verifie avant de continuer.

Keycloak stocke le mot de passe de LIAISON dans sa base et le masque : la
reconciliation d'hier couvrait l'URL, les DN et le mode, pas `bindCredential`.
Tourner le secret aurait coupe Keycloak de l'annuaire. Comme la valeur est
masquee, la reconciliation passe par une empreinte.

Verifie consommateur par consommateur : synchro LDAP de Keycloak (qui prouve la
liaison), carte LDAP de Postfix, Dovecot actif. Les trois rejouent a changed=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-08 05:46:38 -04:00
parent 35602546eb
commit 4642ffec0a
4 changed files with 55 additions and 1 deletions

View file

@ -2,6 +2,29 @@
## 2026-08-06 — le chemin nord-sud devient dérivable ## 2026-08-06 — le chemin nord-sud devient dérivable
### `vault_openldap_admin` — quatre consommateurs et deux pièges
Le secret le plus délicat de la liste : Keycloak, Dovecot, Postfix et Icinga Web 2 s'y lient
tous.
**Premier piège : `ldappasswd` ne peut pas le changer.** `cn=admin` n'est pas une entrée de
la base mais le **rootDN** déclaré dans `cn=config` — la commande répond « No such object ».
Le mot de passe vit dans `olcRootPW` et se modifie par un bind `EXTERNAL`. L'échec était sans
dégât : l'ancien fonctionnait toujours, vérifié avant de continuer.
**Second piège : Keycloak stocke le mot de passe de liaison dans sa base, et le masque.** La
réconciliation ajoutée hier couvrait l'URL, les DN et le mode — pas `bindCredential`. Tourner
le secret aurait coupé Keycloak de l'annuaire, et plus personne n'aurait pu se connecter.
Comme on ne peut pas comparer une valeur masquée, la réconciliation passe par une **empreinte**
— même mécanisme que les comptes de secours.
**Vérifié, consommateur par consommateur** : synchronisation LDAP de Keycloak (qui prouve la
liaison), carte LDAP de Postfix, Dovecot actif. Les trois rejouent à `changed=0`.
Six secrets tournés depuis hier ; chacun a d'abord demandé de **construire la capacité de le
faire**. C'est le motif de fond de ces deux jours : le dépôt savait créer, pas changer.
### `vault_forgejo_oidc` — le secret exposé ne vaut plus rien ### `vault_forgejo_oidc` — le secret exposé ne vaut plus rien
Il avait fui dans une sortie de diagnostic. Le faire tourner a d'abord demandé de rendre la Il avait fui dans une sortie de diagnostic. Le faire tourner a d'abord demandé de rendre la

View file

@ -212,7 +212,20 @@ s'authentifier pour poser la nouvelle valeur. L'ordre est donc inversé :
``` ```
C'est une **procédure**, pas un redéploiement. La même contrainte vaut pour tout secret qui C'est une **procédure**, pas un redéploiement. La même contrainte vaut pour tout secret qui
est aussi la clé de son propre changement — `vault_openldap_admin` en est un. est aussi la clé de son propre changement.
**`vault_openldap_admin` en est un, avec deux pièges de plus.** Il ne se change pas par
`ldappasswd` : `cn=admin` n'est pas une entrée de la base mais le **rootDN** déclaré dans
`cn=config`. Le mot de passe vit dans `olcRootPW`, et se modifie par un bind `EXTERNAL` :
```
slappasswd -h '{SSHA}' -s <nouveau> puis ldapmodify -Y EXTERNAL sur olcDatabase={1}mdb
```
Et il a **quatre consommateurs** : Keycloak (dans sa base), Dovecot, Postfix, Icinga Web 2
(fichiers de configuration). Les trois derniers suivent au redéploiement ; Keycloak stocke le
mot de passe de liaison et le masque — sa réconciliation passe donc par une empreinte, comme
les comptes de secours.
**Et une vérification n'est pas un message de succès.** `grafana-cli` annonçait **Et une vérification n'est pas un message de succès.** `grafana-cli` annonçait
« Admin password changed successfully » en écrivant dans une base qui n'était pas celle du « Admin password changed successfully » en écrivant dans une base qui n'était pas celle du

View file

@ -116,3 +116,7 @@ serveur_keycloak_ldap_edit_mode: "WRITABLE"
# Compte d'amorcage dont le changement de mot de passe doit etre EXPLICITE dans # Compte d'amorcage dont le changement de mot de passe doit etre EXPLICITE dans
# Keycloak. Vide = aucun alignement. Voir roles/amorcage_acces (D-67). # Keycloak. Vide = aucun alignement. Voir roles/amorcage_acces (D-67).
serveur_keycloak_amorcage_uid: "{{ amorcage_acces_uid | default('sysadmin') }}" serveur_keycloak_amorcage_uid: "{{ amorcage_acces_uid | default('sysadmin') }}"
# Empreinte du mot de passe de LIAISON LDAP applique — Keycloak le masque, on ne
# peut donc pas le comparer. Permet sa rotation. Voir tasks/federation-ldap.yml.
serveur_keycloak_marqueur_bind: "/etc/keycloak/.setops-bind-sha256"

View file

@ -42,6 +42,20 @@
# `|| true` en fin de chaine : avec `pipefail`, un grep sans correspondance # `|| true` en fin de chaine : avec `pipefail`, un grep sans correspondance
# tuerait le script entier. Et l'extraction ne s'ancre pas sur `$` — la ligne # tuerait le script entier. Et l'extraction ne s'ancre pas sur `$` — la ligne
# se termine par un guillemet, pas par la valeur. # se termine par un guillemet, pas par la valeur.
# Le mot de passe de LIAISON ne se compare pas : Keycloak le masque (`****`).
# On memorise donc son empreinte, comme pour les comptes de secours. Sans
# cela, faire tourner `vault_openldap_admin` couperait Keycloak de
# l'annuaire — la federation garderait l'ancienne valeur, et plus personne
# ne pourrait se connecter.
EMPREINTE_VOULUE="{{ serveur_keycloak_ldap_bind_password | hash('sha256') }}"
EMPREINTE_POSEE=$(cat {{ serveur_keycloak_marqueur_bind }} 2>/dev/null || true)
if [ "$EMPREINTE_POSEE" != "$EMPREINTE_VOULUE" ]; then
"$KC" update components/"$FEDID" -r {{ serveur_keycloak_realm }} \
-s "config.bindCredential=[\"$LDAP_BIND_PW\"]" >/dev/null
printf '%s\n' "$EMPREINTE_VOULUE" > {{ serveur_keycloak_marqueur_bind }}
chmod 600 {{ serveur_keycloak_marqueur_bind }}
CHANGED=1
fi
MODE_ACTUEL=$("$KC" get components/"$FEDID" -r {{ serveur_keycloak_realm }} 2>/dev/null \ MODE_ACTUEL=$("$KC" get components/"$FEDID" -r {{ serveur_keycloak_realm }} 2>/dev/null \
| sed -n 's/.*"editMode" : \[ "\([A-Z_]*\)".*/\1/p' | head -1 || true) | sed -n 's/.*"editMode" : \[ "\([A-Z_]*\)".*/\1/p' | head -1 || true)
if [ "$URL_ACTUELLE" != "{{ serveur_keycloak_ldap_url }}" ] \ if [ "$URL_ACTUELLE" != "{{ serveur_keycloak_ldap_url }}" ] \