flux : le mot voisins_site, et l'aveu de ce qui manque pour s'en servir

Le registre ne savait pas dire « les autres tenants de ma fabric ». ingress +
externe signifie DEPUIS L'INTERNET : declarer ainsi un cache partage l'aurait
publie au monde.

voisins_site rend les supernets des tenants que CE SITE heberge, en reutilisant
devis_reseau.decouvrir_du_site() plutot qu'en ecrivant un second recensement.
Ma premiere version lisait un `federe` absent comme « non federe » et excluait
Chezlepro et Technolibre en silence -- la decouverte canonique dit l'inverse.

LE CHAINAGE DES CACHES N'EST PAS LIVRE. Aucun generateur ne rend ce mot : zero
regle 3142 au devis de frontiere. Les declarations ont donc ete RETIREES plutot
que laissees a moitie -- un flux declare que personne n'applique est le piege
que ce depot traque.

La difficulte est structurelle : les regles OPNsense s'evaluent sur l'interface
d'ARRIVEE, donc une regle inter-tenant doit etre posee sur l'interface du
VOISIN. Le generateur construit tenant par tenant, sur les interfaces de ce
tenant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-24 17:15:19 -04:00
parent 12d938cdb0
commit 6547eb2fac
4 changed files with 113 additions and 6 deletions

View file

@ -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

View file

@ -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: ""

View file

@ -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 %}

View file

@ -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