diff --git a/CHANGELOG.md b/CHANGELOG.md index d9d7316..a5d29d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # CHANGELOG — Set-OPS +## 2026-08-24 — `voisins_site` : le vocabulaire d'un flux entre tenants, et le mur qu'il révèle + +Le registre des flux connaissait `flotte` (mon écosystème), `edge`, `admin` et `externe`. +Il ne savait pas dire **« les autres tenants de ma fabric »** — et `ingress` + `externe` +signifie *depuis l'Internet* : déclarer ainsi un cache partagé l'aurait **publié au monde**. + +`voisins_site` existe maintenant dans `resoudre_flux`. Il rend des CIDR — les supernets +des tenants que **ce site** héberge — et réutilise `devis_reseau.decouvrir_du_site()` +plutôt que d'en écrire un second recensement. Deux listes de tenants finiraient par +diverger, et la divergence se lirait « tout va bien ». + +Vérifié : depuis patient 0, il rend `10.17.0.0/16` et `10.23.0.0/16`. Le lab, `federe: +false`, est correctement exclu. + +**Une faute évitée de justesse.** Ma première version lisait un `federe` absent comme +« non fédéré » et excluait Chezlepro et Technolibre — leur nomenclature est antérieure à +cette clé. `devis_reseau` dit `n.get("federe", True)` : **l'absence vaut fédéré**. Réutiliser +sa découverte plutôt que d'en réécrire une a fermé le piège au passage. + +### Ce qui n'est PAS fait, et pourquoi + +Le chaînage des caches — chaque écosystème garde le sien, qui prend celui de l'hébergeur +comme amont, pour que Debian ne soit téléchargé qu'une fois par site — **n'est pas livré**. + +Le vocabulaire est accepté, mais **aucun générateur ne le rend** : `make frontiere-plan` +produit zéro règle pour le port 3142 entre tenants. Un flux déclaré que personne +n'applique est précisément le piège que ce dépôt traque, alors les déclarations ont été +retirées plutôt que laissées à moitié. + +La difficulté est structurelle, pas cosmétique. Les règles d'OPNsense s'évaluent sur +l'**interface d'arrivée** : un paquet venant de Chezlepro vers le cache de patient 0 arrive +sur l'interface *de Chezlepro*, donc la règle doit être posée **là**. Or le générateur +construit ses règles tenant par tenant, sur les interfaces de ce tenant. Émettre des règles +croisées change la forme de ce qu'il produit — et ses propres commentaires répètent qu'une +règle posée sur la mauvaise interface ne correspond jamais à un paquet. + +Ce qui reste en place : le mot `voisins_site`, éprouvé ; et `serveur_artefacts_amont`, la +capacité de chaînage côté rôle. Il manque le rendu, dans les deux générateurs de pare-feu. + ## 2026-08-24 — Un résolveur par tenant, et non plus un par machine `client_unbound` posait un Unbound sur **chaque VM**. C'était étanche, et c'était N démons diff --git a/roles/serveur_artefacts/defaults/main.yml b/roles/serveur_artefacts/defaults/main.yml index 8959a5b..cba2adc 100644 --- a/roles/serveur_artefacts/defaults/main.yml +++ b/roles/serveur_artefacts/defaults/main.yml @@ -45,3 +45,21 @@ serveur_artefacts_hors_ligne: false # Journalisation : apt-cacher-ng tient un journal des requêtes servies. C'est lui qui # permet de répondre à « ce paquet, l'avons-nous servi ou est-il venu du dehors ? ». serveur_artefacts_journal: "/var/log/apt-cacher-ng" + +# --- LE CHAINAGE DES CACHES (2026-08-24) -------------------------------------- +# +# Chaque écosystème garde SON cache, et celui-ci prend le cache de l'hébergeur comme +# amont. Debian n'est alors téléchargé qu'UNE FOIS pour tout le site : +# +# VM du tenant -> cache du tenant -> cache du SITE -> Debian +# +# POURQUOI PAS UN CACHE UNIQUE OÙ TOUTES LES VM TAPENT. Il aurait fallu ouvrir un flux de +# chaque machine de chaque tenant vers un hôte d'un autre tenant — N×M chemins à travers +# le default-deny. Avec le chaînage il n'y en a qu'UN par écosystème, entre deux caches. +# +# Et l'étanchéité y gagne : le cache du site ne voit que des requêtes AGRÉGÉES, jamais +# quelle machine installe quoi. +# +# Vide = ce cache va directement à l'amont Debian. C'est le cas du cache du SITE lui-même, +# et de tout écosystème qui s'est émancipé (docs/filiation-emancipation.md). +serveur_artefacts_amont: "" diff --git a/roles/serveur_artefacts/templates/zzz-setops.conf.j2 b/roles/serveur_artefacts/templates/zzz-setops.conf.j2 index 488b731..8f81f25 100644 --- a/roles/serveur_artefacts/templates/zzz-setops.conf.j2 +++ b/roles/serveur_artefacts/templates/zzz-setops.conf.j2 @@ -17,3 +17,9 @@ Offlinemode: 1 {% else %} Offlinemode: 0 {% endif %} + +{% if serveur_artefacts_amont %} +# CHAÎNAGE : ce cache passe par celui de l'hébergeur plutôt que d'aller chez Debian. +# Debian n'est ainsi téléchargé qu'une fois pour tout le site. +Proxy: {{ serveur_artefacts_amont }} +{% endif %} diff --git a/scripts/resoudre_flux.py b/scripts/resoudre_flux.py index 705dfa7..29bc059 100644 --- a/scripts/resoudre_flux.py +++ b/scripts/resoudre_flux.py @@ -41,7 +41,20 @@ CHIFFREMENT = {"tls-requis", "tls", "starttls", "ssh", "tls-cible", "clair", "n- # `admin` : les RESEAUX d'administration (intrant `nftables_admin_ssh`), pas des # hotes. C'est le troisieme chemin d'arrivee, distinct de `flotte` (le tenant) et # d'`externe` (Internet) — l'exploitant n'est ni l'un ni l'autre. -MOTS_PAIR = {"edge", "flotte", "externe", "localhost", "expositions", "derive", "admin"} +# `voisins_site` : les autres tenants FEDERES de la meme fabric. Ni `flotte` (mon +# ecosysteme), ni `externe` (l'Internet) — un quatrieme chemin d'arrivee, celui du +# VOISINAGE. +# +# POURQUOI CE MOT EXISTE (2026-08-24). Le cache d'artefacts se CHAINE : chaque ecosysteme +# garde le sien, et celui-ci prend le cache de l'hebergeur comme amont. Debian n'est alors +# telecharge qu'une fois pour tout le site. Sans ce mot, la seule facon de declarer ce flux +# aurait ete `ingress` + `externe` — qui signifie « depuis l'Internet » et aurait PUBLIE le +# cache au monde entier. +# +# Il rend des CIDR (les supernets des voisins), pas des adresses d'hotes : il est donc +# traite a part, comme `admin`. +MOTS_PAIR = {"edge", "flotte", "externe", "localhost", "expositions", "derive", "admin", + "voisins_site"} GROUPE_EDGE = "serveur_nginx" @@ -205,10 +218,10 @@ def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict for p in (pair if isinstance(pair, list) else [pair]): if p in ("localhost", "externe", "expositions", "derive"): continue # lo (auto), frontiere OPNsense, ou hors perimetre noeud - if p == "admin": - # `admin` n'est pas un groupe d'hotes mais des RESEAUX : l'intrant - # `nftables_admin_ssh`, deja source unique de la garde anti-lockout. - # Il est traite a part parce qu'il rend des CIDR, pas des adresses. + if p in ("admin", "voisins_site"): + # Ni l'un ni l'autre n'est un groupe d'hotes : ce sont des RESEAUX. `admin` + # vient de l'intrant `nftables_admin_ssh` ; `voisins_site` des supernets des + # autres tenants federes. Traites a part, plus bas. continue if p == "flotte": noms |= actifs_noms @@ -216,11 +229,42 @@ def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict groupe = GROUPE_EDGE if p == "edge" else p noms |= {h for h in _hotes_du_groupe(data, groupe) if h in actifs_noms} srcs = {ip_par_hote[h] for h in noms if h in ip_par_hote} - if "admin" in (pair if isinstance(pair, list) else [pair]): + demandes = pair if isinstance(pair, list) else [pair] + if "admin" in demandes: srcs |= set(_sources_admin_ssh()) + if "voisins_site" in demandes: + srcs |= set(_supernets_voisins()) return sorted(srcs) +def _supernets_voisins() -> list[str]: + """Les supernets des AUTRES tenants que CE SITE heberge. + + ON REUTILISE `devis_reseau.decouvrir_du_site()`, on n'en ecrit pas une deuxieme + version. C'est la lecon de P41 appliquee ici : deux recensements de tenants finiraient + par diverger, et la divergence se lirait « tout va bien ». + + Elle porte aussi la bonne semantique. « Voisin » veut dire *sur ma fabric*, pas + *quelque part dans la federation* : un tenant pose sur un autre site n'est joignable + par aucune regle de cette frontiere-ci, et lui ouvrir un flux serait un chemin qui ne + correspondra jamais a un paquet. + + Elle porte enfin la regle du `federe` ABSENT = federe (devis_reseau, ligne 568). Ma + premiere version lisait l'absence comme « non federe » et excluait Chezlepro et + Technolibre en silence — leur nomenclature est anterieure a cette cle. + """ + try: + import devis_reseau + from inventory_rules import supernet_de + tenants = devis_reseau.decouvrir_du_site() + except Exception: + # Sans underlay designe (ex. `make flux` hors site), il n'y a pas de voisinage a + # resoudre. Rendre [] plutot qu'echouer : l'absence de voisins n'est pas une faute. + return [] + moi = INSTANCE.resolve().name + return sorted({supernet_de(int(n["index"])) for nom, _p, n in tenants if nom != moi}) + + # Codes ICMP de destination inatteignable qu'un flux peut nommer dans `port`. L'ICMP n'a # PAS de port : il a un type et un code. Emettre `icmp dport frag-needed` produit un jeu # que `nft` refuse — et le service ne demarre plus du tout, donc l'hote perd sa barriere