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
This commit is contained in:
parent
db4223904d
commit
b4e3520561
16 changed files with 459 additions and 12 deletions
|
|
@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
|
||||||
## Écrire, puis relire (D-68)
|
## Écrire, puis relire (D-68)
|
||||||
|
|
||||||
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
||||||
C'est aussi ce que font les 71 preuves de `make prouver` : elles lisent le dépôt, sans le
|
C'est aussi ce que font les 72 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||||
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
||||||
dépôt annonce.**
|
dépôt annonce.**
|
||||||
|
|
||||||
|
|
|
||||||
63
CHANGELOG.md
63
CHANGELOG.md
|
|
@ -1,5 +1,68 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-09-13 (2) — L'annuaire cesse de tout ouvrir avec la meme cle
|
||||||
|
|
||||||
|
### Ce que la mesure a montre
|
||||||
|
|
||||||
|
Les quatre services qui interrogent l'annuaire — Keycloak, Dovecot, Postfix, Icinga Web 2
|
||||||
|
— s'y 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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
### Ce qui a ete fait
|
||||||
|
|
||||||
|
- `ou=services` et **un compte par consommateur**, secret propre en voute.
|
||||||
|
- **Sept regles d'acces**, posees en entier (`state: exact`) plutot qu'inserees : l'ordre
|
||||||
|
est la regle, et inserer c'est parier sur ce que le paquet aura mis avant nous. Keycloak
|
||||||
|
ecrit (mots de passe, creations), les trois autres lisent, les comptes de service ne se
|
||||||
|
voient pas entre eux, et **la lecture anonyme est fermee**.
|
||||||
|
- `amorcage_acces` garde le compte d'administration, et c'est **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 : elle aurait annonce un
|
||||||
|
annuaire VIDE — la panne la plus grave — sur un annuaire parfaitement sain.
|
||||||
|
- **La rotation du compte d'administration est enfin possible.** Son mot de passe n'etait
|
||||||
|
pose qu'a l'installation, par debconf : le faire tourner en voute ne descendait nulle
|
||||||
|
part, et les deux divergeaient en silence. Un secret qu'on ne peut pas faire tourner est
|
||||||
|
un secret qu'on ne fera pas tourner.
|
||||||
|
|
||||||
|
### Quatre marches payees en chemin
|
||||||
|
|
||||||
|
1. **Un cinquieme appelant.** `amorcage_acces` inclut aussi le role partage. Oublie, il
|
||||||
|
echouait — et `no_log`, qui protege la valeur, masquait aussi la RAISON. La garde
|
||||||
|
refuse desormais dans une tache SANS `no_log` : elle nomme la cle absente, jamais son
|
||||||
|
contenu.
|
||||||
|
2. **Une garde qui surveillait deux champs sur trois.** La federation Keycloak ne
|
||||||
|
reecrivait son `bindDn` que si l'URL ou le mode changeaient. Nouveau secret, ancien
|
||||||
|
nom : `error code 49 - Invalid Credentials`, qui accuse les identifiants sans dire
|
||||||
|
lequel des deux a bouge.
|
||||||
|
3. **Une rotation placee trop tard.** Les taches qui se lient en administrateur passaient
|
||||||
|
AVANT la mise a jour du mot de passe. Un secret qui tourne doit descendre avant que
|
||||||
|
quoi que ce soit s'en serve.
|
||||||
|
4. **`ansible-vault` et son tube.** Sortie non bloquante = echec silencieux ; la voute
|
||||||
|
paraissait tournee et etait identique a l'octet. La garde qui compare l'empreinte du
|
||||||
|
FICHIER avant et apres l'a rattrape — c'est la lecon deja ecrite dans ce depot, et elle
|
||||||
|
vient de se repayer.
|
||||||
|
|
||||||
|
### La garde
|
||||||
|
|
||||||
|
`P72` exige que tout role incluant `resoudre_annuaire` NOMME son compte de service, et
|
||||||
|
qu'aucun sauf `amorcage_acces` ne nomme `admin`. Eprouvee dans les deux sens.
|
||||||
|
|
||||||
|
### Rotation
|
||||||
|
|
||||||
|
`vault_openldap_admin` et `vault_ldap_bind_postfix` ont ete renouveles : les deux avaient
|
||||||
|
transite en clair par une session d'exploitation. Verifie sur l'infrastructure — les
|
||||||
|
anciennes valeurs rendent `Invalid credentials (49)`, les nouvelles ouvrent, et les quatre
|
||||||
|
services repondent.
|
||||||
|
|
||||||
|
|
||||||
## 2026-09-13 — Le genome ne nait plus chez un locataire
|
## 2026-09-13 — Le genome ne nait plus chez un locataire
|
||||||
|
|
||||||
### Ce que la console montrait
|
### Ce que la console montrait
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||||
|
|
||||||
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
|
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
|
||||||
- **Verdict** : ✅ CONFORME (70 OK · 0 echec · 1 saute)
|
- **Verdict** : ✅ CONFORME (71 OK · 0 echec · 1 saute)
|
||||||
|
|
||||||
## Preuves
|
## Preuves
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
||||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
||||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 23 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 27 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
||||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 29 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 29 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
||||||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
|
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
|
||||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 5 instance(s) federee(s), aucun index en collision. |
|
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 5 instance(s) federee(s), aucun index en collision. |
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
||||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 14. |
|
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 14. |
|
||||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
||||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (71 preuves, 68 roles, 41 groupes). |
|
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (72 preuves, 68 roles, 41 groupes). |
|
||||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
||||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
||||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `904ece3` (publie le 2026-09-12). |
|
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `904ece3` (publie le 2026-09-12). |
|
||||||
|
|
@ -84,6 +84,7 @@
|
||||||
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
|
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
|
||||||
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
|
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
|
||||||
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
|
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
|
||||||
|
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
|
||||||
|
|
||||||
## Couverture des affirmations ✅ du registre
|
## Couverture des affirmations ✅ du registre
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
|
||||||
|
|
||||||
## Le trou qu'il comble
|
## Le trou qu'il comble
|
||||||
|
|
||||||
`scripts/prouver.py` porte 71 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
`scripts/prouver.py` porte 72 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||||
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
||||||
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
||||||
propriétaire, que rien n'est codé en dur.
|
propriétaire, que rien n'est codé en dur.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,15 @@
|
||||||
- name: Résoudre la connexion à l'annuaire
|
- name: Résoudre la connexion à l'annuaire
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: resoudre_annuaire
|
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)
|
- name: Exiger le secret d'amorçage et le mot de passe d'annuaire (Vault)
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,20 @@ resoudre_annuaire_app: "openldap"
|
||||||
resoudre_annuaire_hote: "id-ldap-01"
|
resoudre_annuaire_hote: "id-ldap-01"
|
||||||
resoudre_annuaire_port: 636
|
resoudre_annuaire_port: 636
|
||||||
resoudre_annuaire_encryption: "ldaps"
|
resoudre_annuaire_encryption: "ldaps"
|
||||||
# Compte de service (bind) — secret déréférencé depuis la voûte dans le rôle consommateur.
|
# COMPTE DE LIAISON — CHAQUE CONSOMMATEUR A LE SIEN (2026-09-13).
|
||||||
resoudre_annuaire_bind_cn: "admin"
|
#
|
||||||
resoudre_annuaire_secret: "vault_openldap_admin"
|
# Ce defaut valait `admin` : les quatre consommateurs se liaient donc avec le compte
|
||||||
|
# d'administration de l'annuaire. C'est le rootDN, et slapd lui fait CONTOURNER TOUTES
|
||||||
|
# LES ACL — y compris ses propres controles de qualite de mot de passe. Un seul secret,
|
||||||
|
# quatre services, tous les droits sur l'arbre, pour ce qui est trois fois sur quatre une
|
||||||
|
# simple lecture.
|
||||||
|
#
|
||||||
|
# Le rôle etait deja pret : `bind_cn` et `secret` sont des variables. Il ne manquait que
|
||||||
|
# les comptes, leurs droits, et que chaque consommateur nomme le sien.
|
||||||
|
#
|
||||||
|
# LE DEFAUT N'EST PLUS UN COMPTE QUI MARCHE. Un consommateur qui ne nomme pas le sien se
|
||||||
|
# lierait comme `cn=inconnu,ou=services,...`, qui n'existe pas : l'echec est immediat et
|
||||||
|
# lisible, au lieu de reussir avec tous les droits.
|
||||||
|
resoudre_annuaire_bind_ou: "services"
|
||||||
|
resoudre_annuaire_bind_cn: "inconnu"
|
||||||
|
resoudre_annuaire_secret: "vault_ldap_bind_inconnu"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
# resoudre_annuaire_uri — ldaps://id-ldap-01.<domaine>
|
# resoudre_annuaire_uri — ldaps://id-ldap-01.<domaine>
|
||||||
# resoudre_annuaire_base_dn — dc=...,dc=...
|
# resoudre_annuaire_base_dn — dc=...,dc=...
|
||||||
# resoudre_annuaire_users_dn — ou=people,dc=...
|
# resoudre_annuaire_users_dn — ou=people,dc=...
|
||||||
# resoudre_annuaire_bind_dn — cn=admin,dc=...
|
# resoudre_annuaire_bind_dn — cn=<service>,ou=services,dc=...
|
||||||
# resoudre_annuaire_bind_password — secret déréférencé depuis la voûte (no_log)
|
# resoudre_annuaire_bind_password — secret déréférencé depuis la voûte (no_log)
|
||||||
- name: Exiger un domaine interne non vide
|
- name: Exiger un domaine interne non vide
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
|
|
@ -36,9 +36,28 @@
|
||||||
resoudre_annuaire_port: "{{ resoudre_annuaire_port }}"
|
resoudre_annuaire_port: "{{ resoudre_annuaire_port }}"
|
||||||
resoudre_annuaire_base_dn: "dc={{ domaine_interne.split('.') | join(',dc=') }}"
|
resoudre_annuaire_base_dn: "dc={{ domaine_interne.split('.') | join(',dc=') }}"
|
||||||
resoudre_annuaire_users_dn: "ou=people,dc={{ domaine_interne.split('.') | join(',dc=') }}"
|
resoudre_annuaire_users_dn: "ou=people,dc={{ domaine_interne.split('.') | join(',dc=') }}"
|
||||||
resoudre_annuaire_bind_dn: "cn={{ resoudre_annuaire_bind_cn }},dc={{ domaine_interne.split('.') | join(',dc=') }}"
|
resoudre_annuaire_bind_dn: >-
|
||||||
|
cn={{ resoudre_annuaire_bind_cn }},{{ ('ou=' ~ resoudre_annuaire_bind_ou ~ ',')
|
||||||
|
if resoudre_annuaire_bind_ou | length > 0 else '' }}dc={{ domaine_interne.split('.') | join(',dc=') }}
|
||||||
|
|
||||||
|
# UNE GARDE CENSUREE NE GARDE PAS (mesure du 2026-09-13). La premiere version
|
||||||
|
# dereferencait directement : quand le secret manquait, `lookup('vars', ...)` levait — et
|
||||||
|
# `no_log: true`, qui protege legitimement la VALEUR, masquait aussi la RAISON. Le
|
||||||
|
# deploiement s'arretait sur « the output has been hidden », ce qui ne designe rien.
|
||||||
|
#
|
||||||
|
# On dereference donc avec un repli, puis on refuse dans une tache SANS `no_log` : elle
|
||||||
|
# ne nomme que la cle absente, jamais son contenu.
|
||||||
- name: Déréférencer le mot de passe de bind (Vault)
|
- name: Déréférencer le mot de passe de bind (Vault)
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
resoudre_annuaire_bind_password: "{{ lookup('vars', resoudre_annuaire_secret) }}"
|
resoudre_annuaire_bind_password: "{{ lookup('vars', resoudre_annuaire_secret, default='') }}"
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
|
- name: Exiger que le compte de liaison ait un secret
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- resoudre_annuaire_bind_password | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
Le compte de liaison « {{ resoudre_annuaire_bind_dn }} » n'a pas de secret :
|
||||||
|
la cle « {{ resoudre_annuaire_secret }} » est absente ou vide dans la voute.
|
||||||
|
Un role qui consomme l'annuaire doit nommer SON compte de service — voir
|
||||||
|
roles/resoudre_annuaire/defaults/main.yml.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
- name: Resoudre l'annuaire (role partage)
|
- name: Resoudre l'annuaire (role partage)
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: resoudre_annuaire
|
name: resoudre_annuaire
|
||||||
|
vars:
|
||||||
|
# SON compte, pas celui de l'administrateur de l'annuaire (2026-09-13).
|
||||||
|
resoudre_annuaire_bind_cn: "dovecot"
|
||||||
|
resoudre_annuaire_secret: "vault_ldap_bind_dovecot"
|
||||||
|
|
||||||
- name: Adopter la connexion annuaire pour Dovecot
|
- name: Adopter la connexion annuaire pour Dovecot
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@
|
||||||
- name: Résoudre l'annuaire (rôle partagé)
|
- name: Résoudre l'annuaire (rôle partagé)
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: resoudre_annuaire
|
name: resoudre_annuaire
|
||||||
|
vars:
|
||||||
|
# SON compte, pas celui de l'administrateur de l'annuaire (2026-09-13).
|
||||||
|
resoudre_annuaire_bind_cn: "icingaweb2"
|
||||||
|
resoudre_annuaire_secret: "vault_ldap_bind_icingaweb2"
|
||||||
|
|
||||||
- name: Adopter la connexion annuaire pour Icinga Web 2
|
- name: Adopter la connexion annuaire pour Icinga Web 2
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,21 @@
|
||||||
fi
|
fi
|
||||||
MODE_ACTUEL=$("$KC" get components/"$FEDID" -r {{ serveur_keycloak_realm }} 2>/dev/null \
|
MODE_ACTUEL=$("$KC" get components/"$FEDID" -r {{ serveur_keycloak_realm }} 2>/dev/null \
|
||||||
| sed -n 's/.*"editMode" : \[ "\([A-Z_]*\)".*/\1/p' | head -1 || true)
|
| sed -n 's/.*"editMode" : \[ "\([A-Z_]*\)".*/\1/p' | head -1 || true)
|
||||||
|
# LE DN DE LIAISON COMPTE AUTANT QUE L'URL (mesure du 2026-09-13).
|
||||||
|
#
|
||||||
|
# Cette condition ne regardait que l'URL et le mode. Le jour ou Keycloak a cesse
|
||||||
|
# de se lier avec le compte d'administration pour prendre le sien, le mot de passe
|
||||||
|
# a bien suivi — il se reconcilie sur son empreinte, juste au-dessus — mais le DN
|
||||||
|
# est reste l'ancien. Nouveau secret, ancien nom :
|
||||||
|
#
|
||||||
|
# javax.naming.AuthenticationException: [LDAP: error code 49 - Invalid Credentials]
|
||||||
|
#
|
||||||
|
# Le message accuse les identifiants sans dire LEQUEL des deux a bouge. Une garde
|
||||||
|
# qui surveille deux champs sur trois laisse passer le troisieme en silence.
|
||||||
|
BINDDN_ACTUEL=$("$KC" get components/"$FEDID" -r {{ serveur_keycloak_realm }} 2>/dev/null \
|
||||||
|
| sed -n 's/.*"bindDn" : \[ "\([^"]*\)".*/\1/p' | head -1 || true)
|
||||||
if [ "$URL_ACTUELLE" != "{{ serveur_keycloak_ldap_url }}" ] \
|
if [ "$URL_ACTUELLE" != "{{ serveur_keycloak_ldap_url }}" ] \
|
||||||
|
|| [ "$BINDDN_ACTUEL" != "{{ serveur_keycloak_ldap_bind_dn }}" ] \
|
||||||
|| [ "$MODE_ACTUEL" != "{{ serveur_keycloak_ldap_edit_mode }}" ]; then
|
|| [ "$MODE_ACTUEL" != "{{ serveur_keycloak_ldap_edit_mode }}" ]; then
|
||||||
"$KC" update components/"$FEDID" -r {{ serveur_keycloak_realm }} \
|
"$KC" update components/"$FEDID" -r {{ serveur_keycloak_realm }} \
|
||||||
-s 'config.connectionUrl=["{{ serveur_keycloak_ldap_url }}"]' \
|
-s 'config.connectionUrl=["{{ serveur_keycloak_ldap_url }}"]' \
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@
|
||||||
- name: Resoudre l'annuaire (role partage)
|
- name: Resoudre l'annuaire (role partage)
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: resoudre_annuaire
|
name: resoudre_annuaire
|
||||||
|
vars:
|
||||||
|
# SON compte, pas celui de l'administrateur de l'annuaire (2026-09-13).
|
||||||
|
resoudre_annuaire_bind_cn: "keycloak"
|
||||||
|
resoudre_annuaire_secret: "vault_ldap_bind_keycloak"
|
||||||
when: serveur_keycloak_ldap_federation | bool
|
when: serveur_keycloak_ldap_federation | bool
|
||||||
|
|
||||||
- name: Adopter la connexion annuaire pour Keycloak
|
- name: Adopter la connexion annuaire pour Keycloak
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,98 @@ serveur_openldap_admin_password: "{{ vault_openldap_admin | default('') }}" # r
|
||||||
serveur_openldap_ou:
|
serveur_openldap_ou:
|
||||||
- people
|
- people
|
||||||
- groups
|
- groups
|
||||||
|
# LES COMPTES DE SERVICE VIVENT A PART (2026-09-13). Melanges aux personnes, ils
|
||||||
|
# apparaitraient dans chaque recherche d'usager — et un annuaire ou « qui travaille
|
||||||
|
# ici » retourne des demons ne se lit plus.
|
||||||
|
- services
|
||||||
|
|
||||||
|
# --- QUI SE LIE A L'ANNUAIRE, ET AVEC QUELS DROITS (2026-09-13) ----------------------
|
||||||
|
#
|
||||||
|
# AVANT : les quatre consommateurs — Keycloak, Dovecot, Postfix, 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, y compris ses propres controles de qualite de mot de
|
||||||
|
# passe. Un seul secret, quatre services, tous les droits sur l'arbre entier — pour des
|
||||||
|
# operations qui sont, pour trois d'entre eux, de simples LECTURES.
|
||||||
|
#
|
||||||
|
# L'INDICE ETAIT DEJA DANS LE DEPOT. `serveur_keycloak_ldap_durcissement` porte
|
||||||
|
# `validatePasswordPolicy: true` avec ce motif : « Keycloak se lie en rootDN, et slapd
|
||||||
|
# n'applique pas ses controles de qualite au rootDN ». On avait compense la consequence
|
||||||
|
# sans traiter la cause. Avec un compte ordinaire, l'annuaire redevient juge de sa propre
|
||||||
|
# politique — le reglage reste, mais il n'est plus la seule barriere.
|
||||||
|
#
|
||||||
|
# `ecriture: true` n'est accorde qu'a ce qui DOIT ecrire. Keycloak est le seul : il porte
|
||||||
|
# les mots de passe a travers (edit_mode WRITABLE) et cree les comptes que le web ouvre
|
||||||
|
# (syncRegistrations). Les trois autres lisent.
|
||||||
|
serveur_openldap_comptes_service:
|
||||||
|
- cn: keycloak
|
||||||
|
secret: vault_ldap_bind_keycloak
|
||||||
|
ecriture: true
|
||||||
|
raison: "Federation OIDC : ecrit les mots de passe a travers et cree les comptes."
|
||||||
|
- cn: dovecot
|
||||||
|
secret: vault_ldap_bind_dovecot
|
||||||
|
ecriture: false
|
||||||
|
raison: "Qui a droit a sa boite aux lettres."
|
||||||
|
- cn: postfix
|
||||||
|
secret: vault_ldap_bind_postfix
|
||||||
|
ecriture: false
|
||||||
|
raison: "A qui livrer un courriel."
|
||||||
|
- cn: icingaweb2
|
||||||
|
secret: vault_ldap_bind_icingaweb2
|
||||||
|
ecriture: false
|
||||||
|
raison: "Qui peut voir la supervision."
|
||||||
|
|
||||||
|
serveur_openldap_services_dn: "ou=services,{{ serveur_openldap_base_dn }}"
|
||||||
|
|
||||||
|
# Empreinte du mot de passe d'administration APPLIQUE. `slappasswd` sale son condensat :
|
||||||
|
# comparer ce que slapd detient ne dit rien. On garde donc l'empreinte de la valeur
|
||||||
|
# voulue, exactement comme la federation Keycloak le fait pour sa liaison.
|
||||||
|
serveur_openldap_marqueur_admin: "/etc/ldap/.setops-admin-sha256"
|
||||||
|
serveur_openldap_admin_empreinte: "{{ serveur_openldap_admin_password | hash('sha256') }}"
|
||||||
|
|
||||||
|
# L'ANNUAIRE ETAIT LISIBLE PAR N'IMPORTE QUI (mesure du 2026-09-13).
|
||||||
|
#
|
||||||
|
# La regle livree par Debian est `to * by * read` : sur `ldap://`, sans s'authentifier,
|
||||||
|
# une machine du reseau enumerait tous les comptes et toutes les adresses. Des comptes de
|
||||||
|
# service a droits mesures n'auraient servi a rien tant que cette ligne restait — on
|
||||||
|
# aurait ferme la porte en laissant la fenetre.
|
||||||
|
#
|
||||||
|
# `false` restaure le comportement d'origine, pour un ecosysteme qui aurait une raison
|
||||||
|
# explicite de publier son annuaire. Ce n'est pas le cas par defaut.
|
||||||
|
serveur_openldap_lecture_anonyme: false
|
||||||
|
|
||||||
|
# L'identite du root LOCAL sur le socket unix. La sonde de cet annuaire l'utilise :
|
||||||
|
# elle compte les entrees sans reseau et sans secret. Fermer la lecture sans lui accorder
|
||||||
|
# ce droit rendrait la sonde aveugle — elle annoncerait un annuaire vide, c'est-a-dire la
|
||||||
|
# panne la plus grave, sur un annuaire parfaitement sain.
|
||||||
|
serveur_openldap_root_local_dn: "gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth"
|
||||||
|
|
||||||
|
# Raccourcis lisibles, pour que les regles ci-dessous se lisent comme des phrases.
|
||||||
|
serveur_openldap_ecrivains: >-
|
||||||
|
{% for c in serveur_openldap_comptes_service if c.ecriture | default(false) -%}
|
||||||
|
by dn.exact="cn={{ c.cn }},{{ serveur_openldap_services_dn }}" write {% endfor %}
|
||||||
|
serveur_openldap_lecteurs: >-
|
||||||
|
{% for c in serveur_openldap_comptes_service if not (c.ecriture | default(false)) -%}
|
||||||
|
by dn.exact="cn={{ c.cn }},{{ serveur_openldap_services_dn }}" read {% endfor %}
|
||||||
|
serveur_openldap_root_local: 'by dn.exact="{{ serveur_openldap_root_local_dn }}" read'
|
||||||
|
# Ce que voit une identite qui n'est designee par aucune regle precedente.
|
||||||
|
serveur_openldap_reste: "{{ 'by * read' if serveur_openldap_lecture_anonyme | bool else 'by users read by * none' }}"
|
||||||
|
|
||||||
|
# LES DROITS, DANS L'ORDRE OU slapd LES LIT. Voir tasks/main.yml pour ce que chaque
|
||||||
|
# numero protege — et pourquoi deplacer {0} ferme l'annuaire a tout le monde.
|
||||||
|
serveur_openldap_acces:
|
||||||
|
- '{0}to attrs=userPassword by self write by anonymous auth {{ serveur_openldap_ecrivains }} by * none'
|
||||||
|
- '{1}to attrs=shadowLastChange by self write by * read'
|
||||||
|
- '{2}to dn.subtree="{{ serveur_openldap_services_dn }}" {{ serveur_openldap_root_local }} by * none'
|
||||||
|
- >-
|
||||||
|
{3}to dn.subtree="ou=people,{{ serveur_openldap_base_dn }}"
|
||||||
|
{{ serveur_openldap_ecrivains }} {{ serveur_openldap_lecteurs }}
|
||||||
|
by self read {{ serveur_openldap_root_local }} by * none
|
||||||
|
- >-
|
||||||
|
{4}to dn.subtree="ou=groups,{{ serveur_openldap_base_dn }}"
|
||||||
|
{{ serveur_openldap_ecrivains }} {{ serveur_openldap_lecteurs }}
|
||||||
|
by users read {{ serveur_openldap_root_local }} by * none
|
||||||
|
- '{5}to dn.base="" by * read'
|
||||||
|
- '{6}to * {{ serveur_openldap_root_local }} {{ serveur_openldap_reste }}'
|
||||||
|
|
||||||
# --- TLS (LDAPS + STARTTLS) via le certificat d'hote step_ca ---
|
# --- TLS (LDAPS + STARTTLS) via le certificat d'hote step_ca ---
|
||||||
# Requiert l'integration client_pki sur le noeud (fournit le cert + le renouvellement).
|
# Requiert l'integration client_pki sur le noeud (fournit le cert + le renouvellement).
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,85 @@
|
||||||
enabled: true
|
enabled: true
|
||||||
state: started
|
state: started
|
||||||
|
|
||||||
|
# LA ROTATION PASSE AVANT TOUT CE QUI SE LIE EN ADMINISTRATEUR (2026-09-13).
|
||||||
|
#
|
||||||
|
# Placee plus bas, elle arrivait trop tard : « Creer les unites organisationnelles » se
|
||||||
|
# lie deja avec le mot de passe de la VOUTE, tandis que slapd detient encore l'ANCIEN.
|
||||||
|
# Le deploiement echouait sur la huitieme tache, et `no_log` — qui protege la valeur —
|
||||||
|
# masquait aussi la cause. Un secret qu'on fait tourner doit descendre AVANT que quoi que
|
||||||
|
# ce soit s'en serve.
|
||||||
|
#
|
||||||
|
# Ces taches-ci n'ont pas ce probleme : elles ecrivent par `ldapi:///` en s'identifiant
|
||||||
|
# par la socket unix (root local), sans mot de passe. C'est precisement ce qui leur
|
||||||
|
# permet de reparer la liaison qu'elles changent.
|
||||||
|
- name: Trouver la base mdb (pour y poser les droits)
|
||||||
|
community.general.ldap_search:
|
||||||
|
dn: "cn=config"
|
||||||
|
scope: onelevel
|
||||||
|
filter: "(olcDatabase={1}mdb)"
|
||||||
|
attrs:
|
||||||
|
- dn
|
||||||
|
server_uri: "ldapi:///"
|
||||||
|
register: serveur_openldap_base_droits
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Retenir le DN de la base pour les droits
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
serveur_openldap_droits_dn: "{{ (serveur_openldap_base_droits.results | first).dn }}"
|
||||||
|
when: (serveur_openldap_base_droits.results | default([]) | length) > 0
|
||||||
|
|
||||||
|
# --- ROTATION DU COMPTE D'ADMINISTRATION (2026-09-13) -------------------------------
|
||||||
|
#
|
||||||
|
# CE MOT DE PASSE N'ETAIT POSE QU'A L'INSTALLATION, par debconf. Le faire tourner en
|
||||||
|
# voute ne descendait donc nulle part : slapd gardait l'ancien, la voute annoncait le
|
||||||
|
# neuf, et les deux divergeaient EN SILENCE. Le premier a s'en apercevoir aurait ete
|
||||||
|
# `amorcage_acces`, au pire moment — quand on cree un acces.
|
||||||
|
#
|
||||||
|
# Un secret qu'on ne peut pas faire tourner est un secret qu'on ne fera pas tourner.
|
||||||
|
#
|
||||||
|
# `slappasswd` SALE le condensat : deux appels sur la meme valeur rendent deux chaines
|
||||||
|
# differentes, et comparer l'existant ne dit donc rien. On memorise l'empreinte de la
|
||||||
|
# valeur VOULUE, comme la federation Keycloak le fait pour sa liaison — meme probleme,
|
||||||
|
# meme remede, et c'est le signe qu'il s'agit d'un patron, pas d'un bricolage.
|
||||||
|
- name: Lire l'empreinte du mot de passe d'administration en place
|
||||||
|
ansible.builtin.slurp:
|
||||||
|
src: "{{ serveur_openldap_marqueur_admin }}"
|
||||||
|
register: serveur_openldap_marqueur_lu
|
||||||
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Hacher le mot de passe d'administration voulu
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv: ["slappasswd", "-s", "{{ serveur_openldap_admin_password }}"]
|
||||||
|
register: serveur_openldap_admin_hash
|
||||||
|
changed_when: false
|
||||||
|
no_log: true
|
||||||
|
when: serveur_openldap_admin_empreinte != (serveur_openldap_marqueur_lu.content | default('') | b64decode | trim)
|
||||||
|
|
||||||
|
- name: Aligner le mot de passe d'administration de l'annuaire sur la voute
|
||||||
|
community.general.ldap_attrs:
|
||||||
|
dn: "{{ serveur_openldap_droits_dn }}"
|
||||||
|
attributes:
|
||||||
|
olcRootPW: "{{ serveur_openldap_admin_hash.stdout | trim }}"
|
||||||
|
state: exact
|
||||||
|
server_uri: "ldapi:///"
|
||||||
|
no_log: true
|
||||||
|
when:
|
||||||
|
- serveur_openldap_droits_dn | default('') | length > 0
|
||||||
|
- serveur_openldap_admin_hash is not skipped
|
||||||
|
|
||||||
|
# LE MARQUEUR S'ECRIT APRES, JAMAIS AVANT. Ecrit d'abord, une pose ratee laisserait le
|
||||||
|
# depot convaincu d'avoir fait tourner un secret qui n'a pas bouge — et la prochaine
|
||||||
|
# execution ne retenterait rien.
|
||||||
|
- name: Retenir l'empreinte du mot de passe d'administration pose
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "{{ serveur_openldap_admin_empreinte }}\n"
|
||||||
|
dest: "{{ serveur_openldap_marqueur_admin }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0600"
|
||||||
|
when: serveur_openldap_admin_hash is not skipped
|
||||||
|
|
||||||
- name: Creer les unites organisationnelles de base
|
- name: Creer les unites organisationnelles de base
|
||||||
community.general.ldap_entry:
|
community.general.ldap_entry:
|
||||||
dn: "ou={{ item }},{{ serveur_openldap_base_dn }}"
|
dn: "ou={{ item }},{{ serveur_openldap_base_dn }}"
|
||||||
|
|
@ -66,6 +145,81 @@
|
||||||
label: "ou={{ item }}"
|
label: "ou={{ item }}"
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
|
# --- COMPTES DE SERVICE (2026-09-13) ------------------------------------------------
|
||||||
|
#
|
||||||
|
# Quatre services interrogeaient l'annuaire avec son compte d'administration. Voir
|
||||||
|
# `defaults/main.yml` pour le raisonnement ; ici on cree les comptes et on pose les droits.
|
||||||
|
- name: Exiger un secret pour chaque compte de service
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- lookup('vars', item.secret, default='') | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
Le compte de service « {{ item.cn }} » n'a pas de secret : {{ item.secret }} est
|
||||||
|
absent ou vide dans la voute. Un compte sans mot de passe ne se lie pas, et le
|
||||||
|
service qui en depend echouerait a la premiere requete.
|
||||||
|
loop: "{{ serveur_openldap_comptes_service }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.cn }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Creer les comptes de service de l'annuaire
|
||||||
|
community.general.ldap_entry:
|
||||||
|
dn: "cn={{ item.cn }},{{ serveur_openldap_services_dn }}"
|
||||||
|
objectClass:
|
||||||
|
- simpleSecurityObject
|
||||||
|
- organizationalRole
|
||||||
|
attributes:
|
||||||
|
description: "{{ item.raison }}"
|
||||||
|
userPassword: "{{ lookup('vars', item.secret) }}"
|
||||||
|
server_uri: "ldapi:///"
|
||||||
|
bind_dn: "{{ serveur_openldap_admin_dn }}"
|
||||||
|
bind_pw: "{{ serveur_openldap_admin_password }}"
|
||||||
|
loop: "{{ serveur_openldap_comptes_service }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.cn }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
# `ldap_entry` ne fait que CREER : un secret tourne en voute ne descendrait jamais dans
|
||||||
|
# l'annuaire, et la rotation paraitrait faite. `ldap_passwd` compare avant d'ecrire, donc
|
||||||
|
# il est idempotent ET il suit la voute. La lecon est deja celle de la politique de mots
|
||||||
|
# de passe, quinze lignes plus bas.
|
||||||
|
- name: Aligner le mot de passe de chaque compte de service sur la voute
|
||||||
|
community.general.ldap_passwd:
|
||||||
|
dn: "cn={{ item.cn }},{{ serveur_openldap_services_dn }}"
|
||||||
|
passwd: "{{ lookup('vars', item.secret) }}"
|
||||||
|
server_uri: "ldapi:///"
|
||||||
|
bind_dn: "{{ serveur_openldap_admin_dn }}"
|
||||||
|
bind_pw: "{{ serveur_openldap_admin_password }}"
|
||||||
|
loop: "{{ serveur_openldap_comptes_service }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.cn }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
# L'ORDRE DES REGLES EST LA REGLE. slapd applique la PREMIERE dont la cible correspond et
|
||||||
|
# s'arrete : une regle generale placee trop haut rend muettes toutes celles qui suivent.
|
||||||
|
# On pose donc la liste ENTIERE (`state: exact`) plutot que d'inserer — inserer, ici,
|
||||||
|
# c'est parier sur ce que le paquet Debian aura mis avant nous.
|
||||||
|
#
|
||||||
|
# {0} LES MOTS DE PASSE, D'ABORD. Cette regle porte sur un ATTRIBUT : elle vaut pour
|
||||||
|
# toutes les entrees, comptes de service compris — c'est elle qui leur permet de se
|
||||||
|
# lier (`by anonymous auth`). La retirer ou la deplacer plus bas ferme l'annuaire a
|
||||||
|
# tout le monde, y compris a l'exploitant.
|
||||||
|
# {2} LES COMPTES DE SERVICE NE SE VOIENT PAS ENTRE EUX. Compromettre Postfix ne doit pas
|
||||||
|
# livrer le compte de Keycloak. Le rootDN, lui, contourne les ACL : l'exploitant garde
|
||||||
|
# la main.
|
||||||
|
# {3} LES PERSONNES. Keycloak ecrit (mots de passe, creations) ; les trois autres lisent ;
|
||||||
|
# chacun se lit soi-meme.
|
||||||
|
# {5} LE RACINE DE L'ANNUAIRE reste lisible : sans lui, aucun client ne peut meme
|
||||||
|
# decouvrir les contextes servis, et les messages parlent alors de « base introuvable ».
|
||||||
|
- name: Poser les droits d'acces de l'annuaire
|
||||||
|
community.general.ldap_attrs:
|
||||||
|
dn: "{{ serveur_openldap_droits_dn }}"
|
||||||
|
attributes:
|
||||||
|
olcAccess: "{{ serveur_openldap_acces }}"
|
||||||
|
state: exact
|
||||||
|
server_uri: "ldapi:///"
|
||||||
|
when: serveur_openldap_droits_dn | default('') | length > 0
|
||||||
|
|
||||||
# --- Politique de mot de passe (overlay ppolicy) ------------------------------
|
# --- Politique de mot de passe (overlay ppolicy) ------------------------------
|
||||||
# Sans cet overlay, `pwdReset` n'existe pas dans le schema : `amorcage_acces` ne peut
|
# 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
|
# pas forcer le changement du jeton d'amorcage, et rien ne contraint la qualite ni ne
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,14 @@ systemctl is-active --quiet slapd || { echo "L'annuaire (slapd) n'est pas actif.
|
||||||
# ON COMPTE LES ENTREES, ON NE SE CONTENTE PAS D'UNE REPONSE. Un annuaire vide repond
|
# ON COMPTE LES ENTREES, ON NE SE CONTENTE PAS D'UNE REPONSE. Un annuaire vide repond
|
||||||
# `success` a tout — c'est exactement le mensonge des sauvegardes vides : vert, et sans
|
# `success` a tout — c'est exactement le mensonge des sauvegardes vides : vert, et sans
|
||||||
# contenu. Sans ce comptage, la sonde dirait « tout va bien » sur un annuaire efface.
|
# contenu. Sans ce comptage, la sonde dirait « tout va bien » sur un annuaire efface.
|
||||||
n=$(ldapsearch -x -H ldapi:/// -b "${BASE}" -s sub '(objectClass=*)' dn 2>/dev/null | grep -c '^dn:')
|
# `-Y EXTERNAL`, PAS `-x` (2026-09-13). La sonde lisait en ANONYME. Ca marchait tant que
|
||||||
|
# l'annuaire etait ouvert a tous — c'est-a-dire tant qu'il avait le defaut qu'on vient de
|
||||||
|
# corriger. Une fois la lecture anonyme fermee, un `-x` ne compte plus rien et la sonde
|
||||||
|
# annonce un annuaire VIDE : la panne la plus grave, sur un annuaire parfaitement sain.
|
||||||
|
#
|
||||||
|
# `EXTERNAL` sur le socket unix identifie le root local, a qui les droits accordent la
|
||||||
|
# lecture. Aucun secret, aucun reseau — ce que doit rester une sonde.
|
||||||
|
n=$(ldapsearch -Y EXTERNAL -H ldapi:/// -b "${BASE}" -s sub '(objectClass=*)' dn 2>/dev/null | grep -c '^dn:')
|
||||||
[[ "${n}" =~ ^[0-9]+$ ]] || { echo "L'annuaire ne repond pas sous ${BASE}."; exit 2; }
|
[[ "${n}" =~ ^[0-9]+$ ]] || { echo "L'annuaire ne repond pas sous ${BASE}."; exit 2; }
|
||||||
|
|
||||||
if (( n < MINIMUM )); then
|
if (( n < MINIMUM )); then
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
- name: Resoudre l'annuaire (role partage)
|
- name: Resoudre l'annuaire (role partage)
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: resoudre_annuaire
|
name: resoudre_annuaire
|
||||||
|
vars:
|
||||||
|
# SON compte, pas celui de l'administrateur de l'annuaire (2026-09-13).
|
||||||
|
resoudre_annuaire_bind_cn: "postfix"
|
||||||
|
resoudre_annuaire_secret: "vault_ldap_bind_postfix"
|
||||||
when: serveur_postfix_mode != "relais"
|
when: serveur_postfix_mode != "relais"
|
||||||
|
|
||||||
- name: Adopter la connexion annuaire pour Postfix
|
- name: Adopter la connexion annuaire pour Postfix
|
||||||
|
|
|
||||||
|
|
@ -3312,6 +3312,62 @@ def preuve_pool_du_site_nest_pas_celui_du_tenant() -> tuple[bool, str]:
|
||||||
return True, "`site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant."
|
return True, "`site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def preuve_annuaire_sans_compte_maitre() -> tuple[bool, str]:
|
||||||
|
"""Aucun service ne se lie-t-il a l'annuaire avec son compte d'administration ?
|
||||||
|
|
||||||
|
MESURE DU 2026-09-13 : les quatre consommateurs — Keycloak, Dovecot, Postfix,
|
||||||
|
Icinga Web 2 — s'y liaient tous avec `cn=admin`. C'est le rootDN : slapd lui fait
|
||||||
|
CONTOURNER TOUTES LES ACL, y compris ses propres controles de qualite de mot de passe.
|
||||||
|
Un seul secret, quatre services, tous les droits sur l'arbre — pour ce qui est, trois
|
||||||
|
fois sur quatre, une simple lecture.
|
||||||
|
|
||||||
|
LE DEPOT PORTAIT DEJA L'INDICE. `validatePasswordPolicy` existe parce que « slapd
|
||||||
|
n'applique pas ses controles de qualite au rootDN » : la consequence etait compensee,
|
||||||
|
la cause intacte.
|
||||||
|
|
||||||
|
CE QUI EST EXIGE ICI : tout role qui inclut `resoudre_annuaire` NOMME son compte de
|
||||||
|
service. Une seule exception, et elle est nommee — `amorcage_acces` ne consomme pas
|
||||||
|
l'annuaire, il le PROVISIONNE : il cree le compte d'amorcage depuis la socket locale
|
||||||
|
de la machine qui porte l'annuaire. C'est le geste de l'exploitant.
|
||||||
|
|
||||||
|
Sans cette garde, un role neuf heriterait du defaut du role partage et se lierait
|
||||||
|
silencieusement avec ce que ce defaut designe.
|
||||||
|
"""
|
||||||
|
import re as _re
|
||||||
|
roles_dir = RACINE / "roles"
|
||||||
|
incluent: dict[str, str] = {}
|
||||||
|
for role in sorted(p.name for p in roles_dir.iterdir() if p.is_dir()):
|
||||||
|
td = roles_dir / role / "tasks"
|
||||||
|
if not td.is_dir():
|
||||||
|
continue
|
||||||
|
for tf in sorted(td.glob("*.yml")):
|
||||||
|
texte = tf.read_text(encoding="utf-8")
|
||||||
|
if _re.search(r"name:\s*resoudre_annuaire\b", texte):
|
||||||
|
incluent[role] = texte
|
||||||
|
|
||||||
|
if not incluent:
|
||||||
|
return True, "Aucun role n'inclut `resoudre_annuaire` : rien a garder."
|
||||||
|
|
||||||
|
muets, maitres = [], []
|
||||||
|
for role, texte in sorted(incluent.items()):
|
||||||
|
cn = _re.search(r"resoudre_annuaire_bind_cn:\s*[\"']?([\w-]+)", texte)
|
||||||
|
if cn is None:
|
||||||
|
muets.append(role)
|
||||||
|
elif cn.group(1) == "admin" and role != "amorcage_acces":
|
||||||
|
maitres.append(role)
|
||||||
|
|
||||||
|
if maitres:
|
||||||
|
return False, ("Se lie(nt) a l'annuaire avec son compte d'administration — tous les "
|
||||||
|
"droits sur l'arbre pour une lecture : " + ", ".join(maitres))
|
||||||
|
if muets:
|
||||||
|
return False, ("N'a/ont pas nomme leur compte de service et heriteront du defaut du "
|
||||||
|
"role partage : " + ", ".join(muets))
|
||||||
|
return True, (f"{len(incluent)} role(s) consultent l'annuaire, chacun avec SON compte de "
|
||||||
|
"service ; seul `amorcage_acces` garde celui d'administration, et il "
|
||||||
|
"provisionne au lieu de consommer.")
|
||||||
|
|
||||||
|
|
||||||
def preuve_depot_binaires_complet() -> tuple[bool, str]:
|
def preuve_depot_binaires_complet() -> tuple[bool, str]:
|
||||||
"""Le depot du site tient-il TOUT ce que les roles vont chercher sur Internet ?
|
"""Le depot du site tient-il TOUT ce que les roles vont chercher sur Internet ?
|
||||||
|
|
||||||
|
|
@ -3580,6 +3636,8 @@ PREUVES: list[dict] = [
|
||||||
"refs": [], "func": preuve_depot_binaires_complet},
|
"refs": [], "func": preuve_depot_binaires_complet},
|
||||||
{"id": "P71", "titre": "Pool du site : le genome ne nait pas chez un tenant",
|
{"id": "P71", "titre": "Pool du site : le genome ne nait pas chez un tenant",
|
||||||
"refs": [], "func": preuve_pool_du_site_nest_pas_celui_du_tenant},
|
"refs": [], "func": preuve_pool_du_site_nest_pas_celui_du_tenant},
|
||||||
|
{"id": "P72", "titre": "Annuaire : aucun service ne se lie avec le compte du maitre",
|
||||||
|
"refs": [], "func": preuve_annuaire_sans_compte_maitre},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue