diff --git a/docs/audit/preuve-2026-08-09.md b/docs/audit/preuve-2026-08-09.md index 86f6692..c4402b7 100644 --- a/docs/audit/preuve-2026-08-09.md +++ b/docs/audit/preuve-2026-08-09.md @@ -36,7 +36,7 @@ | P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 2 instance(s) federee(s), aucun index en collision. | | P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (20 sections). | | P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 6 reseau(x), aucune collision avec la plage tenant. | -| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 31 regles, 2 routes, admin=10.0.0.0/24,192.168.254.2/32,192.168.255.2/32. | +| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 31 regles, 12 routes, admin=10.0.0.0/24,192.168.254.2/32,192.168.255.2/32. | | P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 38 groupe(s), 64 regle(s). | | P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 4 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 | 0 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. | diff --git a/scripts/devis_opnsense.py b/scripts/devis_opnsense.py index 9eaa53e..c2c3ea2 100644 --- a/scripts/devis_opnsense.py +++ b/scripts/devis_opnsense.py @@ -35,7 +35,7 @@ import yaml RACINE = Path(__file__).resolve().parents[1] sys.path.insert(0, str(RACINE / "scripts")) -from inventory_rules import supernet_de # noqa: E402 +from inventory_rules import sous_reseau_de, supernet_de # noqa: E402 # Helpers internes du resolveur de flux : reutilises VOLONTAIREMENT plutot que # redupliques ici — le registre des flux et l'inventaire doivent avoir une seule # lecture, sinon le devis et les nftables divergeraient en silence. @@ -392,16 +392,33 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: intrants = intrants_frontiere() transit = transit_underlay() saut = prochain_saut(transit) + # UNE ROUTE PAR SOUS-RESEAU REELLEMENT ATTRIBUE, et non une par supernet. + # + # Router le /16 entier faisait porter a la frontiere des destinations qui n'existent + # nulle part. Ces paquets atteignaient le noeud de sortie, y arrivaient dans la table + # PRINCIPALE — pas dans le VRF, qui n'est atteint que par les /24 annonces en BGP — et + # repartaient vers la passerelle du reseau d'ADMINISTRATION. Mesure du 2026-08-09 : + # `ip route get 10.27.99.99` rendait `via 192.168.11.254`. + # + # C'est aussi ce qui faisait reussir tout `connect()` depuis le VLAN d'administration, + # y compris vers des adresses inexistantes — symptome que nous avons attribue pendant + # deux jours a une fonction d'anti-usurpation de la frontiere, alors que c'etait un + # routage trop large. + # + # La frontiere ne route desormais QUE ce qui existe. Le NAT, lui, reste sur le supernet + # (alias `SETOPS_TENANT_*`) : il porte sur la SOURCE, et le supernet contient tous les + # sous-reseaux — la garde P24 reste donc satisfaite. + _sdn = underlay_mod.routage_tenants(underlay_mod.charger()) == "sdn" routes = [ { - "reseau": supernet_de(n["index"]), + "reseau": sous_reseau_de(n["index"], int(z)), "prochain_saut": saut, - "description": f"{nom} (index {n['index']}) — routage inter-zone " - + ("dans la zone EVPN du tenant" - if underlay_mod.routage_tenants(underlay_mod.charger()) == "sdn" - else "sur les switches L3"), + "description": f"{nom} — {cat.get('libelle', f'zone{z}')} (zone {z}, index " + f"{n['index']}) — " + ("zone EVPN du tenant" if _sdn + else "switches L3"), } for nom, _pfx, n in tenants + for z, cat in sorted((n.get("categories") or {}).items(), key=lambda x: int(x[0])) ] # NAT sortant : une regle par tenant, source = son alias de supernet, cible = l'adresse