From 22ef279464455e1dacad29f83cc8f7279d206f85 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Mon, 3 Aug 2026 17:05:43 -0400 Subject: [PATCH] =?UTF-8?q?authentification=20:=20chaque=20r=C3=B4le=20d?= =?UTF-8?q?=C3=A9clare=20sa=20position,=20gard=C3=A9=20par=20P29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 _oidc / _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 --- CHANGELOG.md | 52 +++++++++++ docs/audit/affirmations.md | 2 + docs/audit/preuve-2026-08-03.md | 7 +- docs/authentification.md | 47 ++++++++-- docs/decisions-architecture.md | 2 + .../serveur_backup/meta/authentification.yml | 8 ++ .../meta/authentification.yml | 9 ++ .../serveur_debian/meta/authentification.yml | 8 ++ .../serveur_dovecot/meta/authentification.yml | 9 ++ .../serveur_forgejo/meta/authentification.yml | 12 +++ .../serveur_grafana/meta/authentification.yml | 11 +++ .../serveur_icinga/meta/authentification.yml | 8 ++ .../meta/authentification.yml | 12 +++ .../meta/authentification.yml | 11 +++ roles/serveur_loki/meta/authentification.yml | 9 ++ .../meta/authentification.yml | 12 +++ roles/serveur_nginx/meta/authentification.yml | 8 ++ .../meta/authentification.yml | 12 +++ .../meta/authentification.yml | 10 +++ .../serveur_postfix/meta/authentification.yml | 8 ++ .../meta/authentification.yml | 8 ++ .../meta/authentification.yml | 9 ++ .../meta/authentification.yml | 10 +++ roles/serveur_redis/meta/authentification.yml | 8 ++ .../serveur_rspamd/meta/authentification.yml | 8 ++ .../serveur_step_ca/meta/authentification.yml | 8 ++ .../meta/authentification.yml | 9 ++ .../meta/authentification.yml | 8 ++ scripts/prouver.py | 90 +++++++++++++++++++ 29 files changed, 404 insertions(+), 11 deletions(-) create mode 100644 roles/serveur_backup/meta/authentification.yml create mode 100644 roles/serveur_collabora/meta/authentification.yml create mode 100644 roles/serveur_debian/meta/authentification.yml create mode 100644 roles/serveur_dovecot/meta/authentification.yml create mode 100644 roles/serveur_forgejo/meta/authentification.yml create mode 100644 roles/serveur_grafana/meta/authentification.yml create mode 100644 roles/serveur_icinga/meta/authentification.yml create mode 100644 roles/serveur_icingaweb2/meta/authentification.yml create mode 100644 roles/serveur_keycloak/meta/authentification.yml create mode 100644 roles/serveur_loki/meta/authentification.yml create mode 100644 roles/serveur_nextcloud/meta/authentification.yml create mode 100644 roles/serveur_nginx/meta/authentification.yml create mode 100644 roles/serveur_oauth2_proxy/meta/authentification.yml create mode 100644 roles/serveur_openldap/meta/authentification.yml create mode 100644 roles/serveur_postfix/meta/authentification.yml create mode 100644 roles/serveur_postgresql/meta/authentification.yml create mode 100644 roles/serveur_powerdns/meta/authentification.yml create mode 100644 roles/serveur_prometheus/meta/authentification.yml create mode 100644 roles/serveur_redis/meta/authentification.yml create mode 100644 roles/serveur_rspamd/meta/authentification.yml create mode 100644 roles/serveur_step_ca/meta/authentification.yml create mode 100644 roles/serveur_web_dorsal/meta/authentification.yml create mode 100644 roles/serveur_web_frontal/meta/authentification.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index d1e183a..abb24d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 +(`_oidc`, `_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 diff --git a/docs/audit/affirmations.md b/docs/audit/affirmations.md index 5f6dfdd..5ebc4cf 100644 --- a/docs/audit/affirmations.md +++ b/docs/audit/affirmations.md @@ -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 `_oidc`, ou `ldap-direct` sans variable `_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`, diff --git a/docs/audit/preuve-2026-08-03.md b/docs/audit/preuve-2026-08-03.md index 7b6d3d5..0ba3beb 100644 --- a/docs/audit/preuve-2026-08-03.md +++ b/docs/audit/preuve-2026-08-03.md @@ -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 diff --git a/docs/authentification.md b/docs/authentification.md index 68f20ce..303c24e 100644 --- a/docs/authentification.md +++ b/docs/authentification.md @@ -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 `_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** (`_oidc`, `_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. diff --git a/docs/decisions-architecture.md b/docs/decisions-architecture.md index d910d6d..10a2246 100644 --- a/docs/decisions-architecture.md +++ b/docs/decisions-architecture.md @@ -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** (`_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 | diff --git a/roles/serveur_backup/meta/authentification.yml b/roles/serveur_backup/meta/authentification.yml new file mode 100644 index 0000000..2dd1f94 --- /dev/null +++ b/roles/serveur_backup/meta/authentification.yml @@ -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. diff --git a/roles/serveur_collabora/meta/authentification.yml b/roles/serveur_collabora/meta/authentification.yml new file mode 100644 index 0000000..7908e1b --- /dev/null +++ b/roles/serveur_collabora/meta/authentification.yml @@ -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. diff --git a/roles/serveur_debian/meta/authentification.yml b/roles/serveur_debian/meta/authentification.yml new file mode 100644 index 0000000..48b6fb7 --- /dev/null +++ b/roles/serveur_debian/meta/authentification.yml @@ -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. diff --git a/roles/serveur_dovecot/meta/authentification.yml b/roles/serveur_dovecot/meta/authentification.yml new file mode 100644 index 0000000..ba91994 --- /dev/null +++ b/roles/serveur_dovecot/meta/authentification.yml @@ -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. diff --git a/roles/serveur_forgejo/meta/authentification.yml b/roles/serveur_forgejo/meta/authentification.yml new file mode 100644 index 0000000..af9713c --- /dev/null +++ b/roles/serveur_forgejo/meta/authentification.yml @@ -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). diff --git a/roles/serveur_grafana/meta/authentification.yml b/roles/serveur_grafana/meta/authentification.yml new file mode 100644 index 0000000..472d638 --- /dev/null +++ b/roles/serveur_grafana/meta/authentification.yml @@ -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. diff --git a/roles/serveur_icinga/meta/authentification.yml b/roles/serveur_icinga/meta/authentification.yml new file mode 100644 index 0000000..a5f7899 --- /dev/null +++ b/roles/serveur_icinga/meta/authentification.yml @@ -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. diff --git a/roles/serveur_icingaweb2/meta/authentification.yml b/roles/serveur_icingaweb2/meta/authentification.yml new file mode 100644 index 0000000..f6363b3 --- /dev/null +++ b/roles/serveur_icingaweb2/meta/authentification.yml @@ -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. diff --git a/roles/serveur_keycloak/meta/authentification.yml b/roles/serveur_keycloak/meta/authentification.yml new file mode 100644 index 0000000..d0260b1 --- /dev/null +++ b/roles/serveur_keycloak/meta/authentification.yml @@ -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. diff --git a/roles/serveur_loki/meta/authentification.yml b/roles/serveur_loki/meta/authentification.yml new file mode 100644 index 0000000..d60aa28 --- /dev/null +++ b/roles/serveur_loki/meta/authentification.yml @@ -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. diff --git a/roles/serveur_nextcloud/meta/authentification.yml b/roles/serveur_nextcloud/meta/authentification.yml new file mode 100644 index 0000000..baab605 --- /dev/null +++ b/roles/serveur_nextcloud/meta/authentification.yml @@ -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 " + raison: >- + Interface humaine exposee ; app user_oidc. `hide_login_form` MASQUE seulement + : `?direct=1` reste le chemin de secours documente par l'amont. diff --git a/roles/serveur_nginx/meta/authentification.yml b/roles/serveur_nginx/meta/authentification.yml new file mode 100644 index 0000000..8fd78e6 --- /dev/null +++ b/roles/serveur_nginx/meta/authentification.yml @@ -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. diff --git a/roles/serveur_oauth2_proxy/meta/authentification.yml b/roles/serveur_oauth2_proxy/meta/authentification.yml new file mode 100644 index 0000000..4349467 --- /dev/null +++ b/roles/serveur_oauth2_proxy/meta/authentification.yml @@ -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. diff --git a/roles/serveur_openldap/meta/authentification.yml b/roles/serveur_openldap/meta/authentification.yml new file mode 100644 index 0000000..d591167 --- /dev/null +++ b/roles/serveur_openldap/meta/authentification.yml @@ -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. diff --git a/roles/serveur_postfix/meta/authentification.yml b/roles/serveur_postfix/meta/authentification.yml new file mode 100644 index 0000000..94237c9 --- /dev/null +++ b/roles/serveur_postfix/meta/authentification.yml @@ -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. diff --git a/roles/serveur_postgresql/meta/authentification.yml b/roles/serveur_postgresql/meta/authentification.yml new file mode 100644 index 0000000..5823da6 --- /dev/null +++ b/roles/serveur_postgresql/meta/authentification.yml @@ -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. diff --git a/roles/serveur_powerdns/meta/authentification.yml b/roles/serveur_powerdns/meta/authentification.yml new file mode 100644 index 0000000..afe64e1 --- /dev/null +++ b/roles/serveur_powerdns/meta/authentification.yml @@ -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. diff --git a/roles/serveur_prometheus/meta/authentification.yml b/roles/serveur_prometheus/meta/authentification.yml new file mode 100644 index 0000000..3b5fb2b --- /dev/null +++ b/roles/serveur_prometheus/meta/authentification.yml @@ -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. diff --git a/roles/serveur_redis/meta/authentification.yml b/roles/serveur_redis/meta/authentification.yml new file mode 100644 index 0000000..cb8aa8b --- /dev/null +++ b/roles/serveur_redis/meta/authentification.yml @@ -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. diff --git a/roles/serveur_rspamd/meta/authentification.yml b/roles/serveur_rspamd/meta/authentification.yml new file mode 100644 index 0000000..98aecd8 --- /dev/null +++ b/roles/serveur_rspamd/meta/authentification.yml @@ -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. diff --git a/roles/serveur_step_ca/meta/authentification.yml b/roles/serveur_step_ca/meta/authentification.yml new file mode 100644 index 0000000..cf67887 --- /dev/null +++ b/roles/serveur_step_ca/meta/authentification.yml @@ -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. diff --git a/roles/serveur_web_dorsal/meta/authentification.yml b/roles/serveur_web_dorsal/meta/authentification.yml new file mode 100644 index 0000000..2d7fc42 --- /dev/null +++ b/roles/serveur_web_dorsal/meta/authentification.yml @@ -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. diff --git a/roles/serveur_web_frontal/meta/authentification.yml b/roles/serveur_web_frontal/meta/authentification.yml new file mode 100644 index 0000000..74fecc2 --- /dev/null +++ b/roles/serveur_web_frontal/meta/authentification.yml @@ -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. diff --git a/scripts/prouver.py b/scripts/prouver.py index 993a233..c918bef 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -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 `_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}, ]