Set-OPS-Public/roles/serveur_openldap/tasks/main.yml

535 lines
21 KiB
YAML
Raw Normal View History

---
- name: Exiger le mot de passe admin LDAP (Vault)
ansible.builtin.assert:
that:
- serveur_openldap_admin_password | length > 0
fail_msg: "serveur_openldap_admin_password est requis (a fournir via Ansible Vault)."
- name: Installer debconf-utils (requis par le module debconf, fournit debconf-get-selections)
ansible.builtin.apt:
name: debconf-utils
state: present
update_cache: true
cache_valid_time: 3600
- name: Preconfigurer slapd (debconf)
ansible.builtin.debconf:
name: slapd
question: "{{ item.question }}"
vtype: "{{ item.vtype }}"
value: "{{ item.value }}"
loop:
- { question: "slapd/no_configuration", vtype: "boolean", value: "false" }
- { question: "slapd/domain", vtype: "string", value: "{{ serveur_openldap_domaine }}" }
- { question: "shared/organization", vtype: "string", value: "{{ serveur_openldap_organisation }}" }
- { question: "slapd/backend", vtype: "select", value: "MDB" }
- { question: "slapd/purge_database", vtype: "boolean", value: "true" }
- { question: "slapd/move_old_database", vtype: "boolean", value: "true" }
- { question: "slapd/allow_ldap_v2", vtype: "boolean", value: "false" }
loop_control:
label: "{{ item.question }}"
- name: Preconfigurer le mot de passe admin slapd (debconf)
ansible.builtin.debconf:
name: slapd
question: "{{ item }}"
vtype: password
value: "{{ serveur_openldap_admin_password }}"
loop:
- slapd/password1
- slapd/password2
no_log: true
- name: Installer OpenLDAP
ansible.builtin.apt:
name: "{{ serveur_openldap_paquets }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Activer et demarrer slapd
when: not ansible_check_mode
ansible.builtin.systemd:
name: "{{ serveur_openldap_service }}"
enabled: true
state: started
# L'ANNUAIRE EST-IL VIERGE ? Mesure AVANT que ce role n'y cree quoi que ce soit (unites,
# comptes de service) : apres, il ne le serait plus jamais. Le paquet ne pose que la
# racine — `cn=admin` n'y figure que sur les installations anciennes.
#
# `awk`, PAS `grep -v | wc -l` : sur un annuaire VRAIMENT vierge, `grep -v` ne garde aucune
# ligne et sort en 1 — `pipefail` faisait alors echouer la mesure precisement dans le cas
# qu'elle doit reconnaitre (reconstruction de Chezlepro, 2026-09-30).
- name: Restauration — l'annuaire est-il vierge ?
ansible.builtin.shell: >-
set -o pipefail;
slapcat -b '{{ serveur_openldap_base_dn }}' 2>/dev/null
| awk -v base='dn: {{ serveur_openldap_base_dn | lower }}'
'tolower($0) ~ /^dn: / && tolower($0) != base && tolower($0) != "dn: cn=admin," substr(base, 5) {n++}
END {print n + 0}'
args:
executable: /bin/bash
register: serveur_openldap_entrees_initiales
changed_when: false
check_mode: false
annuaire : un compte de service par consommateur, et la porte se ferme Keycloak, Dovecot, Postfix et Icinga Web 2 se liaient TOUS avec cn=admin, le compte d administration de la base. C est le rootDN : slapd lui fait contourner toutes les ACL. Un seul secret, quatre services, tous les droits sur l arbre — pour ce qui est, trois fois sur quatre, une simple lecture. Et les droits livres par Debian etaient intacts : `to * by * read`. Sur ldap://, sans s authentifier, une machine du reseau enumerait tous les comptes et toutes les adresses. Des comptes a droits mesures n auraient rien valu tant que cette ligne restait : on aurait ferme la porte en laissant la fenetre. L indice etait deja dans le depot. `validatePasswordPolicy` existe parce que slapd n applique pas ses controles de qualite au rootDN : la consequence etait compensee, la cause intacte. - ou=services, un compte par consommateur, secret propre en voute - sept regles d acces posees EN ENTIER (state: exact) : l ordre est la regle, et inserer c est parier sur ce que le paquet aura mis avant nous - amorcage_acces garde le compte d administration, NOMME comme l exception : il ne consomme pas l annuaire, il le provisionne depuis la socket locale - la sonde passe de -x a -Y EXTERNAL : elle lisait en anonyme et aurait annonce un annuaire VIDE sur un annuaire parfaitement sain - la rotation du compte d administration devient possible (elle n etait posee qu a l installation, par debconf : la voute et slapd divergeaient en silence) Quatre marches payees en chemin : 1. un cinquieme appelant oublie, dont l echec etait masque par no_log — la garde refuse desormais SANS no_log : elle nomme la cle absente, jamais son contenu 2. la federation Keycloak ne reecrivait son bindDn que si l URL ou le mode changeaient — nouveau secret, ancien nom, error code 49 3. la rotation placee APRES les taches qui se lient en administrateur 4. ansible-vault et son tube : sortie non bloquante = echec silencieux, la voute paraissait tournee et etait identique a l octet P72 exige que tout role incluant resoudre_annuaire NOMME son compte, et qu aucun sauf amorcage_acces ne nomme admin. Eprouvee dans les deux sens. Verifie sur l infrastructure : chaque compte lit ce qu il doit, aucun ne voit les autres, la lecture anonyme rend 0 entree, et les quatre services repondent (doveadm user, postmap -q, decouverte OIDC 200, portier SSO 200). vault_openldap_admin et vault_ldap_bind_postfix renouveles : les deux avaient transite en clair par une session d exploitation. Les anciennes valeurs rendent Invalid credentials (49). make prouver : 71 OK, 0 echec, 1 saute. ansible-lint : 0 failure, profil production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-13 13:36:38 -04:00
# LA ROTATION PASSE AVANT TOUT CE QUI SE LIE EN ADMINISTRATEUR (2026-09-13).
#
# Placee plus bas, elle arrivait trop tard : « Creer les unites organisationnelles » se
# lie deja avec le mot de passe de la VOUTE, tandis que slapd detient encore l'ANCIEN.
# Le deploiement echouait sur la huitieme tache, et `no_log` — qui protege la valeur —
# masquait aussi la cause. Un secret qu'on fait tourner doit descendre AVANT que quoi que
# ce soit s'en serve.
#
# Ces taches-ci n'ont pas ce probleme : elles ecrivent par `ldapi:///` en s'identifiant
# par la socket unix (root local), sans mot de passe. C'est precisement ce qui leur
# permet de reparer la liaison qu'elles changent.
- name: Trouver la base mdb (pour y poser les droits)
community.general.ldap_search:
dn: "cn=config"
scope: onelevel
filter: "(olcDatabase={1}mdb)"
attrs:
- dn
server_uri: "ldapi:///"
register: serveur_openldap_base_droits
changed_when: false
- name: Retenir le DN de la base pour les droits
ansible.builtin.set_fact:
serveur_openldap_droits_dn: "{{ (serveur_openldap_base_droits.results | first).dn }}"
when: (serveur_openldap_base_droits.results | default([]) | length) > 0
# --- ROTATION DU COMPTE D'ADMINISTRATION (2026-09-13) -------------------------------
#
# CE MOT DE PASSE N'ETAIT POSE QU'A L'INSTALLATION, par debconf. Le faire tourner en
# voute ne descendait donc nulle part : slapd gardait l'ancien, la voute annoncait le
# neuf, et les deux divergeaient EN SILENCE. Le premier a s'en apercevoir aurait ete
# `amorcage_acces`, au pire moment — quand on cree un acces.
#
# Un secret qu'on ne peut pas faire tourner est un secret qu'on ne fera pas tourner.
#
# `slappasswd` SALE le condensat : deux appels sur la meme valeur rendent deux chaines
# differentes, et comparer l'existant ne dit donc rien. On memorise l'empreinte de la
# valeur VOULUE, comme la federation Keycloak le fait pour sa liaison — meme probleme,
# meme remede, et c'est le signe qu'il s'agit d'un patron, pas d'un bricolage.
- name: Lire l'empreinte du mot de passe d'administration en place
ansible.builtin.slurp:
src: "{{ serveur_openldap_marqueur_admin }}"
register: serveur_openldap_marqueur_lu
failed_when: false
changed_when: false
- name: Hacher le mot de passe d'administration voulu
ansible.builtin.command:
argv: ["slappasswd", "-s", "{{ serveur_openldap_admin_password }}"]
register: serveur_openldap_admin_hash
changed_when: false
no_log: true
when: serveur_openldap_admin_empreinte != (serveur_openldap_marqueur_lu.content | default('') | b64decode | trim)
- name: Aligner le mot de passe d'administration de l'annuaire sur la voute
community.general.ldap_attrs:
dn: "{{ serveur_openldap_droits_dn }}"
attributes:
olcRootPW: "{{ serveur_openldap_admin_hash.stdout | trim }}"
state: exact
server_uri: "ldapi:///"
no_log: true
when:
- serveur_openldap_droits_dn | default('') | length > 0
- serveur_openldap_admin_hash is not skipped
# LE MARQUEUR S'ECRIT APRES, JAMAIS AVANT. Ecrit d'abord, une pose ratee laisserait le
# depot convaincu d'avoir fait tourner un secret qui n'a pas bouge — et la prochaine
# execution ne retenterait rien.
- name: Retenir l'empreinte du mot de passe d'administration pose
ansible.builtin.copy:
content: "{{ serveur_openldap_admin_empreinte }}\n"
dest: "{{ serveur_openldap_marqueur_admin }}"
owner: root
group: root
mode: "0600"
when: serveur_openldap_admin_hash is not skipped
- name: Creer les unites organisationnelles de base
community.general.ldap_entry:
dn: "ou={{ item }},{{ serveur_openldap_base_dn }}"
objectClass: organizationalUnit
server_uri: "ldapi:///"
bind_dn: "{{ serveur_openldap_admin_dn }}"
bind_pw: "{{ serveur_openldap_admin_password }}"
loop: "{{ serveur_openldap_ou }}"
loop_control:
label: "ou={{ item }}"
no_log: true
annuaire : un compte de service par consommateur, et la porte se ferme Keycloak, Dovecot, Postfix et Icinga Web 2 se liaient TOUS avec cn=admin, le compte d administration de la base. C est le rootDN : slapd lui fait contourner toutes les ACL. Un seul secret, quatre services, tous les droits sur l arbre — pour ce qui est, trois fois sur quatre, une simple lecture. Et les droits livres par Debian etaient intacts : `to * by * read`. Sur ldap://, sans s authentifier, une machine du reseau enumerait tous les comptes et toutes les adresses. Des comptes a droits mesures n auraient rien valu tant que cette ligne restait : on aurait ferme la porte en laissant la fenetre. L indice etait deja dans le depot. `validatePasswordPolicy` existe parce que slapd n applique pas ses controles de qualite au rootDN : la consequence etait compensee, la cause intacte. - ou=services, un compte par consommateur, secret propre en voute - sept regles d acces posees EN ENTIER (state: exact) : l ordre est la regle, et inserer c est parier sur ce que le paquet aura mis avant nous - amorcage_acces garde le compte d administration, NOMME comme l exception : il ne consomme pas l annuaire, il le provisionne depuis la socket locale - la sonde passe de -x a -Y EXTERNAL : elle lisait en anonyme et aurait annonce un annuaire VIDE sur un annuaire parfaitement sain - la rotation du compte d administration devient possible (elle n etait posee qu a l installation, par debconf : la voute et slapd divergeaient en silence) Quatre marches payees en chemin : 1. un cinquieme appelant oublie, dont l echec etait masque par no_log — la garde refuse desormais SANS no_log : elle nomme la cle absente, jamais son contenu 2. la federation Keycloak ne reecrivait son bindDn que si l URL ou le mode changeaient — nouveau secret, ancien nom, error code 49 3. la rotation placee APRES les taches qui se lient en administrateur 4. ansible-vault et son tube : sortie non bloquante = echec silencieux, la voute paraissait tournee et etait identique a l octet P72 exige que tout role incluant resoudre_annuaire NOMME son compte, et qu aucun sauf amorcage_acces ne nomme admin. Eprouvee dans les deux sens. Verifie sur l infrastructure : chaque compte lit ce qu il doit, aucun ne voit les autres, la lecture anonyme rend 0 entree, et les quatre services repondent (doveadm user, postmap -q, decouverte OIDC 200, portier SSO 200). vault_openldap_admin et vault_ldap_bind_postfix renouveles : les deux avaient transite en clair par une session d exploitation. Les anciennes valeurs rendent Invalid credentials (49). make prouver : 71 OK, 0 echec, 1 saute. ansible-lint : 0 failure, profil production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-13 13:36:38 -04:00
# --- COMPTES DE SERVICE (2026-09-13) ------------------------------------------------
#
# Quatre services interrogeaient l'annuaire avec son compte d'administration. Voir
# `defaults/main.yml` pour le raisonnement ; ici on cree les comptes et on pose les droits.
- name: Exiger un secret pour chaque compte de service
ansible.builtin.assert:
that:
- lookup('vars', item.secret, default='') | length > 0
fail_msg: >-
Le compte de service « {{ item.cn }} » n'a pas de secret : {{ item.secret }} est
absent ou vide dans la voute. Un compte sans mot de passe ne se lie pas, et le
service qui en depend echouerait a la premiere requete.
loop: "{{ serveur_openldap_comptes_service }}"
loop_control:
label: "{{ item.cn }}"
no_log: true
- name: Creer les comptes de service de l'annuaire
community.general.ldap_entry:
dn: "cn={{ item.cn }},{{ serveur_openldap_services_dn }}"
objectClass:
- simpleSecurityObject
- organizationalRole
attributes:
description: "{{ item.raison }}"
userPassword: "{{ lookup('vars', item.secret) }}"
server_uri: "ldapi:///"
bind_dn: "{{ serveur_openldap_admin_dn }}"
bind_pw: "{{ serveur_openldap_admin_password }}"
loop: "{{ serveur_openldap_comptes_service }}"
loop_control:
label: "{{ item.cn }}"
no_log: true
# `ldap_entry` ne fait que CREER : un secret tourne en voute ne descendrait jamais dans
# l'annuaire, et la rotation paraitrait faite. `ldap_passwd` compare avant d'ecrire, donc
# il est idempotent ET il suit la voute. La lecon est deja celle de la politique de mots
# de passe, quinze lignes plus bas.
- name: Aligner le mot de passe de chaque compte de service sur la voute
community.general.ldap_passwd:
dn: "cn={{ item.cn }},{{ serveur_openldap_services_dn }}"
passwd: "{{ lookup('vars', item.secret) }}"
server_uri: "ldapi:///"
bind_dn: "{{ serveur_openldap_admin_dn }}"
bind_pw: "{{ serveur_openldap_admin_password }}"
loop: "{{ serveur_openldap_comptes_service }}"
loop_control:
label: "{{ item.cn }}"
no_log: true
# L'ORDRE DES REGLES EST LA REGLE. slapd applique la PREMIERE dont la cible correspond et
# s'arrete : une regle generale placee trop haut rend muettes toutes celles qui suivent.
# On pose donc la liste ENTIERE (`state: exact`) plutot que d'inserer — inserer, ici,
# c'est parier sur ce que le paquet Debian aura mis avant nous.
#
# {0} LES MOTS DE PASSE, D'ABORD. Cette regle porte sur un ATTRIBUT : elle vaut pour
# toutes les entrees, comptes de service compris — c'est elle qui leur permet de se
# lier (`by anonymous auth`). La retirer ou la deplacer plus bas ferme l'annuaire a
# tout le monde, y compris a l'exploitant.
# {2} LES COMPTES DE SERVICE NE SE VOIENT PAS ENTRE EUX. Compromettre Postfix ne doit pas
# livrer le compte de Keycloak. Le rootDN, lui, contourne les ACL : l'exploitant garde
# la main.
# {3} LES PERSONNES. Keycloak ecrit (mots de passe, creations) ; les trois autres lisent ;
# chacun se lit soi-meme.
# {5} LE RACINE DE L'ANNUAIRE reste lisible : sans lui, aucun client ne peut meme
# decouvrir les contextes servis, et les messages parlent alors de « base introuvable ».
- name: Poser les droits d'acces de l'annuaire
community.general.ldap_attrs:
dn: "{{ serveur_openldap_droits_dn }}"
attributes:
olcAccess: "{{ serveur_openldap_acces }}"
state: exact
server_uri: "ldapi:///"
when: serveur_openldap_droits_dn | default('') | length > 0
# --- Politique de mot de passe (overlay ppolicy) ------------------------------
# Sans cet overlay, `pwdReset` n'existe pas dans le schema : `amorcage_acces` ne peut
# pas forcer le changement du jeton d'amorcage, et rien ne contraint la qualite ni ne
# verrouille apres des echecs. Voir docs/autorisation.md §3.
- name: Ppolicy — Charger le module
community.general.ldap_attrs:
dn: "cn=module{0},cn=config"
attributes:
olcModuleLoad: "ppolicy"
state: present
server_uri: "ldapi:///"
when: serveur_openldap_ppolicy_actif | bool
# Le DN de la base varie (`olcDatabase={1}mdb` en general, mais l'index n'est pas
# garanti) : on le LIT au lieu de le supposer.
- name: Ppolicy — Trouver la base mdb
community.general.ldap_search:
dn: "cn=config"
scope: onelevel
filter: "(olcDatabase={1}mdb)"
attrs:
- dn
server_uri: "ldapi:///"
register: serveur_openldap_base_mdb
changed_when: false
when: serveur_openldap_ppolicy_actif | bool
- name: Ppolicy — Retenir le DN de la base
ansible.builtin.set_fact:
serveur_openldap_mdb_dn: >-
{{ (serveur_openldap_base_mdb.results | first).dn }}
when:
- serveur_openldap_ppolicy_actif | bool
- (serveur_openldap_base_mdb.results | default([]) | length) > 0
identite : une declaration de politique de mot de passe, deux executants Quatre defauts mesures dans l'integration Keycloak/LDAP, meme famille : une valeur declaree d'un cote, consommee de l'autre, rien qui verifie la jonction. 1. Aucune regle ne s'appliquait sur le chemin d'un vrai utilisateur. Sonde : « abcd » refuse par l'operation etendue LDAP, accepte par Keycloak (204), puis actif pour l'authentification. Keycloak ecrivait userPassword en direct (ppolicy aveugle) et le realm n'avait aucune passwordPolicy. 2. ldap_entry ne fait que CREER : la politique etait figee a sa creation. Le depot disait pwdMustChange TRUE, le serveur FALSE — une reconstruction from-zero aurait ressuscite la boucle du 2026-08-07. ldap_attrs state=exact reconcilie la politique et l'overlay (DN lu, pas devine). 3. syncRegistrations absent : un compte cree dans Keycloak n'atteignait jamais ou=people — acces web, aucune boite, invisible du modele de groupes. 4. Le prenom pointait sur cn (nom complet) : « Administrateur systeme systeme ». Ajoute roles/resoudre_politique_mdp : LA declaration, traduite en pwdPolicy, passwordPolicy et anti-force-brute. Les deux roles la consomment sans la redeclarer. usePasswordModifyExtendedOp ET validatePasswordPolicy : la seconde est porteuse, Keycloak se liant en rootDN et slapd n'appliquant pas ses controles de qualite au rootDN. La premiere seule aurait paru juste sans tenir. Verification : abcd -> 400 « minimum length 12 » et absent de LDAP ; mot de passe conforme -> 204 puis ldapwhoami accepte ; POST users -> 201 ET present dans ou=people. Second passage des deux playbooks : changed=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 06:28:10 -04:00
- name: Ppolicy — Résoudre la politique (déclaration unique)
ansible.builtin.include_role:
name: resoudre_politique_mdp
when: serveur_openldap_ppolicy_actif | bool
- name: Ppolicy — Creer l unite des politiques
community.general.ldap_entry:
dn: "ou=policies,{{ serveur_openldap_base_dn }}"
objectClass: organizationalUnit
server_uri: "ldapi:///"
bind_dn: "{{ serveur_openldap_admin_dn }}"
bind_pw: "{{ serveur_openldap_admin_password }}"
no_log: true
when: serveur_openldap_ppolicy_actif | bool
# L'overlay AVANT la politique : `olcPPolicyDefault` reference une entree qui doit
# exister, mais l'overlay doit etre en place pour que le schema pwd* soit connu.
# L'ordre inverse echouerait des deux cotes.
- name: Ppolicy — Activer l overlay sur la base
community.general.ldap_entry:
dn: "olcOverlay=ppolicy,{{ serveur_openldap_mdb_dn }}"
objectClass:
- olcOverlayConfig
- olcPPolicyConfig
attributes:
olcOverlay: "ppolicy"
olcPPolicyDefault: "cn=default,ou=policies,{{ serveur_openldap_base_dn }}"
olcPPolicyHashCleartext: "{{ 'TRUE' if serveur_openldap_ppolicy_hash_cleartext | bool else 'FALSE' }}"
olcPPolicyUseLockout: "{{ 'TRUE' if serveur_openldap_ppolicy_use_lockout | bool else 'FALSE' }}"
server_uri: "ldapi:///"
when:
- serveur_openldap_ppolicy_actif | bool
- serveur_openldap_mdb_dn is defined
- name: Ppolicy — Creer la politique par defaut
community.general.ldap_entry:
dn: "cn=default,ou=policies,{{ serveur_openldap_base_dn }}"
objectClass:
- pwdPolicy
- person
- top
attributes: >-
identite : une declaration de politique de mot de passe, deux executants Quatre defauts mesures dans l'integration Keycloak/LDAP, meme famille : une valeur declaree d'un cote, consommee de l'autre, rien qui verifie la jonction. 1. Aucune regle ne s'appliquait sur le chemin d'un vrai utilisateur. Sonde : « abcd » refuse par l'operation etendue LDAP, accepte par Keycloak (204), puis actif pour l'authentification. Keycloak ecrivait userPassword en direct (ppolicy aveugle) et le realm n'avait aucune passwordPolicy. 2. ldap_entry ne fait que CREER : la politique etait figee a sa creation. Le depot disait pwdMustChange TRUE, le serveur FALSE — une reconstruction from-zero aurait ressuscite la boucle du 2026-08-07. ldap_attrs state=exact reconcilie la politique et l'overlay (DN lu, pas devine). 3. syncRegistrations absent : un compte cree dans Keycloak n'atteignait jamais ou=people — acces web, aucune boite, invisible du modele de groupes. 4. Le prenom pointait sur cn (nom complet) : « Administrateur systeme systeme ». Ajoute roles/resoudre_politique_mdp : LA declaration, traduite en pwdPolicy, passwordPolicy et anti-force-brute. Les deux roles la consomment sans la redeclarer. usePasswordModifyExtendedOp ET validatePasswordPolicy : la seconde est porteuse, Keycloak se liant en rootDN et slapd n'appliquant pas ses controles de qualite au rootDN. La premiere seule aurait paru juste sans tenir. Verification : abcd -> 400 « minimum length 12 » et absent de LDAP ; mot de passe conforme -> 204 puis ldapwhoami accepte ; POST users -> 201 ET present dans ou=people. Second passage des deux playbooks : changed=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 06:28:10 -04:00
{{ {'cn': 'default', 'sn': 'default'}
| combine(resoudre_politique_mdp_ldap, serveur_openldap_ppolicy_extra) }}
server_uri: "ldapi:///"
bind_dn: "{{ serveur_openldap_admin_dn }}"
bind_pw: "{{ serveur_openldap_admin_password }}"
no_log: true
when: serveur_openldap_ppolicy_actif | bool
identite : une declaration de politique de mot de passe, deux executants Quatre defauts mesures dans l'integration Keycloak/LDAP, meme famille : une valeur declaree d'un cote, consommee de l'autre, rien qui verifie la jonction. 1. Aucune regle ne s'appliquait sur le chemin d'un vrai utilisateur. Sonde : « abcd » refuse par l'operation etendue LDAP, accepte par Keycloak (204), puis actif pour l'authentification. Keycloak ecrivait userPassword en direct (ppolicy aveugle) et le realm n'avait aucune passwordPolicy. 2. ldap_entry ne fait que CREER : la politique etait figee a sa creation. Le depot disait pwdMustChange TRUE, le serveur FALSE — une reconstruction from-zero aurait ressuscite la boucle du 2026-08-07. ldap_attrs state=exact reconcilie la politique et l'overlay (DN lu, pas devine). 3. syncRegistrations absent : un compte cree dans Keycloak n'atteignait jamais ou=people — acces web, aucune boite, invisible du modele de groupes. 4. Le prenom pointait sur cn (nom complet) : « Administrateur systeme systeme ». Ajoute roles/resoudre_politique_mdp : LA declaration, traduite en pwdPolicy, passwordPolicy et anti-force-brute. Les deux roles la consomment sans la redeclarer. usePasswordModifyExtendedOp ET validatePasswordPolicy : la seconde est porteuse, Keycloak se liant en rootDN et slapd n'appliquant pas ses controles de qualite au rootDN. La premiere seule aurait paru juste sans tenir. Verification : abcd -> 400 « minimum length 12 » et absent de LDAP ; mot de passe conforme -> 204 puis ldapwhoami accepte ; POST users -> 201 ET present dans ou=people. Second passage des deux playbooks : changed=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 06:28:10 -04:00
# `ldap_entry` ne fait que CREER : l'entree restait figee a ce qu'elle valait le
# jour de sa creation, et toute modification ulterieure de la declaration etait
# ignoree en silence. Constate le 2026-08-08 — le depot annoncait
# `pwdMustChange: TRUE`, le serveur portait `FALSE` (corrige a la main apres la
# boucle de changement de mot de passe du 2026-08-07), et une reconstruction
# from-zero aurait ressuscite le defaut. Il faut donc RECONCILIER en plus.
- name: Ppolicy — Reconcilier la politique par defaut
community.general.ldap_attrs:
dn: "cn=default,ou=policies,{{ serveur_openldap_base_dn }}"
attributes: "{{ resoudre_politique_mdp_ldap | combine(serveur_openldap_ppolicy_extra) }}"
state: exact
server_uri: "ldapi:///"
bind_dn: "{{ serveur_openldap_admin_dn }}"
bind_pw: "{{ serveur_openldap_admin_password }}"
no_log: true
when: serveur_openldap_ppolicy_actif | bool
# Meme raison : l'overlay non plus n'etait pose qu'a la creation. Son DN porte un
# index d'ordre (`{0}`, `{1}`…) attribue par slapd — on le LIT, on ne le devine pas.
- name: Ppolicy — Trouver le DN de l overlay
community.general.ldap_search:
dn: "{{ serveur_openldap_mdb_dn }}"
scope: onelevel
filter: "(olcOverlay=*ppolicy)"
attrs:
- dn
server_uri: "ldapi:///"
register: serveur_openldap_overlay_ppolicy
changed_when: false
when:
- serveur_openldap_ppolicy_actif | bool
- serveur_openldap_mdb_dn is defined
- name: Ppolicy — Reconcilier les reglages de l overlay
community.general.ldap_attrs:
dn: "{{ (serveur_openldap_overlay_ppolicy.results | first).dn }}"
attributes:
olcPPolicyDefault: "cn=default,ou=policies,{{ serveur_openldap_base_dn }}"
olcPPolicyHashCleartext: "{{ 'TRUE' if serveur_openldap_ppolicy_hash_cleartext | bool else 'FALSE' }}"
olcPPolicyUseLockout: "{{ 'TRUE' if serveur_openldap_ppolicy_use_lockout | bool else 'FALSE' }}"
state: exact
server_uri: "ldapi:///"
when:
- serveur_openldap_ppolicy_actif | bool
- (serveur_openldap_overlay_ppolicy.results | default([]) | length) > 0
# --- TLS (LDAPS + STARTTLS) via le certificat step_ca depose par client_pki ---
- name: TLS — creer le repertoire pont lisible par openldap
ansible.builtin.file:
path: "{{ serveur_openldap_tls_dir }}"
state: directory
owner: openldap
group: openldap
mode: "0750"
when: serveur_openldap_tls_actif | bool
- name: TLS — deployer le script de synchronisation du certificat
ansible.builtin.template:
src: setops-slapd-cert-sync.sh.j2
dest: /usr/local/sbin/setops-slapd-cert-sync
owner: root
group: root
mode: "0755"
when: serveur_openldap_tls_actif | bool
- name: TLS — deployer les unites de synchronisation (service + path)
ansible.builtin.template:
src: "{{ item.s }}"
dest: "{{ item.d }}"
owner: root
group: root
mode: "0644"
loop:
- { s: "setops-slapd-cert-sync.service.j2", d: "/etc/systemd/system/setops-slapd-cert-sync.service" }
- { s: "setops-slapd-cert-sync.path.j2", d: "/etc/systemd/system/setops-slapd-cert-sync.path" }
loop_control:
label: "{{ item.d | basename }}"
when: serveur_openldap_tls_actif | bool
- name: TLS — synchroniser le certificat maintenant (si client_pki l'a depose)
ansible.builtin.command: /usr/local/sbin/setops-slapd-cert-sync
changed_when: false
when:
- serveur_openldap_tls_actif | bool
- not ansible_check_mode
- name: TLS — activer la surveillance du certificat (renouvellement)
ansible.builtin.systemd:
name: setops-slapd-cert-sync.path
enabled: true
state: started
daemon_reload: true
when:
- serveur_openldap_tls_actif | bool
- not ansible_check_mode
- name: TLS — verifier que le certificat pont est en place
ansible.builtin.stat:
path: "{{ serveur_openldap_tls_dir }}/cert.pem"
register: serveur_openldap_cert_pont
when: serveur_openldap_tls_actif | bool
- name: TLS — avertir si le certificat n'est pas encore disponible
ansible.builtin.debug:
msg: >-
client_pki n'a pas encore depose de certificat sur ce noeud
({{ serveur_openldap_tls_source_cert }}). TLS slapd NON configure : deploie
l'integration client_pki sur ce noeud, puis relance le deploiement.
when:
- serveur_openldap_tls_actif | bool
- not ansible_check_mode
- not (serveur_openldap_cert_pont.stat.exists | default(false))
- name: TLS — configurer slapd (olcTLS* dans cn=config)
community.general.ldap_attrs:
dn: cn=config
server_uri: ldapi:///
attributes:
olcTLSCACertificateFile: "{{ serveur_openldap_tls_dir }}/ca.pem"
olcTLSCertificateFile: "{{ serveur_openldap_tls_dir }}/cert.pem"
olcTLSCertificateKeyFile: "{{ serveur_openldap_tls_dir }}/key.pem"
state: exact
when:
- serveur_openldap_tls_actif | bool
- not ansible_check_mode
- serveur_openldap_cert_pont.stat.exists | default(false)
notify: Redemarrer slapd
- name: TLS — exposer LDAPS (SLAPD_SERVICES)
ansible.builtin.lineinfile:
path: /etc/default/slapd
regexp: '^SLAPD_SERVICES='
line: 'SLAPD_SERVICES="{{ serveur_openldap_services }}"'
when:
- serveur_openldap_tls_actif | bool
- serveur_openldap_cert_pont.stat.exists | default(false)
notify: Redemarrer slapd
sondes : les cinq qui manquaient le plus (autorite, base, annuaire, zones, edge) CORRECTION DE COMPTE D ABORD. J avais annonce cinq groupes sans sonde. La mesure en donne VINGT-SEPT : j avais compte ceux que j avais en tete, pas ceux que le depot contient. Il en reste vingt-deux. LES CINQ, PAR ORDRE DE DEGAT SILENCIEUX. autorite (step_ca) — la plus urgente : nos certificats vivent 24 h, une AC muette ne casse rien aujourd hui et casse TOUT demain, d un coup, sur les 21 machines. Elle surveille aussi l expiration de la RACINE, que personne ne regarde parce qu elle vit des annees (relevee a 3642 jours). base (postgresql) — une VRAIE requete, pas pg_isready : celui-ci dit que le port repond, pas que la base sert. Plus le compte des connexions : a saturation, chaque application tombe sans que la base ait l air morte. annuaire (openldap) — elle COMPTE les entrees. Un annuaire vide repond success a tout : le mensonge des sauvegardes vides, vert et sans contenu. zones (powerdns) — un autoritatif sans zone repond NXDOMAIN a tout, ce qui se lit comme « ce nom n existe pas ». edge (nginx) — elle valide la configuration SUR DISQUE : nginx sert la derniere valide, et une configuration cassee ne se voit qu au prochain demarrage, souvent des mois plus tard. Les cinq eprouvees vertes sur le sain puis rouges PAR PARAMETRE, sans toucher a un service. Etat : 20 sondes sur 19 roles, 23 services distincts, 70 instances, 67 au vert. Les trois autres sont connues : deux sauvegardes sans donnee a emporter, et Loki en delai de stabilisation apres redeploiement. make prouver : CONFORME, 64 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 03:47:03 -04:00
# --- L'ANNUAIRE D'UNE INCARNATION PRECEDENTE (2026-09-30) ---------------------------
#
# EN FIN DE ROLE, PAS AU DEBUT. Le LDIF porte des attributs de `ppolicy` (`pwdChangedTime`,
# `pwdReset`...) que slapd refuse tant que l'overlay n'est pas charge — c'est-a-dire plus
# haut dans ce role. On remplace donc ce que le role vient de creer par ce qui vivait
# avant ; `setops-restaurer` le rejoue d'abord a blanc, et met la base neuve de cote.
#
# Sans ceci, une reconstruction rendait a `sysadmin` le mot de passe d'amorcage : c'est
# ainsi que l'absence de restauration a ete decouverte, le 2026-09-30.
- name: Restauration — l'annuaire d'une incarnation precedente ?
ansible.builtin.include_role:
name: client_backup
tasks_from: restaurer.yml
vars:
client_backup_restaurer_jeu: serveur_openldap
client_backup_restaurer_vierge: "{{ (serveur_openldap_entrees_initiales.stdout | default('1') | int) == 0 }}"
- name: Restauration — remettre l'annuaire
changed_when: true
ansible.builtin.command:
argv:
- /usr/local/sbin/setops-restaurer
- annuaire
- --instantane
- "{{ client_backup_restauration.instantane }}"
- "{{ serveur_openldap_base_dn }}"
when: client_backup_restauration.etat == 'a_restaurer'
# Les comptes de service reviennent avec le mot de passe de l'EPOQUE. On les realigne sur
# la voute tout de suite : Keycloak, Postfix et Dovecot s'y lient avec la valeur actuelle.
- name: Restauration — realigner les comptes de service sur la voute
community.general.ldap_passwd:
dn: "cn={{ item.cn }},{{ serveur_openldap_services_dn }}"
passwd: "{{ lookup('vars', item.secret) }}"
server_uri: "ldapi:///"
bind_dn: "{{ serveur_openldap_admin_dn }}"
bind_pw: "{{ serveur_openldap_admin_password }}"
loop: "{{ serveur_openldap_comptes_service }}"
loop_control:
label: "{{ item.cn }}"
no_log: true
when: client_backup_restauration.etat == 'a_restaurer'
- name: Restauration — acter l'annuaire remis
ansible.builtin.include_role:
name: client_backup
tasks_from: acter.yml
vars:
client_backup_acter_etat: restaure
when: client_backup_restauration.etat == 'a_restaurer'
sondes : les cinq qui manquaient le plus (autorite, base, annuaire, zones, edge) CORRECTION DE COMPTE D ABORD. J avais annonce cinq groupes sans sonde. La mesure en donne VINGT-SEPT : j avais compte ceux que j avais en tete, pas ceux que le depot contient. Il en reste vingt-deux. LES CINQ, PAR ORDRE DE DEGAT SILENCIEUX. autorite (step_ca) — la plus urgente : nos certificats vivent 24 h, une AC muette ne casse rien aujourd hui et casse TOUT demain, d un coup, sur les 21 machines. Elle surveille aussi l expiration de la RACINE, que personne ne regarde parce qu elle vit des annees (relevee a 3642 jours). base (postgresql) — une VRAIE requete, pas pg_isready : celui-ci dit que le port repond, pas que la base sert. Plus le compte des connexions : a saturation, chaque application tombe sans que la base ait l air morte. annuaire (openldap) — elle COMPTE les entrees. Un annuaire vide repond success a tout : le mensonge des sauvegardes vides, vert et sans contenu. zones (powerdns) — un autoritatif sans zone repond NXDOMAIN a tout, ce qui se lit comme « ce nom n existe pas ». edge (nginx) — elle valide la configuration SUR DISQUE : nginx sert la derniere valide, et une configuration cassee ne se voit qu au prochain demarrage, souvent des mois plus tard. Les cinq eprouvees vertes sur le sain puis rouges PAR PARAMETRE, sans toucher a un service. Etat : 20 sondes sur 19 roles, 23 services distincts, 70 instances, 67 au vert. Les trois autres sont connues : deux sauvegardes sans donnee a emporter, et Loki en delai de stabilisation apres redeploiement. make prouver : CONFORME, 64 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 03:47:03 -04:00
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la
# fait tourner et pousse le verdict, sans savoir ce qu'elle mesure.
- name: Assurer le repertoire des sondes de supervision
ansible.builtin.file:
path: /usr/local/lib/setops/sondes
state: directory
owner: root
group: root
mode: "0755"
- name: Deposer la sonde « annuaire »
ansible.builtin.template:
src: sonde-annuaire.sh.j2
dest: /usr/local/lib/setops/sondes/annuaire.sh
owner: root
group: root
mode: "0750"