Set-OPS-Public/roles/amorcage_acces/tasks/main.yml
Daniel Allaire b4e3520561 annuaire : un compte de service par consommateur, et la porte se ferme
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
2026-09-13 13:36:38 -04:00

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