openldap : charge ppolicy — le jeton d'amorcage devient a usage unique

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 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-07 13:52:00 -04:00
parent ba70b80197
commit 69536a2b1a
4 changed files with 158 additions and 5 deletions

View file

@ -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 <jeton>
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

View file

@ -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.

View file

@ -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

View file

@ -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: