amorcage_acces : le role qui cree UN acces puis se retire
Cree uid=sysadmin et cn=sysadmin dans LDAP. Prouve sur idm-01 : le compte
s'authentifie, et le second passage ne touche a rien (changed=0, 7 taches
sautees) — idempotence par EXISTENCE, pas par conformite (D-67).
Il suit l'annuaire au lieu de se declarer au plan : il ecrit par `ldapi:///` et
doit tourner sur cet hote. Le declarer comme groupe obligerait chaque instance a
le poser sur le bon hote, et elles ne le nomment pas pareil (idm-01 ici,
id-ldap-01 chez Technolibre) — je l'ai d'abord pose sur infra-pki-01 par erreur.
Trois defauts trouves en le construisant :
1. `pwdReset` n'existe pas dans ce schema (overlay ppolicy non charge) :
l'entree entiere etait rejetee et `no_log` masquait la cause. J'avais suppose
un mecanisme sans verifier. Le role le DETECTE maintenant, et la doctrine ne
promet plus un changement force qui n'a pas lieu.
2. Le mot de passe aurait ete stocke EN CLAIR : `ldap_entry` ecrit userPassword
litteralement. Hache par `slappasswd -h {SSHA}` desormais.
3. `voute.py` ne scannait que roles/<groupe> : un role applique par un playbook
sans etre un groupe echappait au recensement, ce que D-20 interdit. Il suit
maintenant les listes `roles:` des playbooks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
be20ab43a9
commit
ba70b80197
7 changed files with 310 additions and 2 deletions
33
CHANGELOG.md
33
CHANGELOG.md
|
|
@ -2,6 +2,39 @@
|
|||
|
||||
## 2026-08-06 — le chemin nord-sud devient dérivable
|
||||
|
||||
### Le rôle d'amorçage : `amorcage_acces`
|
||||
|
||||
Crée **un** compte (`uid=sysadmin`) et **un** groupe (`cn=sysadmin`) dans LDAP, puis se
|
||||
retire. Prouvé sur `idm-01` : le compte s'authentifie (`ldapwhoami` retourne son DN), et le
|
||||
**second passage ne touche à rien** — `changed=0`, sept tâches sautées.
|
||||
|
||||
**Il suit l'annuaire, il ne se déclare pas au plan.** Le rôle écrit par `ldapi:///` — socket
|
||||
locale — donc il doit tourner sur l'hôte de l'annuaire. Le déclarer comme groupe obligerait
|
||||
chaque instance à le poser sur le bon hôte, et les instances ne nomment pas cet hôte pareil :
|
||||
`idm-01` chez Chezlepro, `id-ldap-01` chez Technolibre. Je m'en suis convaincu en le posant
|
||||
d'abord sur `infra-pki-01` par erreur. Il est donc appliqué par le playbook de
|
||||
`serveur_openldap`.
|
||||
|
||||
**Trois défauts trouvés en le construisant, tous par la machine :**
|
||||
|
||||
`pwdReset` **n'existe pas dans ce schéma** — il vient de l'overlay `ppolicy`, non chargé
|
||||
(seul `back_mdb` l'était). L'entrée entière était rejetée, et `no_log: true` masquait la
|
||||
cause. J'avais supposé un mécanisme sans vérifier qu'il existait. Le rôle le **détecte**
|
||||
désormais : overlay présent → changement forcé ; absent → il le dit en clair, et le
|
||||
changement devient une obligation d'exploitation. La doctrine a été corrigée pour ne plus
|
||||
promettre ce qui n'a pas lieu.
|
||||
|
||||
**Le mot de passe aurait été stocké en clair.** `ldap_entry` écrit `userPassword`
|
||||
littéralement ; le jeton d'amorçage aurait été lisible par quiconque lit l'annuaire. Il est
|
||||
maintenant haché par `slappasswd -h {SSHA}` sur la cible.
|
||||
|
||||
**Le recensement des secrets ne voyait pas ce rôle.** `voute.py` ne scannait que
|
||||
`roles/<groupe>` — un rôle appliqué par un playbook sans être lui-même un groupe échappait au
|
||||
recensement, ce que D-20 interdit. Il suit désormais les listes `roles:` des playbooks, ce qui
|
||||
vaut pour tout rôle futur dans ce cas. Le gabarit signale correctement
|
||||
`vault_sysadmin_amorcage`, généré ensuite dans les deux tenants.
|
||||
|
||||
|
||||
### Set-OPS amorce les accès, le sysadmin gouverne (D-65 → D-67)
|
||||
|
||||
L'authentification était résolue et gardée (P29) ; l'**autorisation** n'existait nulle part.
|
||||
|
|
|
|||
|
|
@ -103,7 +103,9 @@ ansible-vault view instance/inventories/principal/group_vars/all/vault.yml \
|
|||
| grep vault_sysadmin_amorcage
|
||||
```
|
||||
|
||||
Il ne servira qu'une fois : la première connexion impose son changement.
|
||||
**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.
|
||||
|
||||
> 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
|
||||
|
|
@ -137,7 +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, imposé.
|
||||
1. **Le mot de passe d'amorçage** — dès la première connexion. Rien ne l'impose encore :
|
||||
voir §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.
|
||||
|
|
|
|||
|
|
@ -13,3 +13,11 @@
|
|||
|
||||
roles:
|
||||
- serveur_openldap
|
||||
# L'AMORCAGE DES ACCES suit l'annuaire, il ne se declare pas au plan : il
|
||||
# ecrit par `ldapi:///` (socket locale) et doit donc tourner ICI. Le declarer
|
||||
# comme groupe obligerait chaque instance a le poser sur le bon hote — et les
|
||||
# instances ne nomment pas cet hote pareil (`idm-01` ici, `id-ldap-01` la).
|
||||
#
|
||||
# Rejoue a chaque deploiement, et c'est voulu : il est idempotent PAR
|
||||
# EXISTENCE (D-67). Compte present -> aucune action, quel que soit son etat.
|
||||
- amorcage_acces
|
||||
|
|
|
|||
44
roles/amorcage_acces/README.md
Normal file
44
roles/amorcage_acces/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# amorcage_acces — l'accès initial du sysadmin
|
||||
|
||||
Crée **un** compte (`uid=sysadmin`) et **un** groupe (`cn=sysadmin`) dans LDAP, une
|
||||
seule fois, pour que l'exploitant puisse prendre la main. Puis se retire.
|
||||
|
||||
## Ce qu'il ne fait pas
|
||||
|
||||
Ce n'est **pas** un gestionnaire de comptes. Après son passage, créer, suspendre ou
|
||||
réaffecter relève d'une personne — pas du dépôt (D-67, `docs/autorisation.md`).
|
||||
|
||||
## Idempotence par existence, pas par conformité
|
||||
|
||||
C'est ce qui distingue ce rôle de tous les autres du dépôt.
|
||||
|
||||
```
|
||||
compte absent → créé, secret généré en voûte, changement forcé
|
||||
compte présent → AUCUNE action, quel que soit son état
|
||||
```
|
||||
|
||||
Un compte présent est laissé strictement intact : ni mot de passe, ni groupes, ni
|
||||
attributs. Il a pu être renommé, promu, déplacé — c'est le droit de celui qui exploite.
|
||||
|
||||
Partout ailleurs dans Set-OPS, un écart entre le déclaré et le réel est un défaut à
|
||||
corriger. **Ici, l'écart est le travail du sysadmin.** Réconcilier effacerait le compte
|
||||
créé la veille pour un nouvel employé.
|
||||
|
||||
## Le secret d'amorçage
|
||||
|
||||
`vault_sysadmin_amorcage` est **généré** — le dépôt en est la source, puisque personne
|
||||
n'existe encore pour le choisir. Mais ce n'est jamais *le* mot de passe de quelqu'un :
|
||||
`pwdReset` impose son changement à la première ouverture.
|
||||
|
||||
Sans ce changement, l'auteur du déploiement connaîtrait le mot de passe du sysadmin.
|
||||
|
||||
## Où il s'exécute
|
||||
|
||||
Sur l'hôte qui porte `serveur_openldap` — il écrit par `ldapi:///`, socket locale. La
|
||||
connexion à l'annuaire est dérivée par `resoudre_annuaire`, jamais écrite.
|
||||
|
||||
## Après
|
||||
|
||||
`docs/autorisation.md` §6 est le runbook de reprise : où administrer quoi, comment se
|
||||
rouvrir si on se ferme dehors, et ce qu'il faut régénérer pour que la livraison soit un
|
||||
vrai transfert.
|
||||
36
roles/amorcage_acces/defaults/main.yml
Normal file
36
roles/amorcage_acces/defaults/main.yml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
# --- AMORCAGE DES ACCES (D-67) ------------------------------------------------
|
||||
# Ce role cree UN acces — celui du sysadmin — puis se retire. Il n'est PAS un
|
||||
# gestionnaire de comptes : apres son passage, c'est une personne qui gouverne.
|
||||
#
|
||||
# IDEMPOTENCE PAR EXISTENCE, PAS PAR CONFORMITE. Si le compte est la, on n'y
|
||||
# touche pas : ni mot de passe, ni groupes, ni attributs. Il a pu etre renomme,
|
||||
# promu, deplace — c'est le droit de celui qui exploite. Reconcilier effacerait
|
||||
# le compte cree la veille pour un nouvel employe.
|
||||
#
|
||||
# Voir docs/autorisation.md (§6 = runbook de reprise).
|
||||
|
||||
amorcage_acces_actif: true
|
||||
|
||||
# Le compte d'amorcage. `uid` est l'identifiant de connexion.
|
||||
amorcage_acces_uid: "sysadmin"
|
||||
amorcage_acces_nom: "Administrateur systeme"
|
||||
amorcage_acces_courriel: "" # facultatif ; vide = pas d'attribut mail
|
||||
|
||||
# Le groupe qui porte l'habilitation. Les services le reconnaissent par
|
||||
# `meta/acces.yml` — eux sont reconcilies, l'appartenance ne l'est pas.
|
||||
amorcage_acces_groupe: "sysadmin"
|
||||
|
||||
# Le secret d'amorcage. GENERE par le depot (il en est la source : personne
|
||||
# n'existe encore pour le choisir), depose en voute, et CHANGE A LA PREMIERE
|
||||
# OUVERTURE. Ce n'est donc jamais *le* mot de passe de quelqu'un : c'est un jeton
|
||||
# a usage unique. Sans le changement force, l'auteur du deploiement connaitrait
|
||||
# le mot de passe du sysadmin.
|
||||
amorcage_acces_secret: "{{ vault_sysadmin_amorcage | default('') }}"
|
||||
|
||||
# Force le changement a la premiere connexion (attribut LDAP `pwdReset`, lu par
|
||||
# Keycloak et par la politique de mot de passe de slapd).
|
||||
amorcage_acces_changement_force: true
|
||||
|
||||
# Connexion a l'annuaire — DERIVEE par le role `resoudre_annuaire`, jamais ecrite.
|
||||
amorcage_acces_admin_password: "{{ vault_openldap_admin | default('') }}"
|
||||
165
roles/amorcage_acces/tasks/main.yml
Normal file
165
roles/amorcage_acces/tasks/main.yml
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
---
|
||||
# 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
|
||||
|
||||
- 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 -----------------
|
||||
|
||||
# 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 }}"
|
||||
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
|
||||
|
|
@ -101,6 +101,25 @@ def secrets_exiges() -> dict[str, set[str]]:
|
|||
for nom in _refs_vault(f):
|
||||
noter(nom, f"role {groupe}")
|
||||
|
||||
# Roles appliques PAR UN PLAYBOOK sans etre eux-memes un groupe (ex.
|
||||
# `amorcage_acces`, porte par le playbook de l'annuaire). Sans ce passage,
|
||||
# leurs secrets echappaient au recensement — ce que D-20 interdit : « la
|
||||
# liste des secrets se recense, elle ne s'ecrit pas ». Constate le 2026-08-07.
|
||||
for pb in sorted((RACINE / "playbooks" / "groupes").glob("*.yml")):
|
||||
try:
|
||||
docs = yaml.safe_load(pb.read_text(encoding="utf-8")) or []
|
||||
except yaml.YAMLError:
|
||||
continue
|
||||
for jeu in docs if isinstance(docs, list) else [docs]:
|
||||
for r in (jeu or {}).get("roles") or []:
|
||||
nom_role = r if isinstance(r, str) else (r or {}).get("role", "")
|
||||
dossier = ROLES / str(nom_role)
|
||||
if not dossier.is_dir():
|
||||
continue
|
||||
for f in list(dossier.rglob("*.yml")) + list(dossier.rglob("*.j2")):
|
||||
for nom in _refs_vault(f):
|
||||
noter(nom, f"role {nom_role} (playbook {pb.name})")
|
||||
|
||||
d = _inventaire_dir()
|
||||
if d and (d / "group_vars").is_dir():
|
||||
for f in (d / "group_vars").rglob("*.yml"):
|
||||
|
|
|
|||
Loading…
Reference in a new issue