From 69536a2b1a617e5e641ee2e5979a64cbad4f77a0 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Fri, 7 Aug 2026 13:52:00 -0400 Subject: [PATCH] =?UTF-8?q?openldap=20:=20charge=20ppolicy=20=E2=80=94=20l?= =?UTF-8?q?e=20jeton=20d'amorcage=20devient=20a=20usage=20unique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le module etait sur disque mais jamais charge (seul back_mdb l'etait). Depuis OpenLDAP 2.5 son schema est INTEGRE au module : aucun .ldif a charger. La contrainte mord, mesuree sur un compte fraichement amorce : Insufficient access (50) Operations are restricted to bind/unbind/abandon/StartTLS/modify password Le sysadmin peut se connecter et RIEN d'autre que changer son mot de passe. Ce que la doctrine promettait est garanti techniquement, plus seulement demande. `pwdMustChange` est ce qui donne son effet a `pwdReset` : sans lui, marquer une entree n'oblige a rien. La politique apporte aussi longueur minimale 12, verrouillage apres 5 echecs, historique. `olcPPolicyUseLockout` reste FALSE : annoncer « compte verrouille » renseignerait un attaquant sur son existence. Le DN de la base est LU, pas suppose : olcDatabase={1}mdb est l'usage mais l'index n'est pas garanti. La detection du role d'amorcage s'est verifiee d'elle-meme : rejoue apres le chargement, il annonce « Changement FORCE » la ou il disait l'inverse une heure plus tot. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 35 +++++++++++ docs/autorisation.md | 10 +-- roles/serveur_openldap/defaults/main.yml | 38 +++++++++++ roles/serveur_openldap/tasks/main.yml | 80 ++++++++++++++++++++++++ 4 files changed, 158 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9454649..72c3b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ ## 2026-08-06 — le chemin nord-sud devient dérivable +### `ppolicy` chargé — le jeton d'amorçage devient vraiment à usage unique + +`serveur_openldap` charge désormais l'overlay `ppolicy` et pose une politique par défaut. +Le module était sur disque (`/usr/lib/ldap/ppolicy.so`) mais jamais chargé : seul +`back_mdb` l'était. Depuis OpenLDAP 2.5 son schéma est **intégré au module** — aucun +`.ldif` à charger, contrairement à 2.4. + +**La contrainte mord**, mesuré sur un compte fraîchement amorcé : + +``` +$ ldapsearch -D uid=sysadmin,… -w +Insufficient access (50) +Operations are restricted to bind/unbind/abandon/StartTLS/modify password +``` + +Le sysadmin peut se connecter et **rien d'autre** que changer son mot de passe. Ce que la +doctrine promettait est maintenant garanti techniquement, pas seulement demandé. + +`pwdMustChange` est ce qui donne son effet à `pwdReset` : sans lui, marquer une entrée +n'oblige à rien. Les deux vont ensemble, et c'est le genre de couple qu'on découvre en le +testant. La politique apporte aussi la longueur minimale (12 — `pwdMinLength` n'est appliqué +que si `pwdCheckQuality` > 0), le verrouillage après 5 échecs et l'historique. + +`olcPPolicyUseLockout` reste à `FALSE`, délibérément : répondre « ce compte est verrouillé » +renseignerait un attaquant sur l'existence du compte. + +**Le DN de la base est lu, pas supposé.** `olcDatabase={1}mdb` est l'usage courant mais +l'index n'est pas garanti — le rôle le cherche. + +**Et la détection du rôle d'amorçage s'est vérifiée d'elle-même** : rejoué après le +chargement, il annonce « Changement FORCÉ à la première ouverture (ppolicy actif) » là où il +disait l'inverse une heure plus tôt. C'est exactement pourquoi il détecte au lieu de +supposer. + + ### Le rôle d'amorçage : `amorcage_acces` Crée **un** compte (`uid=sysadmin`) et **un** groupe (`cn=sysadmin`) dans LDAP, puis se diff --git a/docs/autorisation.md b/docs/autorisation.md index eaaf65a..6028e0d 100644 --- a/docs/autorisation.md +++ b/docs/autorisation.md @@ -103,9 +103,9 @@ ansible-vault view instance/inventories/principal/group_vars/all/vault.yml \ | grep vault_sysadmin_amorcage ``` -**Change-le immédiatement.** Tant que l'overlay `ppolicy` n'est pas chargé, rien ne -l'impose techniquement (§3) — c'est à toi de le faire, et c'est le premier geste de la -reprise. +**Tu ne pourras rien faire d'autre avant de l'avoir changé** : l'annuaire refuse toute +opération sauf le changement lui-même (§3). C'est le premier geste de la reprise, et il +n'est pas optionnel. > Le mot de passe de la voûte est lu depuis `ANSIBLE_VAULT_PASSWORD_FILE` > (`~/.config/setops-vault-pass` par défaut). **Sans ce fichier, rien de ce qui suit n'est @@ -139,8 +139,8 @@ dépendent ni de Keycloak, ni de l'annuaire. C'est le sens de D-40. ### 6.4 Ce que tu dois changer en priorité -1. **Le mot de passe d'amorçage** — dès la première connexion. Rien ne l'impose encore : - voir §3. +1. **Le mot de passe d'amorçage** — imposé dès la première connexion, tu n'as pas le + choix (§3). 2. **Les comptes de secours** générés au déploiement : ils sont en voûte, et l'auteur du déploiement y a eu accès. Les régénérer transfère réellement le contrôle. 3. **Le mot de passe de la voûte** elle-même, si le dépôt change de mains. diff --git a/roles/serveur_openldap/defaults/main.yml b/roles/serveur_openldap/defaults/main.yml index 0ff7b81..9823950 100644 --- a/roles/serveur_openldap/defaults/main.yml +++ b/roles/serveur_openldap/defaults/main.yml @@ -32,3 +32,41 @@ serveur_openldap_tls_source_ca: "/etc/step/certs/root_ca.crt" serveur_openldap_tls_dir: "/etc/ldap/tls" # Services slapd exposes (ldaps ajoute pour le TLS ; ldapi pour l'admin local). serveur_openldap_services: "ldap:/// ldaps:/// ldapi:///" + +# --- Politique de mot de passe (overlay ppolicy) ------------------------------ +# Sans cet overlay, `pwdReset` n'existe pas dans le schema : le changement force a +# la premiere ouverture est impossible, et rien ne contraint la qualite ni ne +# verrouille apres des echecs repetes. Mesure le 2026-08-07 : seul `back_mdb` +# etait charge. Voir docs/autorisation.md §3. +# +# Depuis OpenLDAP 2.5, le schema ppolicy est INTEGRE au module : aucun fichier +# `.ldif` a charger, contrairement a 2.4. +serveur_openldap_ppolicy_actif: true + +# `pwdMustChange` est ce qui donne son effet a `pwdReset` : sans lui, marquer une +# entree n'oblige a rien. Les deux vont ensemble. +serveur_openldap_ppolicy: + pwdMustChange: "TRUE" + # Qualite : 1 = verifier si possible, accepter sinon. `pwdMinLength` n'est + # applique QUE si `pwdCheckQuality` > 0. + pwdCheckQuality: "1" + pwdMinLength: "12" + # Verrouillage apres echecs repetes. `pwdLockoutDuration` en secondes. + pwdLockout: "TRUE" + pwdMaxFailure: "5" + pwdFailureCountInterval: "900" + pwdLockoutDuration: "900" + # Historique : interdit de reprendre un mot de passe recent. + pwdInHistory: "3" + # L'utilisateur peut changer son propre mot de passe — indispensable, puisque + # `pwdMustChange` l'y oblige. + pwdAllowUserChange: "TRUE" + +# Reglages de l'OVERLAY lui-meme (pas de la politique). `HashCleartext` hache un +# mot de passe recu en clair au lieu de le stocker tel quel : filet de securite si +# un client oublie de hacher. +# `UseLockout` laisse volontairement a FALSE : dire « ce compte est verrouille » +# renseigne un attaquant sur l'existence du compte. L'utilisateur legitime voit +# « identifiants invalides » et attend la duree de verrouillage. +serveur_openldap_ppolicy_hash_cleartext: true +serveur_openldap_ppolicy_use_lockout: false diff --git a/roles/serveur_openldap/tasks/main.yml b/roles/serveur_openldap/tasks/main.yml index 91b49ad..9f41e51 100644 --- a/roles/serveur_openldap/tasks/main.yml +++ b/roles/serveur_openldap/tasks/main.yml @@ -66,6 +66,86 @@ label: "ou={{ item }}" no_log: true +# --- 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 — 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', + 'pwdAttribute': 'userPassword'} | combine(serveur_openldap_ppolicy) }} + server_uri: "ldapi:///" + bind_dn: "{{ serveur_openldap_admin_dn }}" + bind_pw: "{{ serveur_openldap_admin_password }}" + no_log: true + when: serveur_openldap_ppolicy_actif | bool + # --- TLS (LDAPS + STARTTLS) via le certificat step_ca depose par client_pki --- - name: TLS — creer le repertoire pont lisible par openldap ansible.builtin.file: