# Registre des flux réseau — conception > **Pour qui :** le **mainteneur** qui déclare un flux réseau dans `roles/*/meta/flux.yml`. ## But Set-OPS tient un **registre des flux** (qui parle à qui, sur quel port, dans quel sens, pourquoi) pour **deux usages** : 1. **Générer les règles nftables** de chaque serveur — autoriser les flux déclarés, **tout refuser d'autre** (moindre privilège). Complète la couche *chiffrement* (TLS) par la couche *accès*. 2. **Preuve d'audit** — la matrice d'accès réseau documentée (source → dest : port : chiffrement : raison), exigée par un audit de sécurité et le label de certification. ## Principe : le rôle possède ses flux Comme `meta/empreinte.yml` (dimensionnement) est la propriété du rôle, **`roles//meta/flux.yml`** déclare les flux du logiciel. Le rôle sait sur quoi il écoute et à quoi il se connecte ; personne d'autre. Le registre global se **dérive** de l'ensemble des `flux.yml` des rôles présents sur un nœud. ## Schéma de `meta/flux.yml` ```yaml flux: - sens: ingress # ingress = ce rôle ÉCOUTE ; egress = ce rôle SE CONNECTE port: 5432 # entier ou liste [80, 443] protocole: tcp # tcp | udp pair: [serveur_keycloak, serveur_forgejo] # QUI — voir « Résolution du pair » chiffrement: tls-requis # tls-requis | tls | starttls | ssh | tls-cible | clair | n-a (bonus audit) # tls-requis : chiffré + pair vérifié (verify-full) | tls : chiffré | starttls : mise à niveau opportuniste # ssh : transport SSH (chiffré, hôte vérifié) | tls-cible : TLS visé mais pas encore appliqué (feuille de route) # clair : non chiffré (local ou terminé à l'edge) | n-a : sans objet raison: "Connexions applicatives (verify-full)." # lisible, pour l'audit ``` ### Résolution du `pair` | Valeur | Résout vers | |---|---| | un **rôle/groupe** (`serveur_prometheus`) | les IP des hôtes de ce groupe (via le plan) | | `edge` | le(s) hôte(s) de l'edge (`serveur_nginx`) | | `flotte` | tous les nœuds de l'instance | | `externe` | hors flotte (frontière publique — géré à l'OPNsense, pas dans le nœud) | | `localhost` | boucle locale — aucune règle inter-nœud (nftables autorise `lo`) | | `expositions` | dérivé des `expose:` des applications (cas de l'edge → backends) | | `admin` | les **réseaux** d'administration (intrant `nftables_admin_ssh`) — l'exploitant n'est ni la flotte, ni l'Internet | | `voisins_site` | les **autres tenants fédérés** de la même fabric (leurs supernets) — le voisinage, quatrième chemin d'arrivée | | `fabric` | le **matériel de l'hébergeur** : hyperviseurs, frontière, commutateurs | | `runner_site` | le **runner du site**, seul autorisé à matérialiser des VM | | `derive` | résolu ailleurs que par le nœud (hors périmètre de sa règle) | La résolution `pair → IP` réutilise le **plan** (registre IP/FQDN/zones) déjà en place. > **Quatre de ces mots sont nés d'un piège, et pas d'un besoin de vocabulaire.** > `voisins_site` (2026-08-24) : sans lui, le chaînage des caches d'artefacts n'aurait pu se > déclarer qu'en `ingress` + `externe` — ce qui aurait **publié le cache à l'Internet > entier**. `fabric` (2026-08-25) : le runner de site déclarait son API Proxmox en > `externe`, or « externe » se rend par « tout sauf les espaces privés » et les hyperviseurs > *sont* en RFC 1918 — la règle avait l'air d'ouvrir le flux et l'excluait. Un flux qui a > l'air ouvert et qui ne l'est pas est pire qu'un flux fermé : il ne se cherche pas. > > Les cinq derniers ne rendent **pas des hôtes** : `admin`, `voisins_site`, `fabric` et > `runner_site` rendent des CIDR ou des sources extérieures à l'écosystème, `derive` ne rend > rien. Ils sont donc traités à part dans `scripts/resoudre_flux.py`. ## Génération Un **résolveur** (miroir de `instancier`) agrège, par serveur, les `flux.yml` de tous ses rôles (services + intégrations), résout les `pair`, et produit : - **`nftables`** : ruleset par serveur (allow des flux résolus, `policy drop` par défaut), consommé par le rôle `nftables_baseline` ; - **le registre d'audit** : `docs/registre-flux.md` (généré) + une cible `make flux`. ## Activation prudente ### Descriptions conservées par les pare-feux La `raison` non vide du flux et son rôle alimentent les champs natifs : `comment` pour nftables et Proxmox, `description` pour le filtrage OPNsense (`descr` pour ses redirections). Un commentaire de fichier `# ...` ne survit pas au chargement nftables. Les règles communes (administration, connexions établies, refus) portent aussi leur motif. Les libellés sont sur une ligne. Le commentaire nftables est borné à 127 octets UTF-8 ; OPNsense garde sa clé `setops:...` complète et borne le texte qui suit pour rester dans 255 octets. Une raison abrégée reste disponible en entier dans le registre des flux. Le commentaire Proxmox conserve aussi le pair nommé dans le devis. Une correction de description apparaît dans `make proxmox-fw-plan` et `make frontiere-plan`. Son application met à jour le champ sur la règle existante, puis le relit : elle ne recrée pas la règle et ne modifie ni son activation ni sa politique. Les confirmations habituelles restent requises pour appliquer ces devis. `make test` vérifie notamment cette convergence et les limites des champs. Références des champs natifs : [nftables](https://netfilter.org/projects/nftables/manpage.html), [API Proxmox](https://github.com/proxmox/pve-firewall/blob/master/src/PVE/API2/Firewall/Rules.pm), [descriptions OPNsense](https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/DescriptionField.php). ### Activer le filtrage Activer nftables = **action destructive** (peut couper l'accès) → confirmation explicite + déploiement graduel (garder l'accès SSH/Ansible, tester par nœud). > **Le pare-feu est armé sur la flotte depuis.** Ce paragraphe disait « nftables reste > *préparé mais non activé* tant que le registre n'est pas complet et validé ». Le registre > **est** complet — **P49** vérifie qu'il reproduit exactement ce que les `meta/flux.yml` > déclarent — et `nftables_baseline_enabled` vaut `true` pour `hotes_actifs` dans le plan. > Le rôle, lui, garde `false` par défaut : le pare-feu n'est pas une propriété du rôle, > c'est une décision de l'instance. Le gabarit doré reste à `false`, et c'est voulu. ## Séquence — franchie 1. ✅ figer le schéma (ce doc) + **piloter** sur postgresql / client_metrique / nginx ; 2. ✅ le **résolveur** (`scripts/resoudre_flux.py`) — agrégation → règles + registre ; 3. ✅ **remplir** tous les rôles (transcription du travail zéro-confiance) ; 4. ✅ **générer** + registre d'audit (`docs/registre-flux.md`, gardé par **P49**) ; puis **activer** nftables nœud par nœud — fait. Ce qui a suivi la séquence n'y était pas prévu : le même registre alimente désormais aussi le **pare-feu est-ouest de l'hyperviseur** (`make proxmox-fw-plan`, garde **P45**) et la **frontière nord/sud OPNsense** (`make frontiere-plan`). Trois couches, une seule décision — elles ne peuvent pas se contredire parce qu'elles dérivent de la même source. ## `poste: false` — un service publié qui ne s'adresse pas à un humain Ajouté le 2026-08-09. Ne concerne que les flux `ingress` dont le pair inclut `externe`, c'est-à-dire les services publiés. La frontière étend ces services au **VLAN d'administration** : le poste de l'exploitant y est, et c'est de là qu'il ouvre ses consoles web ou son client de courriel. Mais tous les services publiés ne s'adressent pas à un humain — le `25` entrant de Postfix est un flux *serveur à serveur*, les MX distants. | Situation | Exemple | `poste` | |---|---|---| | un poste de travail s'y connecte | `serveur_nginx` 443, `serveur_dovecot` 993 | absent (défaut `true`) | | flux serveur à serveur uniquement | `serveur_postfix` 25 entrant | `false` | Sans ce mot-clé, la frontière autorisait `admin → 25` que le `nftables` de l'hôte refusait : deux couches déclarant deux politiques différentes. Mesuré par `make frontiere-mesurer`, qui distingue précisément ce cas d'une vraie fuite. Le mot-clé vit dans `meta/flux.yml`, avec le rôle **qui sait ce que son port veut dire**. Le générateur de la frontière, lui, ne connaît aucun numéro de port. ## `partage:` — décrire une écoute plutôt que l'ouvrir Ajouté le 2026-08-09 avec **P33**, qui refuse que deux rôles co-localisés revendiquent le même port. Le registre confondait deux situations : | Situation | Exemple | `partage` | |---|---|---| | le rôle **ouvre** l'écoute | `serveur_dovecot` lie 12345 (SASL réseau) | absent | | le rôle **décrit** celle d'un autre | `serveur_backup` emprunte le sshd de `serveur_debian` | `true` | Sans cette distinction, la seule co-location légitime de la flotte — `tcp/22` sur le dépôt de sauvegarde, `site-backup-01` depuis que les écosystèmes déposent chez leur hébergeur — serait signalée à tort. Une preuve qui crie sur un cas sain finit par être ignorée, ce qui est pire que de ne pas l'avoir. **Corollaire à retenir** : un port qu'on **subit** (le défaut amont d'un logiciel) doit être imposé et déclaré comme les autres. Celui d'Alloy ne l'était pas, et c'est la seule raison pour laquelle la collision a pu durer des semaines.