authentification : chaque rôle déclare sa position, gardé par P29

Une règle qu'aucune garde ne vérifie finit par ne plus être vraie — c'est ce qui
était arrivé aux 28 lignes d'intégration recopiées. Chaque rôle serveur_* porte
un meta/authentification.yml, confronté à son code par P29.

web-sso 5, socle-identite 2 (keycloak/openldap : ils SONT la chaîne d'identité),
ldap-direct 2, interne-sans-auth 2, sans-auth-humaine 12.

La preuve refuse l'oubli ET le mensonge. Éprouvée par sabotage sur sept cas :
déclaration supprimée, portée inventée, secours retiré, posture de formulaire
retirée, raison retirée, ldap-direct mensonger, réglage retiré des defaults.

Les deux derniers passaient dans la première version :

- le mensonge passait à cause d'un commentaire. Je cherchais le mot « ldap » dans
  le rôle, et serveur_grafana/defaults/main.yml contient « désactiver quelqu'un
  dans LDAP » : de la prose validait une déclaration fausse. La preuve exige
  maintenant un indice nommé — variable <rôle>_oidc / <rôle>_ldap, ou URI ldap://
- le réglage retiré passait parce que le gabarit citait encore la variable alors
  que plus rien ne lui donnait de valeur. La preuve lit defaults/main.yml en YAML
  et exige que la clé y soit définie, pas mentionnée.

Elle a aussi forcé une valeur : oauth2-proxy était déclaré « formulaire local
fermé » alors qu'il n'a aucun compte local. D'où formulaire_local: aucun, qui
distingue « il n'y en a jamais eu » de « il y en a un, il est fermé ».

Correction d'une note de la veille : Prometheus et Loki ne sont PAS exposés
publiquement (aucun expose au plan). Seuls six groupes le sont. Le risque est
intra-tenant, pas frontalier. Les deux lacunes sont comptées à chaque exécution,
pas masquées.

AFF-111, D-42. 29 preuves OK, ansible-lint (production) sur 375 fichiers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-03 17:05:43 -04:00
parent 6a62f0a4c7
commit 22ef279464
29 changed files with 404 additions and 11 deletions

View file

@ -1,5 +1,57 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 2026-08-03 (suite 16) — la directive d'authentification est gardée (P29)
Une règle qu'aucune garde ne vérifie finit par ne plus être vraie : c'est exactement ce
qui était arrivé aux 28 lignes d'intégration recopiées. Chaque rôle `serveur_*` porte
désormais un `meta/authentification.yml`, et **P29** le confronte à son code.
```
web-sso 5 grafana, forgejo, nextcloud, icingaweb2, oauth2-proxy
socle-identite 2 keycloak, openldap — ils SONT la chaîne d'identité
ldap-direct 2 dovecot, postfix
interne-sans-auth 2 prometheus, loki — lacunes nommées
sans-auth-humaine 12
```
### Elle refuse l'oubli **et** le mensonge
Éprouvée par sabotage, sept cas : déclaration supprimée, portée inventée, secours retiré,
posture de formulaire retirée, raison retirée, `ldap-direct` mensonger, réglage retiré des
`defaults`. Les sept sont attrapés.
Les deux derniers **passaient** dans ma première version.
**Le mensonge passait à cause d'un de mes propres commentaires.** Je cherchais le mot
« ldap » dans le rôle — or `serveur_grafana/defaults/main.yml` contient la phrase
« désactiver quelqu'un dans LDAP ». De la prose suffisait à valider une déclaration fausse.
La preuve exige maintenant un indice **nommé** : une variable du namespace du rôle
(`<rôle>_oidc`, `<rôle>_ldap`) ou une URI `ldap://`.
**Le réglage retiré passait** parce que le gabarit citait encore la variable alors que plus
rien ne lui donnait de valeur. La preuve lit désormais `defaults/main.yml` **en YAML** et
exige que la clé y soit *définie*, pas mentionnée.
### Une valeur que la preuve a forcé à inventer
Elle a d'abord échoué sur `oauth2-proxy` : je l'avais déclaré « formulaire local fermé »
alors qu'il n'a **aucun compte local** — c'est une passerelle. D'où `formulaire_local:
aucun`, qui distingue « il n'y en a jamais eu » de « il y en a un, il est fermé ». Écrire
« fermé » aurait laissé croire qu'une porte avait été close.
### Correction : Prometheus et Loki ne sont pas exposés
Contrairement à ce que la note de la veille affirmait, ils n'ont **aucun `expose` au plan**.
Seuls six groupes sont exposés : collabora, forgejo, grafana, keycloak, nextcloud,
oauth2-proxy. Le risque est intra-tenant, pas frontalier — plus petit qu'annoncé, réel
quand même.
Ces deux lacunes sont **comptées, pas masquées** : `interne-sans-auth` ne fait pas échouer
la preuve, mais figure dans chaque rapport. La refuser bloquerait le harnais sur une
décision déjà prise ; la taire la ferait oublier.
**AFF-111**, décision **D-42**. 29 preuves OK, 0 échec, 0 sautée.
## 2026-08-03 (suite 15) — la porte de secours cesse d'être annoncée ## 2026-08-03 (suite 15) — la porte de secours cesse d'être annoncée
Client OIDC **Nextcloud** ajouté aux deux tenants — quatre clients chacun désormais, sur Client OIDC **Nextcloud** ajouté aux deux tenants — quatre clients chacun désormais, sur

View file

@ -159,6 +159,7 @@ Légende du statut : ✅ prouvée · 🟡 partielle · ❌ fausse · ⚪ invéri
| AFF-108 | « Une intégration universelle est déclarée par le rôle, pas recopiée par serveur : aucun hôte n'y échappe et le plan ne la redit pas » (`docs/decisions-architecture.md` D-33) | structurelle | **P26** (`scripts/prouver.py`) | ✅ | L'exemption se dérive du **service rendu** (`sauf_role`), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. | | AFF-108 | « Une intégration universelle est déclarée par le rôle, pas recopiée par serveur : aucun hôte n'y échappe et le plan ne la redit pas » (`docs/decisions-architecture.md` D-33) | structurelle | **P26** (`scripts/prouver.py`) | ✅ | L'exemption se dérive du **service rendu** (`sauf_role`), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. |
| AFF-109 | « Ce qui appartient à l'hébergeur (cluster, fabric, frontière) vit dans son dépôt, jamais recopié chez un tenant » (`docs/decisions-architecture.md` D-35) | structurelle | **P27** (`scripts/prouver.py`) | ✅ | Le chemin se dérive du symlink `underlay.yml` : l'hébergeur n'est toujours pas déclaré (D-17). Sautée si aucun underlay n'est monté. | | AFF-109 | « Ce qui appartient à l'hébergeur (cluster, fabric, frontière) vit dans son dépôt, jamais recopié chez un tenant » (`docs/decisions-architecture.md` D-35) | structurelle | **P27** (`scripts/prouver.py`) | ✅ | Le chemin se dérive du symlink `underlay.yml` : l'hébergeur n'est toujours pas déclaré (D-17). Sautée si aucun underlay n'est monté. |
| AFF-110 | « Chaque tenant a son pool Proxmox, dérivé, sans collision de nom ni de VMID » (`docs/decisions-architecture.md` D-37) | structurelle | **P28** (`scripts/devis_proxmox_pools.py --verifier`) | ✅ | Le nom dérive du dossier d'instance et de l'`index`, déjà unique par P21 — aucun registre de plus. Le devis ne lit pas le cluster : il affirme la cohérence du **plan**, pas l'état appliqué. | | AFF-110 | « Chaque tenant a son pool Proxmox, dérivé, sans collision de nom ni de VMID » (`docs/decisions-architecture.md` D-37) | structurelle | **P28** (`scripts/devis_proxmox_pools.py --verifier`) | ✅ | Le nom dérive du dossier d'instance et de l'`index`, déjà unique par P21 — aucun registre de plus. Le devis ne lit pas le cluster : il affirme la cohérence du **plan**, pas l'état appliqué. |
| AFF-111 | « Chaque rôle `serveur_*` déclare sa position d'authentification, et la déclaration est cohérente avec son code » (`docs/authentification.md`, D-38 à D-41) | structurelle | **P29** (`scripts/prouver.py`) | ✅ | 23 rôles déclarés. La preuve refuse l'oubli **et** le mensonge : une déclaration `web-sso natif` sans variable `<rôle>_oidc`, ou `ldap-direct` sans variable `<rôle>_ldap`, échoue. Les indices sont **nommés** — une première version cherchait les mots « ldap »/« oidc » et le mot LDAP dans un commentaire suffisait à valider un mensonge. Les 2 portées `interne-sans-auth` (Prometheus, Loki) sont des lacunes **comptées, pas masquées**. |
| AFF-106 | « Le panneau couvre le schéma du plan : aucun champ n'oblige à éditer le YAML » (`docs/intrants-base-gui-conception.md`) | fonctionnelle | **P19** (`scripts/couverture_gui.py`) | 🟡 | Vrai des registres du plan. **Exceptions assumées** : `reseaux` et `hotes` de l'underlay (listes de tables) restent hors du panneau ; les ports physiques et le nœud de sortie EVPN se nomment à la main. | | AFF-106 | « Le panneau couvre le schéma du plan : aucun champ n'oblige à éditer le YAML » (`docs/intrants-base-gui-conception.md`) | fonctionnelle | **P19** (`scripts/couverture_gui.py`) | 🟡 | Vrai des registres du plan. **Exceptions assumées** : `reseaux` et `hotes` de l'underlay (listes de tables) restent hors du panneau ; les ports physiques et le nœud de sortie EVPN se nomment à la main. |
**Non enregistré volontairement.** Les devis eux-mêmes (`make devis-reseau`, **Non enregistré volontairement.** Les devis eux-mêmes (`make devis-reseau`,
@ -399,6 +400,7 @@ Chaque affirmation ✅ est soit **rejouée** par une preuve de `make prouver`
| AFF-108 | **P26** (intégrations universelles : aucun hôte laissé de côté) | | AFF-108 | **P26** (intégrations universelles : aucun hôte laissé de côté) |
| AFF-109 | **P27** (propriété des intrants : hébergeur et tenant séparés) | | AFF-109 | **P27** (propriété des intrants : hébergeur et tenant séparés) |
| AFF-110 | **P28** (pools Proxmox : un par tenant) | | AFF-110 | **P28** (pools Proxmox : un par tenant) |
| AFF-111 | **P29** (authentification : chaque rôle déclare sa position) |
**Structurelles/doctrinales, hors harnais récurrent** (vérifiées à l'audit) : AFF-005 **Structurelles/doctrinales, hors harnais récurrent** (vérifiées à l'audit) : AFF-005
(`make`=aide), AFF-014 (ciblage `groupe:&hotes_actifs`), AFF-024 (`instancier-appliquer`, (`make`=aide), AFF-014 (ciblage `groupe:&hotes_actifs`), AFF-024 (`instancier-appliquer`,

View file

@ -7,7 +7,7 @@
> [`docs/audit/affirmations.md`](affirmations.md). > [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml` - **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
- **Verdict** : ✅ CONFORME (27 OK · 0 echec · 1 saute) - **Verdict** : ✅ CONFORME (29 OK · 0 echec · 0 saute)
## Preuves ## Preuves
@ -28,9 +28,9 @@
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. | | P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. | | P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
| 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 | ✅ OK | 14 hotes, 29 groupes (inventaire dechiffre et parse). |
| 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 : 21 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 : 21 secret(s) exige(s), tous presents. Voute reelle : 24 cle(s), aucun manque. |
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 27 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 27 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 : 2 instance(s) federee(s), aucun index en collision. | | P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 2 instance(s) federee(s), aucun index en collision. |
@ -41,6 +41,7 @@
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 3 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). | | P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 3 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). |
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 7 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. | | P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 7 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. | | P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. |
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
## Couverture des affirmations ✅ du registre ## Couverture des affirmations ✅ du registre

View file

@ -85,12 +85,43 @@ Un accès de secours qu'on ne sait pas emprunter n'existe pas : les commandes ex
dans le tableau ci-dessus, et les mots de passe locaux vivent dans la voûte unique de dans le tableau ci-dessus, et les mots de passe locaux vivent dans la voûte unique de
l'instance. l'instance.
## 5. Ce qui reste à faire ## 5. La garde : chaque rôle déclare sa position
- **Prometheus et Loki** exposent une interface sans SSO. `oauth2-proxy` est déjà éprouvé Une directive qu'aucune garde ne vérifie finit par ne plus être vraie. Chaque rôle
devant Icinga Web 2 : le patron existe, il reste à l'appliquer. `serveur_*` porte donc un `meta/authentification.yml`, et **P29** le confronte à son code.
- **Aucune preuve ne garde cette directive.** C'est le même risque que les intégrations
universelles avant leur inversion : une règle qu'aucune garde ne vérifie finit par ne | Portée | Sens | Rôles |
plus être vraie. Le remède symétrique serait qu'un rôle **déclare sa position** |---|---|---|
(`web-sso` / `ldap-direct` / `sans-auth-humaine`, plus son accès de secours) et qu'une | `web-sso` | authentification humaine web → Keycloak (`mecanisme: natif` ou `oauth2-proxy`) | 5 |
preuve refuse une interface humaine sans SSO ni secours déclaré. | `socle-identite` | **est** la chaîne d'identité, ne peut pas se déléguer à elle-même | keycloak, openldap |
| `ldap-direct` | protocole non-OIDC lié à LDAP | dovecot, postfix |
| `interne-sans-auth` | interface joignable **dans** le tenant sans authentification, non exposée | prometheus, loki |
| `sans-auth-humaine` | aucun point d'authentification humaine | 12 |
La preuve refuse **l'oubli et le mensonge** : un rôle sans déclaration, une portée
inventée, un `web-sso` sans accès de secours ou sans posture de formulaire, un `web-sso
natif` sans réglage `<rôle>_connexion_locale` **défini dans `defaults`**, et une
déclaration que le code contredit.
> **Les indices doivent être nommés.** Une première version cherchait les mots « ldap » et
> « oidc » dans le rôle. Le mot *LDAP*, présent dans un commentaire de `serveur_grafana`,
> suffisait alors à valider une déclaration `ldap-direct` mensongère. La preuve exige
> désormais une **variable du namespace du rôle** (`<rôle>_oidc`, `<rôle>_ldap`) ou une URI
> `ldap://` — ce qu'une phrase en prose ne produit pas.
Une valeur mérite d'être expliquée : `formulaire_local: aucun`. Elle distingue un service
qui **n'a pas de compte local du tout**`oauth2-proxy` est une passerelle — de celui qui
en a un et l'a fermé. Sans elle, on écrirait « fermé », ce qui laisserait croire qu'une
porte a été close alors qu'il n'y en a jamais eu.
### Les lacunes sont comptées, pas masquées
`interne-sans-auth` ne fait pas échouer la preuve : c'est une lacune **assumée et nommée**.
Mais elle est comptée à chaque exécution, donc visible dans chaque rapport. La refuser
bloquerait le harnais sur une décision déjà prise ; la taire la ferait oublier.
Prometheus et Loki sont dans ce cas. À noter, contre une idée reçue de la veille : **ils ne
sont pas exposés publiquement** — aucun `expose` au plan. Seuls six groupes le sont
(collabora, forgejo, grafana, keycloak, nextcloud, oauth2-proxy). Le risque est donc
intra-tenant, pas frontalier — plus petit qu'annoncé, réel quand même. `oauth2-proxy` est
déjà éprouvé devant Icinga Web 2 : le patron existe, il reste à l'appliquer.

View file

@ -60,6 +60,8 @@ sont les seules vérifiables.
| **D-40** | L'accès de secours passe par **`sudo` sur l'hôte**, pas par un compte web permanent | `service → Keycloak → LDAP` est en série : sans secours, une panne exclut tout le monde, y compris pour réparer | `authentification.md` §4 | — | | **D-40** | L'accès de secours passe par **`sudo` sur l'hôte**, pas par un compte web permanent | `service → Keycloak → LDAP` est en série : sans secours, une panne exclut tout le monde, y compris pour réparer | `authentification.md` §4 | — |
| **D-41** | Le formulaire de connexion locale n'est **pas proposé au repos** (`<rôle>_connexion_locale: false`) | il contourne la politique de mot de passe, le MFA et surtout la **révocation centrale** ; `sudo` est le mécanisme de réouverture | `authentification.md` §3 | — | | **D-41** | Le formulaire de connexion locale n'est **pas proposé au repos** (`<rôle>_connexion_locale: false`) | il contourne la politique de mot de passe, le MFA et surtout la **révocation centrale** ; `sudo` est le mécanisme de réouverture | `authentification.md` §3 | — |
| **D-42** | Chaque rôle **déclare sa position** d'authentification (`meta/authentification.yml`), gardée par une preuve | une directive qu'aucune garde ne vérifie finit par ne plus être vraie — c'était le cas des 28 lignes d'intégration recopiées | `authentification.md` §5 | P29 |
## 3. Les secrets ## 3. Les secrets
| # | Décision | Pourquoi | Détail | Garde | | # | Décision | Pourquoi | Détail | Garde |

View file

@ -0,0 +1,8 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: sans-auth-humaine
raison: >-
Depots restic joints par cle SSH ; aucun humain ne s'y connecte.

View file

@ -0,0 +1,9 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: sans-auth-humaine
raison: >-
Expose, mais aucun compte : l'acces est autorise par un jeton WOPI emis par
Nextcloud, qui a deja authentifie l'humain.

View file

@ -0,0 +1,8 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: sans-auth-humaine
raison: >-
Socle du systeme : SSH par cle, aucune interface d'authentification propre.

View file

@ -0,0 +1,9 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: ldap-direct
raison: >-
IMAP ne parle pas OIDC. Authentification LDAP directe : le chemin differe, la
source est la meme.

View file

@ -0,0 +1,12 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: web-sso
mecanisme: natif
formulaire_local: ferme
secours: "forgejo admin user change-password"
raison: >-
Interface humaine exposee ; OAuth2 natif. ENABLE_INTERNAL_SIGNIN ferme aussi
l'API en Basic (Forgejo >= 10).

View file

@ -0,0 +1,11 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: web-sso
mecanisme: natif
formulaire_local: ferme
secours: "grafana-cli admin reset-admin-password"
raison: >-
Interface humaine exposee ; OIDC natif (generic_oauth) vers Keycloak.

View file

@ -0,0 +1,8 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: sans-auth-humaine
raison: >-
Coeur de supervision, sans interface : l'UI est serveur_icingaweb2.

View file

@ -0,0 +1,12 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: web-sso
mecanisme: oauth2-proxy
formulaire_local: ferme
secours: "icingacli user ... (via sudo sur l'hote)"
raison: >-
Pas d'OIDC natif : la passerelle oauth2-proxy porte l'authentification et
transmet l'identite en en-tete. L'hote n'est pas expose directement.

View file

@ -0,0 +1,11 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: socle-identite
secours: "kcadm.sh / bin/kc.sh (sudo)"
raison: >-
EST le fournisseur d'identite : il ne peut pas deleguer son authentification a
lui-meme. Son administrateur local est, par construction, l'avant-dernier
maillon avant sudo.

View file

@ -0,0 +1,9 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: interne-sans-auth
raison: >-
API HTTP sans authentification, joignable DANS le tenant. Non exposee au plan.
Consommee par Grafana, pas par un humain — mais rien ne l'empeche.

View file

@ -0,0 +1,12 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: web-sso
mecanisme: natif
formulaire_local: masque
secours: "occ user:resetpassword <utilisateur>"
raison: >-
Interface humaine exposee ; app user_oidc. `hide_login_form` MASQUE seulement
: `?direct=1` reste le chemin de secours documente par l'amont.

View file

@ -0,0 +1,8 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: sans-auth-humaine
raison: >-
Frontal HTTP : il sert et relaie, il n'authentifie pas d'humain.

View file

@ -0,0 +1,12 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: web-sso
mecanisme: natif
formulaire_local: aucun
secours: "systemctl stop oauth2-proxy, puis joindre l'amont en local (sudo)"
raison: >-
C'EST la passerelle SSO generique. Elle n'a pas de compte propre : sans elle,
l'amont n'est joignable qu'en local.

View file

@ -0,0 +1,10 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: socle-identite
secours: "ldapmodify sur cn=config / cn=admin (sudo)"
raison: >-
EST la source de verite des comptes. Son cn=admin est la racine de la chaine
de secours : si LDAP tombe, plus rien n'authentifie.

View file

@ -0,0 +1,8 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: ldap-direct
raison: >-
SMTP ne parle pas OIDC. Consultation LDAP pour les identites et la remise.

View file

@ -0,0 +1,8 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: sans-auth-humaine
raison: >-
Comptes de service, secrets en voute ; pas d'humain.

View file

@ -0,0 +1,9 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: sans-auth-humaine
raison: >-
DNS autoritatif ; l'API est joignable en local avec une cle, pas par un
humain.

View file

@ -0,0 +1,10 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: interne-sans-auth
raison: >-
Interface web sans authentification, joignable DANS le tenant. Non exposee au
plan (aucun `expose`). Lacune connue : oauth2-proxy s'appliquerait, comme
devant Icinga Web 2.

View file

@ -0,0 +1,8 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: sans-auth-humaine
raison: >-
Cache local, secret en voute ; pas d'humain.

View file

@ -0,0 +1,8 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: sans-auth-humaine
raison: >-
Filtrage courriel ; l'IHM de contrôle n'est pas activee.

View file

@ -0,0 +1,8 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: sans-auth-humaine
raison: >-
AC interne : les hotes s'enrolent par jeton de provisionneur, pas par compte.

View file

@ -0,0 +1,9 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: sans-auth-humaine
raison: >-
Execute l'application du tenant ; l'authentification appartient a cette
application.

View file

@ -0,0 +1,8 @@
---
# Position de ce role dans la directive d'authentification (D-38..D-41).
# Voir docs/authentification.md. Gardee par la preuve P29 : un role sans cette
# declaration, ou dont la declaration contredit son code, fait echouer le harnais.
authentification:
portee: sans-auth-humaine
raison: >-
Sert des fichiers statiques.

View file

@ -163,6 +163,94 @@ def preuve_modele_socle() -> tuple[bool, str]:
return True, "Modele public socle : domaines/serveurs/applications/bases valides." return True, "Modele public socle : domaines/serveurs/applications/bases valides."
PORTEES_AUTH = {"web-sso", "socle-identite", "ldap-direct", "interne-sans-auth",
"sans-auth-humaine"}
def preuve_authentification() -> tuple[bool, str]:
"""Chaque role serveur_* declare sa position d'authentification, et ne ment pas.
La directive (D-38..D-41) est une regle de conduite : sans garde, elle finit par
ne plus etre vraie c'est ce qui etait arrive aux 28 lignes d'integration
recopiees. La declaration seule ne suffirait pas non plus : on verifie qu'elle
est COHERENTE avec le code du role.
Echecs : role sans declaration, portee inconnue, `web-sso` sans acces de secours,
`web-sso natif` avec compte local mais sans reglage `<role>_connexion_locale`,
`web-sso natif` dont le code ne mentionne aucun OIDC, `ldap-direct` sans
reference LDAP.
Les portees `interne-sans-auth` sont COMPTEES, pas refusees : ce sont des lacunes
assumees et nommees. Les taire les ferait oublier ; les refuser bloquerait le
harnais sur une decision deja prise.
"""
import yaml as _yaml
roles = RACINE / "roles"
fautes, lacunes, par_portee = [], [], {}
for d in sorted(x for x in roles.iterdir() if x.is_dir() and x.name.startswith("serveur_")):
meta = d / "meta" / "authentification.yml"
if not meta.is_file():
fautes.append(f"{d.name}: aucune declaration (meta/authentification.yml)")
continue
decl = ((_yaml.safe_load(meta.read_text(encoding="utf-8")) or {})
.get("authentification") or {})
portee = str(decl.get("portee", "")).strip()
if portee not in PORTEES_AUTH:
fautes.append(f"{d.name}: portee '{portee}' inconnue")
continue
par_portee.setdefault(portee, []).append(d.name)
if not str(decl.get("raison", "")).strip():
fautes.append(f"{d.name}: portee sans raison")
# Confronter la declaration au CODE. Les premieres versions cherchaient les
# mots « ldap » / « oidc » dans le role : trop faible — le mot « LDAP » dans un
# commentaire de serveur_grafana suffisait a valider une declaration
# `ldap-direct` mensongere. On exige donc des indices NOMMES : une variable du
# namespace du role, ou une URI de schema ldap. Une phrase en prose n'en cree pas.
corps = ""
for f in d.rglob("*"):
if f.is_file() and f.suffix in (".yml", ".j2"):
corps += f.read_text(encoding="utf-8", errors="ignore").lower()
defauts = {}
fd = d / "defaults" / "main.yml"
if fd.is_file():
defauts = _yaml.safe_load(fd.read_text(encoding="utf-8")) or {}
preuve_ldap = (f"{d.name}_ldap" in corps or "ldap://" in corps or "ldaps://" in corps)
preuve_oidc = any(f"{d.name}_{m}" in corps for m in ("oidc", "oauth"))
if portee in ("web-sso", "socle-identite") and not str(decl.get("secours", "")).strip():
fautes.append(f"{d.name}: {portee} sans acces de secours declare")
if portee == "web-sso":
if decl.get("mecanisme") not in ("natif", "oauth2-proxy"):
fautes.append(f"{d.name}: web-sso sans mecanisme (natif|oauth2-proxy)")
# `aucun` = le service n'a pas de compte local du tout (une passerelle).
# Sans cette valeur, on serait tente d'ecrire « ferme » — ce qui laisserait
# croire qu'une porte a ete fermee alors qu'il n'y en a jamais eu.
if decl.get("formulaire_local") not in ("ferme", "masque", "ouvert", "aucun"):
fautes.append(f"{d.name}: web-sso sans posture de formulaire local")
if decl.get("mecanisme") == "natif" and decl.get("formulaire_local") != "aucun":
# DEFINI dans defaults, pas seulement reference ailleurs : le gabarit
# peut citer la variable alors que plus rien ne lui donne de valeur.
if f"{d.name}_connexion_locale" not in defauts:
fautes.append(f"{d.name}: web-sso natif sans reglage "
f"{d.name}_connexion_locale dans defaults/main.yml")
if not preuve_oidc:
fautes.append(f"{d.name}: se declare web-sso natif, mais aucune "
f"variable {d.name}_oidc/_oauth dans son code")
if portee == "ldap-direct" and not preuve_ldap:
fautes.append(f"{d.name}: se declare ldap-direct, mais aucune variable "
f"{d.name}_ldap ni URI ldap:// dans son code")
if portee == "interne-sans-auth":
lacunes.append(d.name)
if fautes:
return False, "; ".join(fautes[:6])
total = sum(len(v) for v in par_portee.values())
resume = ", ".join(f"{k} {len(v)}" for k, v in sorted(par_portee.items()))
suffixe = f" ; {len(lacunes)} lacune(s) nommee(s) : {', '.join(lacunes)}" if lacunes else ""
return True, f"{total} role(s) serveur declares ({resume}){suffixe}."
def preuve_propriete_des_intrants() -> tuple[bool, str]: def preuve_propriete_des_intrants() -> tuple[bool, str]:
"""Aucune cle de l'HEBERGEUR ne subsiste dans un group_vars de tenant. """Aucune cle de l'HEBERGEUR ne subsiste dans un group_vars de tenant.
@ -380,6 +468,8 @@ PREUVES: list[dict] = [
"func": preuve_propriete_des_intrants}, "func": preuve_propriete_des_intrants},
{"id": "P28", "titre": "Pools Proxmox : un par tenant, sans collision", "refs": ["AFF-110"], {"id": "P28", "titre": "Pools Proxmox : un par tenant, sans collision", "refs": ["AFF-110"],
"cmds": [[sys.executable, "scripts/devis_proxmox_pools.py", "--verifier"]]}, "cmds": [[sys.executable, "scripts/devis_proxmox_pools.py", "--verifier"]]},
{"id": "P29", "titre": "Authentification : chaque role declare sa position", "refs": ["AFF-111"],
"func": preuve_authentification},
] ]