Set-OPS-Public/roles/serveur_openldap/tasks/main.yml
Daniel Allaire 8a9da0c31a restauration : la reconstruction remet l'etat de l'incarnation precedente
Chaque role proprietaire (AC, bases, annuaire, Nextcloud, rspamd/DKIM,
courriel, forge, web) remet son etat au moment ou il le creerait neuf,
depuis le dernier instantane anterieur a la naissance de la machine.
La sauvegarde refuse de deposer tant qu'un etat d'avant attend.
Outil de noeud setops-restaurer ; cibles sauvegarder-maintenant,
restauration-etat, restauration-renoncer ; test_restauration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:39:22 -04:00

529 lines
21 KiB
YAML

---
- 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.
- name: Restauration — l'annuaire est-il vierge ?
ansible.builtin.shell: >-
set -o pipefail;
slapcat -b '{{ serveur_openldap_base_dn }}' 2>/dev/null | grep '^dn:'
| grep -v -i -x -e 'dn: {{ serveur_openldap_base_dn }}' -e 'dn: cn=admin,{{ serveur_openldap_base_dn }}'
| wc -l
args:
executable: /bin/bash
register: serveur_openldap_entrees_initiales
changed_when: false
check_mode: false
# 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
# --- 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
- 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: >-
{{ {'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
# `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
# --- 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'
# --- 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"