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
194 lines
7.8 KiB
YAML
194 lines
7.8 KiB
YAML
---
|
|
# Amorcage des acces — D-67. Voir defaults/main.yml pour la doctrine, et
|
|
# docs/autorisation.md §6 pour le runbook de reprise.
|
|
|
|
- name: Résoudre la connexion à l'annuaire
|
|
ansible.builtin.include_role:
|
|
name: resoudre_annuaire
|
|
vars:
|
|
# L'EXCEPTION, ET ELLE EST NOMMEE (2026-09-13). Les quatre services ont desormais
|
|
# chacun leur compte de service, a droits mesures. Ce role-ci n'est pas un service :
|
|
# il PROVISIONNE — il cree le compte d'amorcage et impose son changement de mot de
|
|
# passe. C'est le geste de l'exploitant, joue depuis la machine de l'annuaire par sa
|
|
# socket locale. Il garde donc le compte d'administration, qui est fait pour ca.
|
|
resoudre_annuaire_bind_ou: ""
|
|
resoudre_annuaire_bind_cn: "admin"
|
|
resoudre_annuaire_secret: "vault_openldap_admin"
|
|
|
|
- name: Exiger le secret d'amorçage et le mot de passe d'annuaire (Vault)
|
|
ansible.builtin.assert:
|
|
that:
|
|
- amorcage_acces_secret | length > 0
|
|
- amorcage_acces_admin_password | length > 0
|
|
fail_msg: >-
|
|
`vault_sysadmin_amorcage` et `vault_openldap_admin` sont requis (voûte).
|
|
Le premier est un jeton d'amorçage à usage unique — pas un mot de passe
|
|
permanent : voir docs/autorisation.md §6.
|
|
when: amorcage_acces_actif | bool
|
|
|
|
# --- LE CŒUR DU RÔLE : on REGARDE avant d'agir --------------------------------
|
|
# La condition n'est pas la conformité, c'est l'EXISTENCE. Un compte présent est
|
|
# laissé strictement intact, quel que soit son état — c'est ce qui distingue ce
|
|
# rôle de tous les autres du dépôt (D-67).
|
|
- name: Le compte d'amorçage existe-t-il déjà ?
|
|
community.general.ldap_search:
|
|
dn: "ou=people,{{ resoudre_annuaire_base_dn }}"
|
|
scope: onelevel
|
|
filter: "(uid={{ amorcage_acces_uid }})"
|
|
server_uri: "ldapi:///"
|
|
bind_dn: "{{ resoudre_annuaire_bind_dn }}"
|
|
bind_pw: "{{ amorcage_acces_admin_password }}"
|
|
register: amorcage_acces_existant
|
|
changed_when: false
|
|
no_log: true
|
|
when: amorcage_acces_actif | bool
|
|
|
|
- name: Retenir si l'amorçage a déjà eu lieu
|
|
ansible.builtin.set_fact:
|
|
amorcage_acces_deja_fait: >-
|
|
{{ (amorcage_acces_existant.results | default([]) | length) > 0 }}
|
|
when: amorcage_acces_actif | bool
|
|
|
|
- name: Annoncer que l'amorçage est déjà fait (aucune action)
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
Compte `{{ amorcage_acces_uid }}` déjà présent : Set-OPS n'y touche pas.
|
|
Les habilitations sont gouvernées par une personne (D-67) — voir
|
|
docs/autorisation.md §6.
|
|
when:
|
|
- amorcage_acces_actif | bool
|
|
- amorcage_acces_deja_fait | bool
|
|
|
|
# --- Tout ce qui suit ne s'exécute QUE si le compte est absent -----------------
|
|
|
|
# Verifie ICI et non plus haut : un ecosysteme deja amorce ne doit pas se mettre
|
|
# a echouer parce qu'on a durci la regle apres coup.
|
|
- name: Exiger une adresse de courriel pour le compte d'amorçage
|
|
ansible.builtin.assert:
|
|
that:
|
|
- amorcage_acces_courriel | length > 0
|
|
fail_msg: >-
|
|
`amorcage_acces_courriel` est vide. Ce compte est le seul recours quand
|
|
plus rien d'autre ne repond : sans adresse joignable, « mot de passe
|
|
oublie » ne mene nulle part et la reprise repasse par un tiers qui
|
|
manipule le mot de passe de quelqu'un d'autre. Declarer une adresse que
|
|
la personne lit DEJA, hors de l'ecosysteme qu'on amorce.
|
|
when:
|
|
- amorcage_acces_actif | bool
|
|
- not (amorcage_acces_deja_fait | bool)
|
|
|
|
# Le mot de passe est HACHÉ par slapd lui-même. `ldap_entry` écrirait la chaîne
|
|
# littéralement : le jeton d'amorçage se retrouverait en clair dans la base, lisible
|
|
# par quiconque peut lire l'annuaire.
|
|
- name: Hacher le jeton d'amorçage (slappasswd, SSHA)
|
|
ansible.builtin.command:
|
|
argv:
|
|
- slappasswd
|
|
- "-h"
|
|
- "{SSHA}"
|
|
- "-s"
|
|
- "{{ amorcage_acces_secret }}"
|
|
register: amorcage_acces_hache
|
|
changed_when: false
|
|
no_log: true
|
|
when:
|
|
- amorcage_acces_actif | bool
|
|
- not amorcage_acces_deja_fait | bool
|
|
|
|
# Le changement forcé s'appuie sur `pwdReset`, qui vient de l'overlay `ppolicy`.
|
|
# On le DÉTECTE au lieu de le supposer : sans l'overlay, l'attribut est inconnu du
|
|
# schéma et l'entrée entière est rejetée — panne obscure, et le compte n'existe pas
|
|
# du tout. Mesuré le 2026-08-07 : seul `back_mdb` était chargé.
|
|
- name: L'overlay ppolicy est-il chargé (attribut `pwdReset` connu) ?
|
|
ansible.builtin.command:
|
|
argv:
|
|
- ldapsearch
|
|
- "-Y"
|
|
- EXTERNAL
|
|
- "-H"
|
|
- "ldapi:///"
|
|
- "-b"
|
|
- "cn=schema,cn=config"
|
|
- "(olcAttributeTypes=*pwdReset*)"
|
|
- dn
|
|
register: amorcage_acces_ppolicy
|
|
changed_when: false
|
|
failed_when: false
|
|
when:
|
|
- amorcage_acces_actif | bool
|
|
- not amorcage_acces_deja_fait | bool
|
|
|
|
- name: Retenir si le changement forcé est applicable
|
|
ansible.builtin.set_fact:
|
|
amorcage_acces_reset_possible: >-
|
|
{{ amorcage_acces_changement_force | bool
|
|
and 'dn:' in (amorcage_acces_ppolicy.stdout | default('')) }}
|
|
when:
|
|
- amorcage_acces_actif | bool
|
|
- not amorcage_acces_deja_fait | bool
|
|
|
|
- name: Créer le compte d'amorçage du sysadmin
|
|
community.general.ldap_entry:
|
|
dn: "uid={{ amorcage_acces_uid }},ou=people,{{ resoudre_annuaire_base_dn }}"
|
|
objectClass:
|
|
- inetOrgPerson
|
|
- organizationalPerson
|
|
- person
|
|
- top
|
|
attributes:
|
|
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) }}"
|
|
server_uri: "ldapi:///"
|
|
bind_dn: "{{ resoudre_annuaire_bind_dn }}"
|
|
bind_pw: "{{ amorcage_acces_admin_password }}"
|
|
no_log: true
|
|
when:
|
|
- amorcage_acces_actif | bool
|
|
- not amorcage_acces_deja_fait | bool
|
|
|
|
# `groupOfNames` EXIGE au moins un membre : un groupe vide est refusé par le
|
|
# schéma — ce qui tombe bien, un groupe d'habilitation sans personne n'aurait
|
|
# aucun sens à l'amorçage.
|
|
- name: Créer le groupe d'habilitation avec le sysadmin pour membre
|
|
community.general.ldap_entry:
|
|
dn: "cn={{ amorcage_acces_groupe }},ou=groups,{{ resoudre_annuaire_base_dn }}"
|
|
objectClass:
|
|
- groupOfNames
|
|
- top
|
|
attributes:
|
|
cn: "{{ amorcage_acces_groupe }}"
|
|
member: "uid={{ amorcage_acces_uid }},ou=people,{{ resoudre_annuaire_base_dn }}"
|
|
description: >-
|
|
Habilitation d'administration. Amorcé par Set-OPS, gouverné ensuite par
|
|
une personne — le dépôt ne réconcilie pas ses membres (D-67).
|
|
server_uri: "ldapi:///"
|
|
bind_dn: "{{ resoudre_annuaire_bind_dn }}"
|
|
bind_pw: "{{ amorcage_acces_admin_password }}"
|
|
no_log: true
|
|
when:
|
|
- amorcage_acces_actif | bool
|
|
- not amorcage_acces_deja_fait | bool
|
|
|
|
- name: Annoncer la reprise en main
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
{{ [
|
|
'Accès d amorçage créé : uid=' ~ amorcage_acces_uid ~ ', groupe=' ~ amorcage_acces_groupe ~ '.',
|
|
'Mot de passe en voûte sous `vault_sysadmin_amorcage`.',
|
|
(amorcage_acces_reset_possible | bool)
|
|
| ternary('Changement FORCÉ à la première ouverture (ppolicy actif).',
|
|
'ATTENTION : rien ne force le changement — l overlay ppolicy n est pas chargé. '
|
|
~ 'Le changer à la main dès la première connexion (docs/autorisation.md §6).'),
|
|
'À partir d ici, Set-OPS ne touche plus aux habilitations (D-67).'
|
|
] }}
|
|
when:
|
|
- amorcage_acces_actif | bool
|
|
- not amorcage_acces_deja_fait | bool
|