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
## 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
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-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-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. |
**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-109 | **P27** (propriété des intrants : hébergeur et tenant séparés) |
| 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
(`make`=aide), AFF-014 (ciblage `groupe:&hotes_actifs`), AFF-024 (`instancier-appliquer`,

View file

@ -7,7 +7,7 @@
> [`docs/audit/affirmations.md`](affirmations.md).
- **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
@ -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. |
| 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. |
| 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. |
| 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. |
| 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. |
@ -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). |
| 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. |
| 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

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
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é
devant Icinga Web 2 : le patron existe, il reste à l'appliquer.
- **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
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
preuve refuse une interface humaine sans SSO ni secours déclaré.
Une directive qu'aucune garde ne vérifie finit par ne plus être vraie. Chaque rôle
`serveur_*` porte donc un `meta/authentification.yml`, et **P29** le confronte à son code.
| Portée | Sens | Rôles |
|---|---|---|
| `web-sso` | authentification humaine web → Keycloak (`mecanisme: natif` ou `oauth2-proxy`) | 5 |
| `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-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
| # | 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."
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]:
"""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},
{"id": "P28", "titre": "Pools Proxmox : un par tenant, sans collision", "refs": ["AFF-110"],
"cmds": [[sys.executable, "scripts/devis_proxmox_pools.py", "--verifier"]]},
{"id": "P29", "titre": "Authentification : chaque role declare sa position", "refs": ["AFF-111"],
"func": preuve_authentification},
]