From 561c034eb447f6d86aab6a33f8f2e7c65cf214e3 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Wed, 26 Aug 2026 16:58:17 -0400 Subject: [PATCH] =?UTF-8?q?flux=20:=20P49=20=E2=80=94=20le=20registre=20de?= =?UTF-8?q?s=20flux=20avait=20derive=20sans=20bruit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `docs/registre-flux.md` est GENERE depuis les `roles/*/meta/flux.yml`, et c'est le document qu'un humain lit pour savoir ce que le pare-feu laisse passer. Son EXISTENCE etait verifiee depuis longtemps ; sa FRAICHEUR ne l'etait pas. Il avait derive : la garde d'administration y portait encore `10.0.0.0/24` alors que le reseau d'administration vaut `10.17.0.0/24`, deux flux `client_resolveur` ajoutes depuis n'y figuraient pas, et un hote manquait des listes de sources. Un lecteur y aurait lu un pare-feu qui n'existe plus. L'inventaire avait deja sa garde — P03, le diff-vide du plan. Le registre des flux est le meme genre d'artefact : genere, versionne, lu par un humain. Il lui manquait la meme. `generer_registre` etant une fonction PURE, P49 la rejoue en memoire et compare — une preuve qui repare ce qu'elle mesure ne mesure plus rien. Controle negatif ideal, et il ne s'invente pas : la version commitee elle-meme. Restauree, la preuve echoue ; regeneree, elle passe. CE QUE CETTE DECOUVERTE CORRIGE AUSSI DANS MA TETE. J'avais decrit le symptome comme « le runner salit ses propres clones » — une contradiction structurelle entre un depot-clone et un repertoire de travail. C'etait faux, et la question de l'exploitant l'a mis au jour. Regenerer un artefact DOIT produire un diff quand les sources ont change ; ce qui manquait n'etait pas une architecture, c'etait une garde. Un symptome observe depuis un seul endroit ressemble toujours a une propriete de cet endroit. Ce commit emporte aussi la regeneration elle-meme : le registre du moteur, et les quatorze fichiers nftables de Chezlepro, remis en accord avec leurs sources. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 28 ++++++++++++++++++++ docs/audit/preuve-2026-08-26.md | 3 ++- docs/registre-flux.md | 37 ++++++++++++++++++-------- scripts/prouver.py | 47 +++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 864926f..5c6d9a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # CHANGELOG — Set-OPS +## 2026-08-26 — P49 : le registre des flux avait dérivé sans bruit + +**48 preuves.** `docs/registre-flux.md` est **généré** depuis les `roles/*/meta/flux.yml`, +et c'est le document qu'un humain lit pour savoir ce que le pare-feu laisse passer. Son +**existence** était vérifiée depuis longtemps ; sa **fraîcheur** ne l'était pas. + +Il avait dérivé : la garde d'administration y portait encore `10.0.0.0/24` alors que le +réseau d'administration vaut `10.17.0.0/24`, deux flux `client_resolveur` ajoutés depuis +n'y figuraient pas, et un hôte manquait des listes de sources. **Un lecteur y aurait lu un +pare-feu qui n'existe plus.** + +L'inventaire avait déjà sa garde — **P03**, le diff-vide du plan. Le registre des flux est +le même genre d'artefact : généré, versionné, lu par un humain. Il lui manquait la même. +`generer_registre` étant une fonction pure, P49 la rejoue **en mémoire** et compare — une +preuve qui répare ce qu'elle mesure ne mesure plus rien. + +*Contrôle négatif idéal, et il ne s'invente pas : la version commitée elle-même. Restaurée, +la preuve échoue ; régénérée, elle passe.* + +### Ce que cette découverte corrige aussi dans ma tête + +J'avais d'abord décrit le symptôme comme « le runner salit ses propres clones » — une +contradiction structurelle entre un dépôt-clone et un répertoire de travail. C'était faux, +et la question de l'exploitant l'a mis au jour. Régénérer un artefact **doit** produire un +diff quand les sources ont changé ; ce qui manquait n'était pas une architecture, c'était +une garde. *Un symptôme observé depuis un seul endroit ressemble toujours à une propriété +de cet endroit.* + ## 2026-08-26 — `serveur_ops_tenant` : le runner d'un tenant reçoit enfin sa voûte **47 preuves.** La doctrine des runners décrit trois portées depuis le 2026-08-22 : diff --git a/docs/audit/preuve-2026-08-26.md b/docs/audit/preuve-2026-08-26.md index 235b499..543c86f 100644 --- a/docs/audit/preuve-2026-08-26.md +++ b/docs/audit/preuve-2026-08-26.md @@ -7,7 +7,7 @@ > [`docs/audit/affirmations.md`](affirmations.md). - **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/production/hosts.yml` -- **Verdict** : ✅ CONFORME (47 OK · 0 echec · 1 saute) +- **Verdict** : ✅ CONFORME (48 OK · 0 echec · 1 saute) ## Preuves @@ -61,6 +61,7 @@ | P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos | | P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). | | P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 84 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. | +| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (110 lignes). | ## Couverture des affirmations ✅ du registre diff --git a/docs/registre-flux.md b/docs/registre-flux.md index 025f342..026a7f2 100644 --- a/docs/registre-flux.md +++ b/docs/registre-flux.md @@ -11,13 +11,17 @@ | `client_journal` | egress | 3100 | tcp | serveur_loki | tls | Expédition des journaux par Alloy vers le collecteur central Loki (HTTPS, cert step-ca). | | `client_metrique` | ingress | 9100 | tcp | serveur_prometheus | tls | Scrape des métriques par Prometheus (node_exporter en HTTPS). | | `client_pki` | egress | 8443 | tcp | serveur_step_ca | tls-requis | Émission/renouvellement des certificats par ACME et récupération de la racine auprès de l'AC interne. | +| `client_resolveur` | egress | 53 | udp | serveur_resolveur | clair | Résoudre auprès du résolveur de l'écosystème, et de personne d'autre. | +| `client_resolveur` | egress | 53 | tcp | serveur_resolveur | clair | Réponses longues et bascule TCP, obligatoires en DNS. | | `client_smtp` | egress | 25 | tcp | serveur_postfix | starttls | Relais des notifications locales vers le MTA central (Postfix), STARTTLS. | -| `client_resolveur` | ingress | 53 | udp | localhost | clair | Résolveur local sur boucle locale (les processus du nœud interrogent 127.0.0.1). | -| `client_resolveur` | egress | 53 | tcp | serveur_powerdns | clair | Transfert des requêtes de la zone souveraine vers le DNS autoritatif interne (PowerDNS). | -| `client_resolveur` | egress | 53 | udp | externe | clair | Récursion DNS depuis la racine (UDP d'abord), validée par DNSSEC — la confidentialité du transport n'est pas l'enjeu, l'authenticité l'est. | -| `client_resolveur` | egress | 53 | tcp | externe | clair | Récursion DNS en TCP : repli obligatoire quand la réponse dépasse la taille UDP (fréquent avec DNSSEC). | +| `serveur_artefacts` | ingress | 3142 | tcp | flotte | clair | Toute la flotte prend ses paquets ici. En clair, et c'est correct : l'intégrité d'un dépôt apt vient de ses signatures, qu'apt vérifie de toute façon — un intermédiaire ne peut pas altérer un paquet sans se faire prendre. | +| `serveur_artefacts` | egress | 80 | tcp | externe | clair | Remplir le cache depuis les dépôts Debian amont (deb.debian.org, security). | +| `serveur_artefacts` | egress | 3142 | tcp | voisins_site | clair | Prendre le cache du site comme amont, plutot que d'aller chez Debian. | | `serveur_backup` | ingress | 22 | tcp | client_backup | ssh | Dépôt restic servi par SSH (utilisateur restreint restic + clé) ; chaque client_backup pousse ses instantanés. | | `serveur_backup` | egress | 5665 | tcp | serveur_icinga | tls-requis | Rapport passif des sauvegardes vers l'API Icinga : le depot est le seul a voir ce qui est reellement arrive. | +| `serveur_cache_site` | ingress | 3142 | tcp | voisins_site | clair | Servir les caches des écosystèmes voisins. Debian n'est ainsi téléchargé qu'une fois pour tout le site, et le cache ne voit que des requêtes AGRÉGÉES — jamais quelle machine installe quoi. | +| `serveur_cache_site` | egress | 80 | tcp | externe | clair | Remplir le cache depuis les dépôts Debian amont. En clair parce que les dépôts apt sont signés : l'intégrité vient de la signature, pas du transport. | +| `serveur_cache_site` | egress | 443 | tcp | externe | tls-requis | Les dépôts tiers qui n'existent qu'en HTTPS (smallstep, Grafana, Icinga). Le cache les relaie pour que la flotte n'ait pas à sortir elle-même. | | `serveur_collabora` | ingress | 9980 | tcp | edge | clair | Éditeur servi au navigateur via l'edge (WebSocket WOPI ; TLS terminé à l'edge). | | `serveur_collabora` | ingress | 9980 | tcp | localhost | clair | Vérifications WOPI serveur→Collabora depuis Nextcloud co-localisé. | | `serveur_debian` | ingress | 22 | tcp | flotte, externe | ssh | Plan de gestion : administration et déploiement Ansible par SSH (inter-nœud ; l'accès depuis l'extérieur est filtré à l'OPNsense). | @@ -30,7 +34,8 @@ | `serveur_dovecot` | ingress | 993 | tcp | externe | tls-requis | Accès courriel des utilisateurs (IMAPS). Frontière publique gérée à l'OPNsense. | | `serveur_dovecot` | ingress | 12345 | tcp | serveur_postfix | tls | Authentification SASL déléguée : Postfix valide les identifiants de soumission contre Dovecot. | | `serveur_dovecot` | egress | 636 | tcp | serveur_openldap | tls-requis | userdb/passdb : Dovecot résout et authentifie les comptes sur l'annuaire (LDAPS). | -| `serveur_forgejo` | ingress | 3000 | tcp | edge | clair | Interface web + Git HTTP servis via l'edge (TLS terminé à l'edge). | +| `serveur_forgejo` | ingress | 3000 | tcp | edge | clair | Interface web + Git HTTP derrière un edge : le nginx termine le TLS et parle en clair à la forge. C'est le cas de tout tenant. | +| `serveur_forgejo` | ingress | derive | tcp | flotte | tls | Sans edge devant elle — la forge du SITE — elle sert son propre TLS sur le port du schéma (443), avec le certificat de la machine. `derive` parce que le port vient de `serveur_forgejo_http_port` : écrire 3000 en dur ici serait faux pour elle, et rien ne le signalerait puisque ce flux ne traverse pas la frontière. | | `serveur_forgejo` | egress | 25 | tcp | serveur_postfix | starttls | Notifications courriel (relais via le MTA Postfix). | | `serveur_forgejo` | egress | 443 | tcp | edge | tls-requis | Découverte OIDC et jetons auprès de Keycloak (via son FQDN publié à l'edge). | | `serveur_forgejo` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base de données Forgejo (verify-full). | @@ -61,6 +66,13 @@ | `serveur_oauth2_proxy` | egress | 8080 | tcp | localhost | clair | Relais vers l'application co-localisée protégée (upstream en localhost). | | `serveur_openldap` | ingress | 389 | tcp | flotte | starttls | LDAP + STARTTLS pour les clients internes qui préfèrent la mise à niveau TLS sur 389. | | `serveur_openldap` | ingress | 636 | tcp | serveur_keycloak, serveur_dovecot, serveur_icingaweb2, serveur_postfix | tls-requis | LDAPS : fédération (Keycloak), userdb courriel (Dovecot), auth web (Icinga Web 2), tables virtuelles (Postfix). | +| `serveur_ops` | egress | 22 | tcp | flotte | ssh | Piloter la flotte — c'est la raison d'être du poste. | +| `serveur_ops` | egress | 443 | tcp | edge | tls-requis | Cloner et resynchroniser le génome depuis la forge de l'écosystème. | +| `serveur_ops` | egress | 443 | tcp | voisins_site | tls-requis | Cloner le genome depuis la forge du site, quand cet ecosysteme n'heberge pas la sienne. | +| `serveur_ops` | egress | 8006 | tcp | externe | tls-requis | API de l'hyperviseur : créer et cloner les VM d'un écosystème descendant. | +| `serveur_ops_site` | egress | 22 | tcp | fabric | ssh | Shell des hyperviseurs : ce que l'API ne couvre pas — configuration reseau, ponts, deplacement de disques. Un pouvoir distinct de l'API, donc declare a part. | +| `serveur_ops_site` | egress | 443 | tcp | fabric | tls-requis | API de la frontiere OPNsense : poser les alias et les regles qui ouvrent les flux du tenant qu'on materialise. Preparer le terrain sans cela laisserait un terrain injoignable. | +| `serveur_ops_site` | egress | 8006 | tcp | fabric | tls-requis | API de l'hyperviseur : créer, cloner et détruire les VM de la fabric. Le seul flux par lequel un écosystème peut en matérialiser un autre. | | `serveur_postfix` | ingress | 25 | tcp | externe, client_smtp | starttls | SMTP entrant : courrier externe (MX) et notifications internes (client_smtp). | | `serveur_postfix` | ingress | 587 | tcp | flotte | starttls | Soumission authentifiée (submission) pour les agents internes qui envoient du courrier. | | `serveur_postfix` | egress | 24 | tcp | serveur_dovecot | tls-requis | Remise finale par LMTP au mailstore (Dovecot), en TLS vérifié. | @@ -69,13 +81,16 @@ | `serveur_postfix` | egress | 11332 | tcp | localhost | clair | Filtre milter rspamd co-localisé (antispam + signature DKIM). | | `serveur_postfix` | egress | 12345 | tcp | serveur_dovecot | tls | Validation SASL des identifiants de soumission contre Dovecot. | | `serveur_postgresql` | ingress | 5432 | tcp | serveur_keycloak, serveur_forgejo, serveur_icinga, serveur_nextcloud | tls-requis | Connexions applicatives à PostgreSQL (verify-full ; pg_hba hostssl). | -| `serveur_powerdns` | ingress | 53 | udp | flotte | clair | Résolution DNS interne (zone souveraine). DoT/DoH = feuille de route (chiffrement DNS). | -| `serveur_powerdns` | ingress | 53 | tcp | flotte | clair | Résolution DNS interne en TCP (réponses volumineuses, AXFR restreint par allow_axfr_ips). | +| `serveur_powerdns` | ingress | derive | udp | flotte | clair | Zone souveraine. 53 seul sur son hôte, 5300 sur la loopback derrière le résolveur. | +| `serveur_powerdns` | ingress | derive | tcp | flotte | clair | Idem en TCP (réponses volumineuses, AXFR restreint par allow_axfr_ips). | | `serveur_powerdns` | egress | 53 | udp | externe | clair | Résolution sortante du serveur autoritatif POUR SES PROPRES besoins (apt, NTP) — il ne récurse pour aucun autre hôte. | | `serveur_powerdns` | egress | 53 | tcp | externe | clair | Repli TCP de la résolution sortante du serveur autoritatif (réponses dépassant la taille UDP). | | `serveur_prometheus` | ingress | 9090 | tcp | localhost | clair | Console Prometheus consommée en local par Grafana co-localisé (pas d'exposition inter-nœud). | | `serveur_prometheus` | egress | 9100 | tcp | client_metrique | tls | Scrape des node_exporter (HTTPS via cert step-ca) sur chaque nœud instrumenté. | | `serveur_redis` | ingress | 6379 | tcp | localhost | clair | Cache/verrous consommés uniquement par l'application co-localisée (ex. Nextcloud). Aucune exposition inter-nœud. | +| `serveur_resolveur` | ingress | 53 | udp | flotte | clair | Toute la flotte du tenant résout ici — et nulle part ailleurs. | +| `serveur_resolveur` | ingress | 53 | tcp | flotte | clair | Réponses longues et bascule TCP, obligatoires en DNS. | +| `serveur_resolveur` | egress | 53 | udp | externe | clair | Récursion depuis les serveurs racine. Aucun transitaire : l'écosystème ne confie ses questions à personne. | | `serveur_rspamd` | ingress | 11332 | tcp | localhost | clair | Protocole milter consommé par Postfix co-localisé (analyse + signature DKIM). Local uniquement. | | `serveur_rspamd` | ingress | 11334 | tcp | localhost | clair | Interface de contrôle rspamd (statistiques, apprentissage) en local. | | `serveur_step_ca` | ingress | 8443 | tcp | flotte | tls-requis | ACME + API step-ca : chaque nœud (client_pki) émet/renouvelle ses certificats et récupère la racine. | @@ -86,10 +101,10 @@ ## Synthèse chiffrement -- **clair** : 29 flux +- **clair** : 35 flux - **n-a** : 3 flux -- **ssh** : 3 flux +- **ssh** : 5 flux - **starttls** : 6 flux -- **tls** : 8 flux +- **tls** : 9 flux - **tls-cible** : 2 flux -- **tls-requis** : 26 flux +- **tls-requis** : 32 flux diff --git a/scripts/prouver.py b/scripts/prouver.py index e3438c6..fb4dd6b 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -1396,6 +1396,51 @@ def preuve_carte_dit_vrai() -> tuple[bool, str]: f"{len(_carte_chiffres_mesures())} chiffres correspondent a la mesure.") +def preuve_registre_flux_a_jour() -> tuple[bool, str]: + """La matrice d'audit des flux dit ce que le moteur declare AUJOURD'HUI. + + POURQUOI (mesure du 2026-08-26). `docs/registre-flux.md` est GENERE depuis les + `roles/*/meta/flux.yml`, et c'est le document qu'un humain lit pour savoir ce que le + pare-feu laisse passer. Son existence etait verifiee (P31) ; sa FRAICHEUR ne l'etait + pas. + + Il avait derive sans bruit : la garde d'administration y portait encore `10.0.0.0/24` + alors que le reseau d'administration vaut `10.17.0.0/24`, et deux flux `client_resolveur` + ajoutes depuis n'y figuraient pas. Un lecteur y aurait lu un pare-feu qui n'existe plus. + + L'INVENTAIRE AVAIT DEJA SA GARDE — P03, le diff-vide du plan. Le registre des flux est + le meme genre d'artefact : genere, versionne, lu par un humain. Il lui manquait la + meme. + + `generer_registre` est une fonction PURE : on la rejoue en memoire et on compare. Rien + n'est ecrit — une preuve qui repare ce qu'elle mesure ne mesure plus rien. + """ + fichier = RACINE / "docs" / "registre-flux.md" + if not fichier.is_file(): + return False, "docs/registre-flux.md est introuvable." + try: + sys.path.insert(0, str(RACINE / "scripts")) + import resoudre_flux as RF + attendu = RF.generer_registre(RF.charger_flux()) + except Exception as e: # noqa: BLE001 + return False, (f"Le registre des flux n'a pas pu etre rejoue : {e}. " + f"Tant qu'on ne sait pas le regenerer, on ne sait pas s'il dit vrai.") + + actuel = fichier.read_text(encoding="utf-8") + if actuel == attendu: + return True, ("Le registre des flux reproduit exactement ce que les " + f"`meta/flux.yml` declarent ({len(attendu.splitlines())} lignes).") + + a, b = actuel.splitlines(), attendu.splitlines() + ecarts = [f"ligne {i + 1} : le fichier dit « {x[:70]} », le moteur « {y[:70]} »" + for i, (x, y) in enumerate(zip(a, b)) if x != y][:3] + if len(a) != len(b): + ecarts.append(f"longueurs differentes : {len(a)} lignes contre {len(b)}") + return False, ("`docs/registre-flux.md` ne dit plus ce que le moteur declare. " + "Rejouer `make flux` et commiter le resultat.\n - " + + "\n - ".join(ecarts)) + + def preuve_glossaire_enseigne() -> tuple[bool, str]: """Tout mot que le depot emploie devant l'exploitant est explique au glossaire. @@ -1745,6 +1790,8 @@ PREUVES: list[dict] = [ "refs": [], "func": preuve_zones_inverses_couvrent_l_occupe}, {"id": "P48", "titre": "La carte d'orientation designe ce qui existe, et compte juste", "refs": [], "func": preuve_carte_dit_vrai}, + {"id": "P49", "titre": "Registre des flux : la matrice d'audit est a jour", + "refs": [], "func": preuve_registre_flux_a_jour}, {"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [], "func": preuve_devis_frontiere_du_site}, {"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],