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>
This commit is contained in:
Daniel Allaire 2026-08-08 06:28:10 -04:00
parent 775df924cb
commit 419ecaba63
12 changed files with 444 additions and 22 deletions

View file

@ -1,5 +1,54 @@
# CHANGELOG — Set-OPS
## 2026-08-08 — La politique de mot de passe existait des deux côtés et ne s'appliquait d'aucun
Question de l'exploitant : « l'intégration Keycloak/LDAP est incomplète, non ? » Elle
l'était, et pas cosmétiquement. Quatre défauts mesurés, tous de la même famille — une
valeur déclarée d'un côté, consommée de l'autre, sans que rien ne vérifie qu'elles se
rejoignent.
**1. Aucune règle de mot de passe ne s'appliquait sur le chemin d'un vrai utilisateur.**
Compte sonde, même mot de passe `abcd` : l'opération étendue LDAP le refuse
(`Constraint violation (19) — Password fails quality checking policy`), Keycloak l'accepte
(`204`), et `ldapwhoami` avec `abcd` réussit ensuite. Deux causes empilées : Keycloak
écrivait `userPassword` **directement**, donc l'overlay `ppolicy` n'interceptait rien ; et
le realm n'avait aucune `passwordPolicy`. Chacun déléguait la vérification à l'autre.
**2. `ldap_entry` ne fait que créer.** L'entrée `cn=default,ou=policies` était figée à ce
qu'elle valait le jour de sa création : toute modification ultérieure de la déclaration
était ignorée en silence. Le dépôt annonçait `pwdMustChange: TRUE`, le serveur portait
`FALSE` (corrigé à la main après la boucle de changement de mot de passe du 2026-08-07).
Une reconstruction from-zero aurait donc **ressuscité** le défaut. `ldap_attrs state: exact`
réconcilie désormais la politique **et** les réglages de l'overlay — dont le DN, qui porte
un index attribué par slapd, est lu et non deviné.
**3. Un compte créé dans Keycloak n'atteignait jamais l'annuaire.** `POST users``201`,
rien dans `ou=people` : `syncRegistrations` était absent. Ce compte aurait eu un accès web,
aucune boîte aux lettres, et serait resté invisible du modèle de groupes — Postfix et
Dovecot lisent LDAP, pas Keycloak. C'est la divergence nettoyée le matin même sur l'adresse
du sysadmin, réintroduite par une autre porte.
**4. Le prénom était mappé sur `cn`.** Dans `inetOrgPerson`, `cn` porte le nom *complet* :
Keycloak affichait « Administrateur systeme systeme ». Le mappeur pointe désormais sur
`givenName`, que `amorcage_acces` écrit, dérivé par le même découpage que `sn`.
**Ce qui est ajouté.** `roles/resoudre_politique_mdp/` porte **la** déclaration, en termes
neutres, et la traduit dans les trois dialectes qui doivent l'appliquer : `pwdPolicy`,
`passwordPolicy` du realm, protection anti-force-brute. `serveur_openldap` et
`serveur_keycloak` la consomment ; aucun des deux ne la redéclare.
La fédération est durcie de six clés, dont deux portent la correction et se complètent :
`usePasswordModifyExtendedOp` (slapd voit passer le changement) et `validatePasswordPolicy`
(Keycloak valide avant d'écrire). La seconde est la porteuse — Keycloak se lie en rootDN, et
slapd n'applique pas ses contrôles de qualité au rootDN. S'en remettre à la première seule
aurait donné une correction qui *paraît* juste et ne tient pas ; c'est le test qui a tranché,
pas le raisonnement.
**Vérification, mêmes sondes qu'au diagnostic** — `abcd` → `400 Invalid password: minimum
length 12`, absent de LDAP ; mot de passe conforme → `204` puis `ldapwhoami` accepté (l'écriture
traversante reste intacte) ; `POST users``201` **et** `dn: uid=setops-sonde2,ou=people,…`.
Sondes supprimées des deux côtés. Second passage des deux playbooks : `changed=0`.
## 2026-08-08 — « Mot de passe oublié » : Keycloak sait enfin envoyer
Le realm affichait une politique d'accès complète et **aucun moyen d'écrire à qui que ce
@ -114,13 +163,13 @@ pu s'authentifier pour poser la nouvelle valeur.
```
C'est une **procédure**, pas un redéploiement — et la même contrainte vaut pour
`vault_openldap_admin`, qui reste à faire. Consigné au runbook (§6.8), avec le rappel qu'une
`vault_openldap_admin`, qui reste à faire. Consigné au runbook (§6.9), avec le rappel qu'une
vérification n'est pas un message de succès.
### La rotation des comptes de secours devient possible — elle ne l'était pas
Le runbook §6.7 promettait de régénérer les comptes de secours ; **le code ne savait pas le
Le runbook §6.8 promettait de régénérer les comptes de secours ; **le code ne savait pas le
faire**. Les trois rôles ne posaient le mot de passe qu'à la *création* :
```

View file

@ -218,7 +218,39 @@ ldapsearch -x -H ldapi:/// -D "cn=admin,$BASE" -W -b "uid=<uid>,ou=people,$BASE"
# Identity providers → LDAP → Sync all users
```
### 6.7 Ce que tu dois changer en priorité
### 6.7 La politique de mot de passe : une déclaration, deux exécutants
Les règles sont déclarées **une seule fois**, dans `resoudre_politique_mdp`, en termes
neutres. Le rôle les traduit dans les deux dialectes qui doivent les appliquer :
`pwdPolicy` pour OpenLDAP, `passwordPolicy` pour le realm. Changer la longueur minimale
se fait à un seul endroit.
Il a fallu en arriver là parce que les deux se renvoyaient la balle. LDAP portait bien les
règles — mais il ne sait que **refuser**, sans jamais dire pourquoi à l'écran. Keycloak,
lui, ne validait rien, et écrivait `userPassword` directement : l'overlay `ppolicy` ne
voyait même pas passer le changement. Mesuré le 2026-08-08 avec un compte sonde, le même
`abcd` était refusé par l'opération étendue LDAP et **accepté** par Keycloak, puis actif
pour l'authentification.
Deux réglages de la fédération tiennent la correction, et il faut les deux :
| Réglage | Ce qu'il répare |
|---|---|
| `usePasswordModifyExtendedOp` | slapd voit le changement passer, donc `ppolicy` s'applique |
| `validatePasswordPolicy` | Keycloak valide la politique du realm **avant** d'écrire |
Le second est le contrôle porteur : Keycloak se lie en rootDN, et slapd n'applique pas ses
contrôles de qualité au rootDN. L'utilisateur voit désormais *« Invalid password: minimum
length 12 »* au lieu d'un refus muet.
**`pwdMustChange` reste délibérément à `FALSE`.** Il signifie « quand un *administrateur*
pose un mot de passe, l'utilisateur devra le changer ». Comme Keycloak se lie en rootDN,
tout changement qui passe par lui *est* un changement administrateur : l'utilisateur
choisissait un nouveau mot de passe, `ppolicy` reposait aussitôt `pwdReset`, et l'écran
redemandait un changement — sans fin. Le changement forcé à la première connexion est
obtenu par l'action requise `UPDATE_PASSWORD` de Keycloak, qui, elle, se consomme.
### 6.8 Ce que tu dois changer en priorité
1. **Le mot de passe d'amorçage** — imposé dès la première connexion, tu n'as pas le
choix (§3).
@ -226,7 +258,7 @@ ldapsearch -x -H ldapi:/// -D "cn=admin,$BASE" -W -b "uid=<uid>,ou=people,$BASE"
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.
### 6.8 Faire tourner un secret : l'ordre n'est pas indifférent
### 6.9 Faire tourner un secret : l'ordre n'est pas indifférent
**Les comptes de secours des services** (`vault_grafana_admin`, `vault_forgejo_admin`,
`vault_nextcloud_admin`) se régénèrent en voûte, puis un redéploiement les applique. Les rôles

View file

@ -130,6 +130,10 @@
uid: "{{ amorcage_acces_uid }}"
cn: "{{ amorcage_acces_nom }}"
sn: "{{ amorcage_acces_nom.split(' ') | last }}"
# `cn` porte le nom complet ; le prenom a son propre attribut, et c'est lui
# que Keycloak doit lire — sinon il affiche le nom entier suivi du nom de
# famille. Derive comme `sn`, par le meme decoupage.
givenName: "{{ (amorcage_acces_nom.split(' ')[:-1] | join(' ')) | default(amorcage_acces_nom, true) }}"
userPassword: "{{ amorcage_acces_hache.stdout | trim }}"
pwdReset: "{{ 'TRUE' if amorcage_acces_reset_possible | bool else omit }}"
mail: "{{ amorcage_acces_courriel | default(omit, true) }}"

View file

@ -0,0 +1,34 @@
# resoudre_politique_mdp
Rôle **utilitaire** (non déployable) : porte *la* déclaration de la politique de mot de
passe et la traduit dans les deux dialectes qui doivent l'appliquer.
| Dialecte | Consommateur | Sortie |
|---|---|---|
| `pwdPolicy` (ppolicy) | `serveur_openldap` | `resoudre_politique_mdp_ldap` |
| `passwordPolicy` du realm | `serveur_keycloak` | `resoudre_politique_mdp_keycloak` |
| anti-force-brute du realm | `serveur_keycloak` | `resoudre_politique_mdp_brute` |
## Pourquoi
Les règles n'existaient que côté LDAP. LDAP ne sait que **refuser** — il ne dit jamais à
l'écran *pourquoi*. Keycloak, lui, ne validait rien : `passwordPolicy` était vide. Chacun
déléguait la vérification à l'autre.
Mesuré le 2026-08-08 avec un compte sonde, le même mot de passe `abcd` :
```
opération étendue LDAP → Constraint violation (19) — "Password fails quality checking policy"
Keycloak reset-password → 204, puis ldapwhoami avec "abcd" : accepté
```
Deux causes empilées : Keycloak écrivait `userPassword` **directement** en tant que rootDN,
donc l'overlay `ppolicy` n'interceptait rien ; et le realm n'avait aucune politique propre.
La correction tient aux deux bouts — `usePasswordModifyExtendedOp` côté fédération, et la
politique projetée ici.
## Ce que ce rôle ne fait pas
Il ne compare pas les deux dialectes après coup. Ils ne sont pas équivalents : `pwdCheckQuality`
d'OpenLDAP délègue à un module externe, `notUsername`/`notEmail` de Keycloak sont des règles
précises. La garantie porte sur la **source**, pas sur une équivalence terme à terme.

View file

@ -0,0 +1,34 @@
---
# LA declaration de la politique de mot de passe — une seule, en termes neutres.
# Les deux dialectes (ppolicy OpenLDAP, passwordPolicy Keycloak) en sont DERIVES.
#
# Pourquoi un role a part : les regles vivaient dans `serveur_openldap_ppolicy` et
# nulle part ailleurs. LDAP ne sait que refuser, sans jamais dire pourquoi a l'ecran ;
# Keycloak, lui, ne validait rien du tout (`passwordPolicy` vide). Chacun des deux
# deleguait la verification a l'autre, et personne ne la faisait : mesure du
# 2026-08-08, `abcd` refuse par l'operation etendue LDAP et accepte par Keycloak,
# puis actif pour l'authentification.
resoudre_politique_mdp_longueur_min: 12
# Interdit de reprendre un mot de passe recent (0 = pas d'historique).
resoudre_politique_mdp_historique: 3
# Refuse un mot de passe qui contient l'identifiant ou l'adresse.
resoudre_politique_mdp_interdire_identifiant: true
# Verrouillage apres echecs repetes. Cote LDAP c'est `ppolicy`, cote Keycloak c'est
# la protection anti-force-brute du realm — deux mecanismes distincts, memes chiffres.
resoudre_politique_mdp_verrouillage_actif: true
resoudre_politique_mdp_verrouillage_echecs: 5
resoudre_politique_mdp_verrouillage_duree: 900 # secondes
resoudre_politique_mdp_verrouillage_fenetre: 900 # secondes
# `pwdMustChange` — DELIBEREMENT a false, et ce n'est pas un oubli.
#
# Il signifie « quand un ADMINISTRATEUR pose un mot de passe, l'utilisateur devra le
# changer ». Or Keycloak se lie en rootDN : tout changement passe par lui EST un
# changement administrateur. L'utilisateur choisissait donc un nouveau mot de passe,
# ppolicy reposait aussitot `pwdReset`, et l'ecran redemandait un changement — sans
# fin. Vecu le 2026-08-07. Le mecanisme est incompatible par construction avec un IdP
# qui relaie le changement ; le changement force a la premiere connexion est obtenu
# par l'action requise UPDATE_PASSWORD de Keycloak, qui, elle, se consomme.
resoudre_politique_mdp_changement_impose: false

View file

@ -0,0 +1,48 @@
---
# Role utilitaire (pas un groupe deployable) : traduit LA declaration unique dans les
# deux dialectes. Voir defaults/main.yml.
#
# Sorties (facts) :
# resoudre_politique_mdp_ldap — attributs pwd* de l'entree pwdPolicy
# resoudre_politique_mdp_keycloak — chaine `passwordPolicy` du realm
# resoudre_politique_mdp_brute — protection anti-force-brute du realm
- name: Exiger une politique coherente
ansible.builtin.assert:
that:
- resoudre_politique_mdp_longueur_min | int >= 8
- resoudre_politique_mdp_historique | int >= 0
fail_msg: >-
Politique de mot de passe incoherente : une longueur minimale sous 8 caracteres
ne protege rien, et un historique negatif n'a pas de sens.
- name: Dériver la politique dans les deux dialectes
ansible.builtin.set_fact:
resoudre_politique_mdp_ldap: >-
{{ {
'pwdAttribute': 'userPassword',
'pwdCheckQuality': '1',
'pwdMinLength': resoudre_politique_mdp_longueur_min | string,
'pwdInHistory': resoudre_politique_mdp_historique | string,
'pwdMustChange': 'TRUE' if resoudre_politique_mdp_changement_impose | bool else 'FALSE',
'pwdAllowUserChange': 'TRUE',
'pwdLockout': 'TRUE' if resoudre_politique_mdp_verrouillage_actif | bool else 'FALSE',
'pwdMaxFailure': resoudre_politique_mdp_verrouillage_echecs | string,
'pwdLockoutDuration': resoudre_politique_mdp_verrouillage_duree | string,
'pwdFailureCountInterval': resoudre_politique_mdp_verrouillage_fenetre | string,
} }}
resoudre_politique_mdp_keycloak: >-
{{ (
['length(' ~ resoudre_politique_mdp_longueur_min | int ~ ')']
+ (['passwordHistory(' ~ resoudre_politique_mdp_historique | int ~ ')']
if resoudre_politique_mdp_historique | int > 0 else [])
+ (['notUsername', 'notEmail'] if resoudre_politique_mdp_interdire_identifiant | bool else [])
) | join(' and ') }}
resoudre_politique_mdp_brute: >-
{{ {
'bruteForceProtected': resoudre_politique_mdp_verrouillage_actif | bool,
'failureFactor': resoudre_politique_mdp_verrouillage_echecs | int,
'waitIncrementSeconds': resoudre_politique_mdp_verrouillage_duree | int,
'maxFailureWaitSeconds': resoudre_politique_mdp_verrouillage_duree | int,
'maxDeltaTimeSeconds': resoudre_politique_mdp_verrouillage_fenetre | int,
} }}

View file

@ -142,3 +142,37 @@ serveur_keycloak_smtp_hote: ""
# « Mot de passe oublie » sur l'ecran de connexion. Exige le SMTP ci-dessus et une
# adresse `mail` sur le compte — sans elle, Keycloak n'a nulle part ou ecrire.
serveur_keycloak_reset_mot_de_passe: true
# --- Durcissement de la federation LDAP ---------------------------------------
# Forme de l'API (chaque valeur est une LISTE de chaines) : on ecrit le dialecte
# reel plutot que de le fabriquer en Jinja.
#
# `usePasswordModifyExtendedOp` — sans lui, Keycloak ecrit l'attribut `userPassword`
# a la main et l'overlay `ppolicy` de slapd ne voit rien passer.
# `validatePasswordPolicy` — fait valider la politique DU REALM avant l'ecriture.
# C'est le controle porteur : Keycloak se lie en rootDN, et slapd n'applique pas
# ses controles de qualite au rootDN. Les deux cles ensemble, pas l'une ou l'autre.
# `syncRegistrations` — sans lui, un compte cree dans Keycloak n'atteint JAMAIS
# l'annuaire (mesure du 2026-08-08 : `POST users` -> 201, rien dans `ou=people`).
# Il aurait un acces web, aucune boite aux lettres, et serait invisible du modele
# de groupes : Postfix et Dovecot lisent LDAP, pas Keycloak.
# `trustEmail` — l'adresse vient de l'annuaire, qui est la source de verite ; la
# faire re-verifier par courriel demanderait a l'utilisateur de confirmer ce que
# l'exploitant a deja etabli.
# Les delais — un annuaire qui PEND, plutot qu'il refuse, bloque sinon les fils de
# Keycloak sans limite.
serveur_keycloak_ldap_durcissement:
usePasswordModifyExtendedOp: ["true"]
validatePasswordPolicy: ["true"]
syncRegistrations: ["true"]
trustEmail: ["true"]
connectionTimeout: ["5000"]
readTimeout: ["10000"]
# Mappeurs d'attributs a reconcilier : nom du mappeur -> attribut LDAP. Voir
# tasks/attributs-ldap.yml pour ce que le defaut de Keycloak faisait de travers.
serveur_keycloak_ldap_attributs:
first name: "givenName"
last name: "sn"
email: "mail"
username: "uid"

View file

@ -0,0 +1,39 @@
---
# Reconcilie les mappeurs d'attributs de la federation. Keycloak en cree un jeu par
# defaut a la creation du fournisseur, et pour un annuaire « other » il fait pointer
# le prenom sur `cn` — qui, dans `inetOrgPerson`, porte le nom COMPLET. Resultat
# affiche au 2026-08-08 : « Administrateur systeme systeme », le nom entier suivi du
# nom de famille. Le prenom a son attribut, c'est `givenName`.
- name: Lire les mappeurs de la fédération
ansible.builtin.uri:
url: >-
http://localhost:8080/admin/realms/{{ serveur_keycloak_realm }}/components?parent={{
serveur_keycloak_pol_fed.json[0].id }}&type=org.keycloak.storage.ldap.mappers.LDAPStorageMapper
headers:
Authorization: "Bearer {{ serveur_keycloak_pol_jeton.json.access_token }}"
register: serveur_keycloak_mappeurs
no_log: true
- name: Corriger l'attribut LDAP des mappeurs qui divergent
ansible.builtin.uri:
url: >-
http://localhost:8080/admin/realms/{{ serveur_keycloak_realm
}}/components/{{ item.id }}
method: PUT
status_code: [204]
headers:
Authorization: "Bearer {{ serveur_keycloak_pol_jeton.json.access_token }}"
body_format: json
body: >-
{{ item | combine({'config': item.config | combine({
'ldap.attribute': [serveur_keycloak_ldap_attributs[item.name]]
})}) }}
loop: "{{ serveur_keycloak_mappeurs.json }}"
loop_control:
label: "{{ item.name }}"
no_log: true
changed_when: true
when:
- item.name in serveur_keycloak_ldap_attributs
- (item.config['ldap.attribute'] | default(['']) | first) != serveur_keycloak_ldap_attributs[item.name]

View file

@ -163,6 +163,10 @@
ansible.builtin.include_tasks: federation-ldap.yml
when: serveur_keycloak_ldap_federation | bool
- name: Appliquer la politique de mot de passe et durcir la fédération
ansible.builtin.include_tasks: politique-mdp.yml
when: not ansible_check_mode
- name: Configurer le courriel du realm (« mot de passe oublié »)
ansible.builtin.include_tasks: courriel-realm.yml
when: serveur_keycloak_smtp_actif | bool

View file

@ -0,0 +1,103 @@
---
# Projette LA declaration unique (`resoudre_politique_mdp`) dans le realm, et durcit
# la federation pour que l'annuaire soit consulte au lieu d'etre contourne.
#
# Sans cela, les regles n'existaient QUE cote LDAP — et LDAP ne sait que refuser,
# sans jamais dire pourquoi a l'ecran. Pire : Keycloak ecrivait `userPassword`
# directement, donc l'overlay `ppolicy` n'interceptait meme pas. Mesure du
# 2026-08-08 : `abcd` refuse par l'operation etendue LDAP, accepte par Keycloak,
# puis actif pour l'authentification.
#
# La conformite se teste partout ici de la meme facon : fusionner l'attendu dans le
# reel ne change rien => c'est deja conforme. Ca evite d'enumerer les champs deux fois.
- name: Résoudre la politique de mot de passe (déclaration unique)
ansible.builtin.include_role:
name: resoudre_politique_mdp
- name: Obtenir un jeton d'administration
ansible.builtin.uri:
url: "http://localhost:8080/realms/master/protocol/openid-connect/token"
method: POST
body_format: form-urlencoded
body:
grant_type: password
client_id: admin-cli
username: "{{ serveur_keycloak_admin_user }}"
password: "{{ serveur_keycloak_admin_password }}"
register: serveur_keycloak_pol_jeton
no_log: true
- name: Lire le realm
ansible.builtin.uri:
url: "http://localhost:8080/admin/realms/{{ serveur_keycloak_realm }}"
headers:
Authorization: "Bearer {{ serveur_keycloak_pol_jeton.json.access_token }}"
register: serveur_keycloak_pol_realm
no_log: true
- name: Composer l'état attendu du realm
ansible.builtin.set_fact:
serveur_keycloak_realm_attendu: >-
{{ {'passwordPolicy': resoudre_politique_mdp_keycloak}
| combine(resoudre_politique_mdp_brute) }}
- name: Appliquer la politique de mot de passe au realm
ansible.builtin.uri:
url: "http://localhost:8080/admin/realms/{{ serveur_keycloak_realm }}"
method: PUT
status_code: [204]
headers:
Authorization: "Bearer {{ serveur_keycloak_pol_jeton.json.access_token }}"
body_format: json
body: "{{ serveur_keycloak_realm_attendu }}"
no_log: true
changed_when: true
when: >-
(serveur_keycloak_pol_realm.json | combine(serveur_keycloak_realm_attendu))
!= serveur_keycloak_pol_realm.json
# --- Durcissement de la federation --------------------------------------------
# Voir `serveur_keycloak_ldap_durcissement` (defaults) pour ce que chaque cle repare.
- name: Lire la fédération LDAP
ansible.builtin.uri:
url: >-
http://localhost:8080/admin/realms/{{ serveur_keycloak_realm
}}/components?type=org.keycloak.storage.UserStorageProvider
headers:
Authorization: "Bearer {{ serveur_keycloak_pol_jeton.json.access_token }}"
register: serveur_keycloak_pol_fed
no_log: true
- name: Exiger une fédération LDAP en place
ansible.builtin.assert:
that:
- (serveur_keycloak_pol_fed.json | length) == 1
fail_msg: >-
{{ serveur_keycloak_pol_fed.json | length }} fournisseur(s) de stockage
d'utilisateurs dans le realme « {{ serveur_keycloak_realm }} » — il en faut
exactement un. Durcir le mauvais serait pire que ne rien durcir.
- name: Durcir la fédération LDAP
ansible.builtin.uri:
url: >-
http://localhost:8080/admin/realms/{{ serveur_keycloak_realm
}}/components/{{ serveur_keycloak_pol_fed.json[0].id }}
method: PUT
status_code: [204]
headers:
Authorization: "Bearer {{ serveur_keycloak_pol_jeton.json.access_token }}"
body_format: json
body: >-
{{ serveur_keycloak_pol_fed.json[0] | combine({
'config': serveur_keycloak_pol_fed.json[0].config
| combine(serveur_keycloak_ldap_durcissement)
}) }}
no_log: true
changed_when: true
when: >-
(serveur_keycloak_pol_fed.json[0].config | combine(serveur_keycloak_ldap_durcissement))
!= serveur_keycloak_pol_fed.json[0].config
- name: Reconcilier les mappeurs d'attributs
ansible.builtin.include_tasks: attributs-ldap.yml

View file

@ -45,22 +45,12 @@ 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"
# Les REGLES elles-memes ne sont plus declarees ici : elles vivent dans
# `resoudre_politique_mdp`, qui les traduit AUSSI pour Keycloak. Tant qu'elles
# n'existaient que de ce cote, Keycloak n'en validait aucune et le chemin que
# prend un vrai utilisateur ne verifiait rien (mesure du 2026-08-08).
# Laisser vide ; ne surcharger que pour ajouter un attribut pwd* hors politique.
serveur_openldap_ppolicy_extra: {}
# 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

View file

@ -101,6 +101,11 @@
- 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 }}"
@ -138,14 +143,60 @@
- person
- top
attributes: >-
{{ {'cn': 'default', 'sn': 'default',
'pwdAttribute': 'userPassword'} | combine(serveur_openldap_ppolicy) }}
{{ {'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: