From 8a9ac18462d048b3236bd87513d572622bb57d78 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Sun, 13 Sep 2026 22:52:19 -0400 Subject: [PATCH] intrants du site : la table comparait cinq valeurs sur sept Le controle rapportait CONFORME sur un locataire dont le plan d administration pointait les bouts de WAN d un AUTRE site. nftables_admin_ssh et passerelle_sortie n etaient compares par personne. Quatorze machines debout, le runner insemine, et l exploitant bloque a la porte sans qu aucune garde n ait rien eu a dire. nftables_admin_ssh est une liste : la comparaison normalise, parce que l ordre d une liste de sources ne porte aucun sens. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ docs/audit/preuve-2026-09-13.md | 18 +++++++++--------- scripts/site_intrants.py | 32 +++++++++++++++++++++++++++++--- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f330cc9..7334280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # CHANGELOG — Set-OPS +## 2026-09-13 (9) — Le site expose sept intrants ; le controle n'en comparait que cinq + +`site_intrants --verifier` rapportait **CONFORME** avec un aplomb complet sur un locataire +dont le plan d'administration pointait encore les bouts de WAN d'un AUTRE site. + +`OU_LE_LOCATAIRE_LE_DIT` couvrait `dns_amorcage`, `artefacts_amorcage`, +`setops_depot_binaires`, `serveur_ops_forge_amont` et `client_backup_cible`. Pas +`nftables_admin_ssh`. Pas `passerelle_sortie`. + +### Ce que le silence a coute + +Quatorze machines materialisees, le runner insemine — et l'exploitant incapable d'entrer +sur son propre runner pour l'armer. `Connection timed out`, sans qu'aucune garde n'ait rien +eu a dire. La frontiere avait bien pose une regle d'administration, mais sur son interface +**WAN**, puisque c'est ce que le plan declarait. Bonne source, mauvaise porte. + +### Ce qui change + +Les deux entrees manquantes sont ajoutees. `nftables_admin_ssh` etant une **liste**, la +comparaison passe par une normalisation : l'ordre d'une liste de sources ne porte aucun +sens, et comparer `str(['a','b'])` a `str(['b','a'])` ferait crier une difference qui n'en +est pas une. + +Les deux ecosystemes passent de cinq a six intrants compares — le septieme, +`passerelle_sortie`, n'est declare par aucun des deux : il se derive, et la garde le dit +plutot que de l'inventer. + +### La regle, cinquieme occurrence + +**Une liste qui en suit une autre prend du retard.** Le site expose ; la table compare. Deux +listes, et la seconde ne suivait pas. La garde s'ecrit EN MEME TEMPS que la seconde liste, +jamais quand on s'en sert. + ## 2026-09-13 (8) — Le plancher traversait trois modules et se perdait au troisieme Trouve en preparant la creation des quatorze machines de TechnoLibre, une commande avant diff --git a/docs/audit/preuve-2026-09-13.md b/docs/audit/preuve-2026-09-13.md index d15eb6d..a62ef16 100644 --- a/docs/audit/preuve-2026-09-13.md +++ b/docs/audit/preuve-2026-09-13.md @@ -15,7 +15,7 @@ |---|---|---|---|---| | P01 | Lint (ansible-lint) | AFF-006 | ✅ OK |  | | P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK | -| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. | +| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 5 instance(s) verifiee(s) — instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. | | P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | | | P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | | | P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | @@ -33,18 +33,18 @@ | P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 29 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) | | P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 29 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. | | P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. | -| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 5 instance(s) federee(s), aucun index en collision. | +| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 6 instance(s) federee(s), aucun index en collision. | | P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). | | P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 13 reseau(x), aucune collision avec la plage tenant. | | P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. | -| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 48 groupe(s), 85 regle(s). | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 4 tenant(s), 51 groupe(s), 90 regle(s). | | P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). | | P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 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 : 4 pool(s) Proxmox, 40 VM placee(s), aucun nom ni VMID en collision. | +| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 5 pool(s) Proxmox, 44 VM placee(s), aucun nom ni VMID en collision. | | P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 33 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 22, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. | | P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 64 scripts expliques et atteignables, 121 cibles make documentees, 68 roles avec README. | -| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (146 cle(s) declaree(s) par l'instance). | +| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (149 cle(s) declaree(s) par l'instance). | | P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 34 revendication(s) de port, aucune collision entre roles co-localises (36 groupes). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 45 document(s) declarent leur lecteur (39 genere(s) exempte(s)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). | @@ -54,7 +54,7 @@ | P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. | | P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. | | P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 60 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | -| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product | +| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 5 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre | | P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 153 regle(s) du site. | | P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. | | P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). | @@ -62,12 +62,12 @@ | 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 89 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 (127 lignes). | -| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 236 regles `pass`), tous non consignes et tous motives. | +| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 240 regles `pass`), tous non consignes et tous motives. | | P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 | | P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. | | P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 14 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b | | P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. | -| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 0. | +| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 14. | | P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. | | P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (75 preuves, 68 roles, 41 groupes). | | P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. | @@ -85,7 +85,7 @@ | P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. | | P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. | | P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. | -| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 5 intrant(s) du locataire concordent avec ce que le site expose. | +| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 6 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). | | P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. | | P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. | diff --git a/scripts/site_intrants.py b/scripts/site_intrants.py index 230cb46..c2e2321 100644 --- a/scripts/site_intrants.py +++ b/scripts/site_intrants.py @@ -109,9 +109,35 @@ OU_LE_LOCATAIRE_LE_DIT = { "setops_depot_binaires": ("all/10-intrants.yml", "setops_depot_binaires"), "serveur_ops_forge_amont": ("serveur_ops.yml", "serveur_ops_forge_amont"), "client_backup_cible": ("client_backup.yml", "client_backup_cible"), + # CES DEUX-LA MANQUAIENT, ET LE SILENCE A COUTE (2026-09-13). Le site en expose SEPT ; + # cette table n'en comparait que CINQ. Le controle rapportait donc « CONFORME » avec + # un aplomb complet, sur un locataire dont le plan d'administration pointait encore + # les bouts de WAN d'un AUTRE site. + # + # CE QUE CA A COUTE : quatorze machines materialisees, le runner insemine — et + # l'exploitant incapable d'entrer sur son propre runner pour l'armer. `Connection + # timed out`, sans qu'aucune garde n'ait rien eu a dire. + # + # Une liste qui en suit une autre prend du retard : la regle est connue ici, et c'est + # sa cinquieme occurrence. La garde s'ecrit EN MEME TEMPS que la seconde liste. + "nftables_admin_ssh": ("all/10-intrants.yml", "nftables_admin_ssh"), + "passerelle_sortie": ("all/10-intrants.yml", "passerelle_sortie"), } +def _normaliser(v) -> str: + """Une forme comparable, que la valeur soit une chaine ou une liste. + + `nftables_admin_ssh` est une LISTE de CIDR ; les cinq intrants d'origine etaient tous + des chaines, et `str()` suffisait. Comparer `str(['a','b'])` a `str(['b','a'])` ferait + crier une difference qui n'en est pas une — l'ordre d'une liste de sources ne porte + aucun sens ici. + """ + if isinstance(v, (list, tuple)): + return " ".join(sorted(str(x).strip().rstrip("/") for x in v)) + return str(v).strip().rstrip("/") + + def _declare_par_le_locataire() -> dict[str, str]: """Lit, sans Ansible, ce que le locataire monte ecrit a propos de son site.""" # LA RESOLUTION D'INSTANCE EST PARTAGEE, PAS RECOPIEE (P41). La premiere version @@ -126,7 +152,7 @@ def _declare_par_le_locataire() -> dict[str, str]: continue d = yaml.safe_load(f.read_text(encoding="utf-8")) or {} if variable in d: - vu[clef] = str(d[variable]) + vu[clef] = d[variable] return vu @@ -146,9 +172,9 @@ def verifier() -> int: continue # `setops_depot_binaires` se DERIVE legitimement d'`artefacts_amorcage` en Jinja : # une expression n'est pas un retard, c'est le contraire. - if "{{" in vu[clef]: + if "{{" in str(vu[clef]): continue - if vu[clef].rstrip("/") != str(attendu).rstrip("/"): + if _normaliser(vu[clef]) != _normaliser(attendu): ecarts.append(f"{variable} : le locataire dit « {vu[clef]} », " f"le site expose « {attendu} »") if ecarts: