# CHANGELOG — Set-OPS ## 2026-08-03 (suite 11) — le cluster appartient à l'hébergeur En ouvrant le panneau « Intrants de base », on trouvait côte à côte et sans distinction des valeurs du **tenant** (son domaine, son realm, son modèle) et des valeurs de l'**hébergeur** (son cluster, sa frontière, sa fabric). Deux propriétaires, deux dépôts, deux cycles de vie — et rien à l'écran ne le disait. Trois sections sur sept étaient déjà chez l'hébergeur (Frontière, Fabric). **La section Proxmox, elle, ne l'était pas** — alors qu'un cluster est du matériel possédé par l'hébergeur au même titre que ses commutateurs. ### La recopie avait déjà divergé Même cluster, deux inventaires contradictoires : ``` Chezlepro stockages [TrueNAS, CephHDD, CephNVMe] ponts [vmbr3] Technolibre stockages [local-lvm, TrueNAS] ponts [vmbr1, vmbr2] ``` Rien ne « cassait » : ces listes ne peuplent que des menus déroulants. Mais un opérateur plaçant une VM Technolibre ne se voyait jamais proposer `CephNVMe`, et ça n'était la décision de personne. Le fichier de l'hébergeur prend l'**union** des deux — aucune n'était complète, et choisir l'une aurait été arbitraire. **À confirmer contre le cluster réel.** ### Le partage retenu **Hébergeur** (`/proxmox-hebergeur.yml`, à côté d'`underlay.yml`) : `proxmox_api_host`, `_user`, `_port`, `_validate_certs`, `proxmox_noeuds`, `_stockages`, `_ponts`. **Tenant** (`group_vars/proxmox.yml`) : son **golden template** — chaque tenant a le sien — et ses **défauts de placement** (nœud, stockage, pont). Ce sont des choix faits *à l'intérieur* de ce que l'hébergeur offre. Le chemin se **dérive** du symlink `underlay.yml`, qui désigne déjà l'hébergeur : rien de nouveau n'est déclaré (D-17 tenue). Sans underlay monté, tout retombe dans le fichier du tenant — un dépôt autonome fonctionne exactement comme avant. ### Ce que ça a demandé de moins que prévu Les playbooks chargent ces fichiers **par chemin explicite** (`include_vars`), pas par appariement de groupe Ansible — il n'existe d'ailleurs aucun groupe `proxmox` dans l'inventaire. Une tâche `stat` + `include_vars` de plus a suffi ; aucun symlink dans `group_vars`, aucune génération. Vérifié en exécution réelle : depuis Technolibre (tenant actif), la dérivation résout vers `OPS-Chezlepro/proxmox-hebergeur.yml` et charge `asgard` + les quatre stockages. ### Le panneau nomme désormais le propriétaire Chaque section porte une pastille **tenant** (bleu) ou **hébergeur** (ambre), avec en infobulle ce que ça implique : éditer une section « hébergeur » vaut pour **tous** ses tenants. Le schéma d'intrants porte un champ `proprietaire` — c'est la donnée qui manquait, pas l'affichage. **P27** garde la séparation : aucune clé de l'hébergeur ne peut réapparaître dans un `group_vars` de tenant. Sans elle, le premier `make config` lancé d'un autre poste recommençait la recopie. Décisions **D-35** et **D-36**, affirmation **AFF-109**. ### Une chose à trancher `modeleChezlepro` reste déclaré comme golden template de **Technolibre**. Ta décision — un modèle par tenant — le rend incorrect, mais le corriger suppose qu'un `modeleTechnolibre` existe réellement sur le cluster. Laissé tel quel, signalé ici. ## 2026-08-03 (suite 10) — les intégrations universelles cessent d'être recopiées Le plan portait **57 lignes d'intégration écrites à la main**. Le décompte est sans appel : `client_metrique` 14/14, `client_journal` 14/14, `client_pki` 13/14 — mais `client_backup` 7/14, `client_smtp` 8/14, `client_unbound` 1/14. **28 de ces 57 lignes disaient oui à quelque chose de vrai pour tout le monde.** Elles n'existaient donc que pour être oubliées une vingt-neuvième fois — et elles l'avaient été : dans Chezlepro, `backup-01` et `infra-pki-01` n'étaient **ni supervisés, ni journalisés, ni certifiés**. Rien ne l'aurait signalé, puisqu'une machine non supervisée ne proteste pas. ### Ce qui change Le **rôle** déclare sa politique, une fois, dans `roles//meta/integration.yml` : ```yaml integration: universelle: true raison: "Tout hôte est mesuré. Une machine hors supervision tombe sans que personne ne l'apprenne." sauf_role: serveur_step_ca # l'AC ne s'enrôle pas auprès d'elle-même ``` Le plan ne porte plus que les intégrations qui sont **un vrai choix** — sauvegarde, courriel, résolveur. Et il **refuse** désormais une recopie : deux sources finiraient par diverger, et surtout, l'absence de `client_metrique` en face d'un serveur se lirait « non supervisé » alors qu'il l'est. ### L'exemption suit le service, pas le nom d'hôte `sauf_role: serveur_step_ca` retire `client_pki` à l'hôte qui *rend* le service. Déplacez step-ca sur une autre machine et l'exemption suit toute seule. Un nom d'hôte en dur, lui, aurait laissé la nouvelle AC s'enrôler auprès d'elle-même et l'ancienne sans certificat. ### Une seule fonction de résolution `inventory_rules.integrations_de()` est lue par les **trois** consommateurs — inventaire, voûte et panneau. La voûte en particulier : sans elle, les secrets des intégrations universelles auraient cessé d'être exigés et **P18 serait passé au vert sur une voûte incomplète**. ### Vérification Sur Technolibre, `make instancier` donne **diff vide** : la politique reproduit exactement ce que les 41 lignes retirées produisaient. Sur Chezlepro, elle produit précisément les trois groupes manquants sur `backup-01` et les deux sur `infra-pki-01` — et **pas** `client_pki` sur ce dernier, l'exemption ayant joué. Le trou se referme, rien d'autre ne bouge. **P26** garde les deux côtés : aucun hôte sans intégration universelle, aucune recopie dans le plan. Décisions **D-33** et **D-34**. ### Ce que le panneau montre Dans la fiche d'un serveur : les universelles en ✓ non décochables, les exemptions barrées, chacune avec sa raison en infobulle. Sans cet affichage, un plan devenu silencieux se serait lu comme une flotte non supervisée — l'inverse exact de la vérité. ### Et une vue **Intégrations** : la matrice L'autre axe manquait, et c'est celui qui aurait servi. La fiche montre les intégrations **d'un serveur** ; savoir qui n'a pas de sauvegarde demandait d'ouvrir les quatorze. Le trou de Chezlepro n'a d'ailleurs **pas** été trouvé par le panneau — il est sorti du devis de pare-feu Proxmox, qui énumère les rôles par hôte. L'information était à l'écran, répartie sur quatorze clics, donc invisible. La matrice serveurs × intégrations : colonnes ✓ vertes pour la politique, `—` barré pour les exemptions, cases à cocher pour les facultatives — **éditables sur place**, en-têtes et colonne de noms figées, clic sur un nom pour ouvrir sa fiche. La ligne **couverture** affiche `n/N` sans juger : `7/14` sur `client_backup` peut être exactement juste. Elle rend le motif visible ; décider s'il s'agit de choix ou d'oublis reste au lecteur. Sur Technolibre, elle affiche **41 ✓ et une exemption** — soit très exactement les 41 lignes retirées du plan et le `client_pki` de l'AC. ## 2026-08-03 (suite 9) — le SSH inter-nœud était perdu En éclatant les règles par rôle source, un défaut de ma première version est apparu : je **sautais le flux entier** dès qu'un de ses pairs valait `externe`. Or le SSH du socle est déclaré `pair: [flotte, externe]`. La moitié `externe` relève bien de la frontière — mais la moitié **`flotte`**, le SSH entre hôtes, celui d'Ansible, était perdue. Sous une politique `DROP`, **plus aucun hôte n'aurait été joignable en SSH depuis l'intérieur**. Même piège pour le SMTP interne de Postfix, déclaré `[externe, client_smtp]`. `externe` est désormais sauté **pair par pair**, jamais le flux entier. 36 groupes, 56 règles. ### Ajouté — ce qui n'a aucune règle entrante, et pourquoi Onze rôles portés n'ont aucune règle entrante : sous `DROP`, ils sont injoignables. C'est voulu dans les onze cas — la boucle locale pour Prometheus, Redis, rspamd, Icinga et Unbound, la frontière seule pour nginx, aucun service pour `serveur_durci` et les clients. Le devis les **nomme avec leur motif** au lieu de laisser un lecteur le vérifier lui-même. Et un douzième motif existe, marqué `/!\` : « flux entrants déclarés mais aucune source résolue ici » — celui-là serait un vrai trou. ## 2026-08-03 (suite 8) — le pare-feu Proxmox, troisième lecture du même registre `make devis-proxmox-fw` (**preuve P25**). Le filtrage est-ouest intra-tenant est désormais dérivé pour l'hyperviseur : **34 groupes de sécurité, 40 règles, 2 tenants** — depuis les 57 flux intra-tenant que le registre connaissait déjà. Décidé : la défense est **en profondeur**, pas en remplacement. L'hyperviseur filtre, puis l'hôte destinataire filtre à nouveau. Une VM compromise doit franchir les deux. Le coût de maintenance est nul : les deux barrières lisent le registre **par les mêmes fonctions** (`_resoudre_sources`, `_pairs`, `_hotes_du_groupe`) — la duplication est dans l'application, jamais dans la décision. Un IPSet par rôle porte les membres, les groupes de sécurité y renvoient : ajouter un hôte à un rôle met à jour toutes les règles qui l'autorisent, en un seul endroit. ### La garde qui manquait à ma première version Proxmox limite un nom de groupe à **18 caractères**. Ma première version tronquait sans vérifier : deux rôles tronqués au même nom auraient **fusionné leurs règles**, donnant à une VM les autorisations d'un rôle qu'elle ne porte pas — silencieusement. Le préfixe porte maintenant l'**index** (`t17-`) plutôt que l'étiquette (`chez17-`), ce qui rend la troncature bien plus rare, et une garde **échoue** sur toute collision plutôt que d'émettre un devis pareil. Exercée. ### Unifié — un seul schéma de nommage dans le devis Les IPSets portaient l'étiquette longue (`chez17_serveur_postgresql`), les groupes l'index court (`t17-srv-postgresql`) : deux conventions à lire dans un même document. Tout porte désormais le préfixe `t-` et la même forme abrégée de rôle. La troncature reste **propre à chaque objet** : Proxmox est large sur les IPSets, étroit (18 caractères) sur les groupes. Un nom peut donc être entier d'un côté et abrégé de l'autre — chacun respecte sa contrainte, et le préfixe reste commun. Vérifié : aucune collision d'IPSet, et **tout renvoi `+X` d'une règle pointe vers un IPSet qui existe** — 58 IPSets, 34 groupes, aucun orphelin. ### Corrigé — des listes d'adresses en dur, et 48 IPSets inutilisés Six règles par tenant portaient **quatorze adresses en dur** : les mots-clés `flotte` et `edge` n'avaient pas droit à un IPSet, seuls les rôles en avaient. `flotte` en reçoit un désormais, et `edge` renvoie à celui de nginx. **36 des 40 règles** se lisent maintenant `-source +t17-…`. Et le devis listait **28 à 30 IPSets par tenant dont la moitié n'était référencée nulle part** : un opérateur en aurait créé 58 pour n'en utiliser qu'une douzaine. Seuls les IPSets réellement référencés sont émis — **6 par tenant**. Un devis crée ce qu'il liste. ### Puis : une règle par rôle source Les quatre règles restées en liste explicite sont éclatées — un flux dont le pair nomme quatre rôles donne quatre règles, chacune renvoyant à l'IPSet de son rôle. **Plus une seule adresse en dur : 52 règles, toutes par IPSet.** Le gain n'est pas cosmétique : une règle porte désormais **qui** elle autorise. `-source +t17-srv-keycloak` se lit ; `-source 10.27.16.21,10.27.17.11,10.27.19.31,10.27.20.21` demande de retrouver à qui appartient chaque adresse. La raison, elle, appartient au **flux** et non à chacune de ses règles : elle est écrite une fois au-dessus du paquet qu'elle explique, au lieu d'être répétée quatre fois. ### Corrigé — l'affectation variait selon l'état du tenant Elle partait de `hotes_actifs`, avec un repli sur « tous » quand il n'y en avait aucun. Deux tenants donnaient donc deux comportements : Technolibre listait ses 14 VM (zéro actif → repli), Chezlepro **une seule** (un actif). Un opérateur aurait lu qu'une seule VM avait besoin de règles. Les IPSets et les groupes incluaient déjà les hôtes **planifiés**, délibérément — un pare-feu se prépare avant que la VM existe. L'affectation suit désormais la même règle : 14 de chaque côté, toutes avec leur VMID. ### Conséquence à retenir Puisque **tout** ce qui entre dans un tenant passe par la frontière, le contrôleur Ansible aussi. **L'OPNsense devient un prérequis de déploiement**, pas une étape parmi d'autres : sans lui, plus rien ne se déploie. ## 2026-08-03 (suite 7) — le modèle déclarait un MTU que le matériel n'a pas En préparant le déplacement des adresses de VTEP, la lecture des interfaces a montré deux choses que le modèle ignorait. ### `underlay.yml` annonçait 9000, `vmbr3` est à 1500 La garde P23 validait donc **une déclaration fausse** : elle exigeait ≥ 1550 et passait parce que le fichier disait 9000. **Une garde qui valide une déclaration plutôt qu'une réalité donne un faux confort** — c'est pire qu'une garde absente, qui au moins n'endort personne. Le seuil ne peut pas non plus être fixe : 1550 aurait rejeté à tort un transport à 1500 portant un overlay à 1450, qui tient exactement. Il **dérive** désormais d'un `mtu_overlay` déclaré (1450 par défaut) : transport ≥ overlay + 50. Le fichier dit maintenant la vérité — 1500 — et la garde passe pour la bonne raison. Exercé : un overlay porté à 1500 sur ce transport est refusé. ### `vmbr3` n'est pas *VLAN-aware* Pas de `bridge_vlan_aware`, contrairement à `vmbr2`. L'adresse du VTEP y est **non étiquetée** : elle vit dans le VLAN natif du port de commutateur. Déplacer le VTEP vers l'underlay n'est donc pas un changement d'adresse — il faut soit un VLAN natif 10, soit une interface étiquetée dédiée (`bond3.10`). C'est la raison pour laquelle le déplacement n'a **pas** été effectué : le geste demandé suppose une décision de câblage qui n'est pas prise. ## 2026-08-03 (suite 6) — les hyperviseurs entrent au modèle, à leur adresse cible Redresser les pairs EVPN vers l'underlay suppose d'abord que les hyperviseurs **existent dans le modèle**. Ils n'y étaient pas. ### La reconnaissance a montré la cause `vmbr3` — la nouvelle interface 2,5G — porte `10.27.19.{41,43,47}` sur les trois nœuds : l'adresse des VTEP est prise **dans le supernet de Chezlepro**. Le transport du cluster dérive donc de l'index d'un tenant, et une VM de sa zone *Services-infra* partage son sous-réseau avec les trois VTEP. Le modèle **refuse d'exprimer cet état** : déclarer `10.27.19.0/24` en underlay ferait échouer **P23**. La garde écrite deux jours plus tôt détecte la faute avant qu'on ne la documente. `underlay.yml` déclare donc `asgard`, `gandalf` et `vishnu` à leur adresse **cible** `10.0.0.{41,43,47}` — dernier octet conservé, comme sur `vmbr0`. ### Ajouté — un `role` sur les hôtes de l'underlay `switch` (défaut), `hyperviseur`, `frontiere`. Le réseau ne suffit pas à le déduire : un hyperviseur partage le réseau de management avec les commutateurs, et **recevait une configuration de commutateur** en partie B du devis dès qu'on le déclarait. ### Corrigé — une « source unique » qui n'en était pas une `switches_acces()` avait été introduite comme *la* décision du « qui est un switch d'accès », utilisée pour les rayons de l'étoile. Mais `partie_acces()` avait **gardé sa copie locale** du filtre et ne l'appelait jamais. Les deux ont divergé au premier hôte non-commutateur déclaré. Écrire « source unique » dans un commentaire ne la crée pas. ### Deuxième blocage signalé, non corrigé `vishnu` : son `vmbr3` n'a **aucun port physique**. Le pont existe, porte une adresse, ne mène nulle part. Un pair VXLAN pointé sur lui ne fonctionnera jamais — c'est du câblage, pas de la configuration. ## 2026-08-03 (suite 5) — l'ICMP entre au registre, parce que l'overlay descend à 1450 Décision : l'**overlay EVPN plafonne à 1450**. Elle a une conséquence qui ne se voit pas — sous 1500, tout ce qui traverse la frontière dépend de la **découverte de MTU de chemin**, donc de l'ICMP « fragmentation nécessaire ». Or le registre des flux ne connaissait que **TCP et UDP**. Ce message ne pouvait pas être déclaré, et la bordure en `block in log all` l'aurait jeté. Symptôme : la connexion s'établit, les petites requêtes passent, **les grosses réponses restent suspendues** — la panne la plus coûteuse à diagnostiquer, et celle qu'on impute d'abord à l'application. `protocole: icmp` est admis ; pour lui, le champ `port` porte le **type** (`frag-needed`). Le socle déclare les **deux sens** : entrant pour qu'un distant puisse nous demander de réduire nos paquets, sortant pour que nos hôtes signalent l'overlay aux correspondants. Vérifié : les nftables d'hôte sont **inchangés** — le pair `externe` reste sauté, ces flux relèvent de la bordure. Le devis frontière passe à 26 règles. ### Reconnaissance de l'existant (lecture seule) L'EVPN est déjà **à moitié construit** sur le cluster : Proxmox 8.4.19, contrôleur `EVPN0017` (ASN 65000), zones `VRF0011` et `VRF0017` — un VRF par tenant, VNI égal à l'index, conforme à D-08. Mais **aucun VNet** et **aucun nœud de sortie** : le plan de contrôle existe, le plan de données non. Signalé, non corrigé : les **pairs BGP sont `10.27.19.41/.43/.47`**, dans le sous-réseau *Services-infra de Chezlepro*. Le transport du cluster dérive donc de l'index d'un tenant — et une VM de cette zone partage son sous-réseau avec les trois VTEP, ce qui perce l'isolation à l'endroit même que l'EVPN devait fermer. ## 2026-08-03 (suite 4) — un index des décisions d'architecture `docs/decisions-architecture.md`. Les décisions étaient écrites là où elles s'appliquent, et leur histoire dans ce journal — mais « pourquoi le `/29` et pas le `/30` ? » demandait de relire vingt entrées. Le registre ne répète rien : il dit **quelles décisions existent, pourquoi, où lire le détail, et ce qui les garde**. **28 décisions** en quatre familles — le réseau, qui possède quoi, les secrets, la méthode. Chaque ligne porte sa raison en une phrase et sa preuve quand il y en a une. Une décision peut n'être gardée par aucune preuve : elle reste une décision, et le registre le montre plutôt que de laisser croire à une couverture complète. ### La section qu'on omet d'habitude : les décisions renversées Trois y figurent — l'isolation par ACL de commutateur, le routage inter-zone sur les commutateurs, l'underlay gitignoré à la racine du moteur. Les garder évite de refaire le chemin, et **explique pourquoi le code porte encore des branches qui semblent inutiles** : `acl_inter_tenant: true` et `routage_tenants: switch` restent les défauts, parce qu'une autre fabric peut en être capable. > Aucun de ces renversements ne vient d'un changement d'avis : les trois viennent d'un fait > découvert **après** la décision — une commande absente de l'aide du matériel, une capacité > manquante, une dizaine de modifications irrécupérables. C'est l'argument le plus fort pour > éprouver avant de figer. Les 30 renvois internes du registre ont été vérifiés : aucun document ni aucune section citée n'est introuvable. ### Corrigé le jour même — des dates déduites plutôt que vérifiées Les trois dates de la section « décisions renversées » avaient été **estimées**. L'historique git les corrige : les ACL et le routage sur commutateur remontent au `2026-07-07` (`make devis-reseau`), pas au 29 juillet ; l'underlay gitignoré au `2026-07-24`, pas au 31. Un registre qui invente une date perd la confiance qu'on lui accorde sur le reste. Chaque renversement cite désormais **le commit qui l'a opéré**, donc vérifiable en une commande. Ajouté aussi : **qui décide**. Toutes ces décisions sont celles de l'opérateur du dépôt, plusieurs prises sur recommandation — l'assistance propose et argumente, elle ne tranche pas. La distinction compte : une décision se renverse par celui qui l'a prise. ## 2026-08-03 (suite 3) — les preuves réseau sont rattachées à de vraies affirmations Trois preuves — **P21**, **P23**, **P24** — renvoyaient à `AFF-001`, qui affirme que *« Set-OPS est un moteur Ansible générique … à partir d'un plan »*. Aucun rapport avec la fédération, l'underlay ni la frontière. Trois autres — **P17**, **P19**, **P20** — n'avaient aucune référence. **Une preuve accrochée à la mauvaise affirmation ne prouve rien.** Elle passe au vert et n'atteste de rien de ce qu'on croit. ### Ajouté — §10 du registre : architecture réseau et fédération Six affirmations (`AFF-101` à `AFF-106`) : dérivation intégrale depuis le seed, absence de collision d'index, underlay disjoint de la plage tenant, garde anti-lockout de la frontière, validité des modèles underlay compris, couverture du plan par le panneau — cette dernière en 🟡, avec ses exceptions nommées plutôt que tues. Et une affirmation **volontairement absente** : la *justesse* des devis. Leur syntaxe dépend d'un matériel que le dépôt ne possède pas ; six familles ont été confrontées à un commutateur réel, deux étaient fausses, mais c'est une vérification datée et non une preuve rejouable. **Le dépôt n'affirme pas que ses devis s'appliquent ; il affirme qu'ils dérivent.** ### Corrigé — quatre preuves sans référence, et une erreur de la table `P03`, `P06`, `P12`, `P13` portent désormais les références que la table de couverture leur attribuait déjà : la correspondance existait **en double**, dans le document et dans le code, et seul le document la tenait. La table attribuait par ailleurs `AFF-030` (« inventaire complet ») à **P15**, qui valide le modèle socle. C'est **P16** qui exécute `ansible-inventory --list`. Vérifié : 35 affirmations référencées, **aucune référence orpheline**, une seule preuve sans référence — `P16`, dont la référence existe mais sous une autre forme syntaxique. ## 2026-08-03 (suite 2) — le panneau présente les deux devis La vue *Réseau* n'affichait que le devis des commutateurs : **le devis frontière était totalement absent de l'interface**, alors qu'il porte les règles de la bordure et ses avertissements — dont celui sur « Block private networks », invisible dans les règles elles-mêmes. Ajouté : `/api/devis-opnsense` et son bloc d'affichage, avec bouton de copie. Vérifié **par HTTP** que les deux points d'API servent exactement ce que le CLI produit — 181 et 125 lignes, identiques au caractère près. L'import est paresseux et gardé : ce module lit l'underlay et les inventaires de tous les tenants, et une erreur y aurait sinon empêché l'affichage du reste de la vue. ### Corrigé — le texte d'aide était périmé sur trois points Il annonçait les VLAN tenants « uniques **sur le trunk** » — faux en SDN, où aucun ne circule ; renvoyait le dialecte à `SETOPS_DIALECTE` alors que c'est un **intrant** de la section *Fabric* ; et disait que « la route par défaut vers OPNsense reste à adapter » alors que la section 5 l'émet depuis hier. Il dit maintenant ce qui reste réellement à nommer à la main : les **ports physiques** et, en SDN, le **nœud de sortie EVPN**. Rien d'autre — adresses, VLAN et routes se dérivent. ## 2026-08-03 (suite) — le devis frontière rattrape la bascule SDN Trois affirmations du devis frontière étaient devenues fausses, dont une qui cassait le routage. ### Corrigé — le prochain saut des routes tenants Elles pointaient le SVI du commutateur (`10.0.4.6`). En EVPN, il ne route plus les tenants : une route pointée là arriverait sur un équipement **sans chemin vers le tenant**. Configuration qui s'applique sans erreur et ne fonctionne pas — la signature qu'on traque depuis deux jours. Le devis émet désormais `` et dit pourquoi, à figer après le spike. `underlay.passerelle_sortie` **garde** son sens : c'est l'adresse du pare-feu sur le lien de transit, donc la sortie de l'**underlay**. Deux choses distinctes qu'il ne faut pas confondre. ### Corrigé — la description du lien affichait le marqueur du prochain saut Régression de la correction ci-dessus : le champ décrivant le **câblage** du lien de transit réutilisait la variable du **prochain saut**. Les deux étaient identiques jusqu'à la bascule SDN ; elles ont divergé, et la section 1 annonçait `switch `. Sur ce lien, le commutateur est bien à `10.0.4.6` — le nœud de sortie n'y figure pas. Le champ décrit désormais le câblage, indépendamment du routage. ### Corrigé — une contradiction entre les deux devis, antérieure au SDN La section 0 du devis frontière demandait de router les réseaux d'administration **vers `10.0.4.6`** — le SVI du commutateur lui-même. `make devis-reseau` émet `10.0.4.1`, l'adresse du pare-feu. Les deux devis se contredisaient, alors que l'un affirmait que l'autre « émet déjà ces routes ». Elles coïncident maintenant, vérifié ligne à ligne. ### Corrigé — deux commentaires qui affirmaient l'inverse de la décision L'en-tête (« les passerelles de zone restent sur les switches L3 ») et la description des routes (« routage inter-zone sur les switches L3 ») se dérivent maintenant du mode de routage. ## 2026-08-03 — le SDN prend le routage tenant, le devis switch se vide Trois décisions, et le devis les applique déjà : **une zone EVPN par tenant** ; le routage **et le filtrage** entre les zones d'un même tenant à ce niveau ; **l'inter-tenant obligatoirement par l'OPNsense**. ### `underlay.routage_tenants` — `switch` (défaut) ou `sdn` En `sdn`, le devis switch cesse d'émettre VLAN tenants, SVI de zone et ACL, et les retire des trunks. Chez Chezlepro, les trunks ne portent plus que **deux VLAN d'underlay** au lieu de quinze : aucun VLAN de tenant ne circule sur le fil, seulement du VXLAN que le commutateur transporte sans le lire. Les sections 1 à 3 sont remplacées par la raison, dont celle-ci : les passerelles `.1` n'ont pas changé d'adresse, elles ont changé de **porteur** — passerelle anycast du VNet, présente sur chaque hyperviseur. ### Le MTU devient une garde, pas un conseil VXLAN ajoute 50 octets. `make underlay` **refuse** un réseau de transport sous 1550 dès que `routage_tenants: sdn`, en disant pourquoi : sous ce seuil, le ping passe et les transferts échouent — la panne la plus coûteuse à diagnostiquer. Les deux réseaux de la fabric principale passent à 9000. ### Corrigé — la partie B déclarait encore les VLAN tenants Le filtre `routage_tenants` n'avait été posé que sur la partie A et les trunks : la partie B a sa propre boucle de déclaration, et sortait toujours les douze VLAN tenants. Aucun trunk ne les portait, aucun SVI ne les utilisait — mais leur présence suggérait que les switches d'accès devaient les connaître, ce qui contredit la décision. Vérifié dans les deux sens : zéro VLAN tenant en mode `sdn`, les vingt-quatre déclarations (douze par partie) de retour en mode `switch`. ### Le partage des responsabilités, écrit Une table dans `docs/sdn-evpn.md` dit qui route et qui filtre pour chaque nature de trafic. Deux conséquences y sont nommées : **l'inter-tenant ne peut plus être oublié** — il doit sortir du VRF, donc traverser une bordure en `block` par défaut ; et **le commutateur ne voit plus rien du trafic tenant**, donc y chercher la trace d'un problème applicatif est une perte de temps. Point ouvert ajouté : le registre des flux n'a **aucun mot-clé pour un flux inter-tenant**. Défaut sûr aujourd'hui, mais il rend impossible de *déclarer* une exception légitime. ## 2026-08-02 (suite 20) — décision : le routage passe aux hyperviseurs (SDN EVPN) `docs/sdn-evpn.md`. Les commutateurs ne savent pas lier une ACL à une interface de routage ; plutôt que d'assumer indéfiniment la perte d'isolation réseau que cela entraîne, le routage inter-zone passe à **Proxmox SDN, zones EVPN**. **Une zone EVPN est un VRF** — c'est celui qu'on regrettait de ne pas avoir dans le matériel, obtenu en logiciel. Et il referme le trou signalé quelques heures plus tôt : un tenant n'a plus de route vers l'underlay, celui-ci n'étant pas dans sa table de routage. Le plan de gestion redevient protégé **par construction**, pas par une règle qu'on pourrait oublier. **La projection du modèle ne demande aucun changement de dérivation** — vérifiée sur les deux tenants fédérés : | Objet SDN | Vient de | |---|---| | zone (VRF) | le tenant | | VNet | la zone de sécurité | | tag (VNI) | `vlan_de(index, zone)` | | subnet + gateway | `sous_reseau_de(...)` + `passerelle_de(...)` | Le `.1` ne change pas d'adresse, il change de porteur : du SVI d'un commutateur vers la passerelle **anycast** du VNet, présente sur chaque hyperviseur. L'invariant du dernier octet survit tel quel. Le devis switch maigrira d'autant : plus de VLAN tenants, plus de SVI de zone, plus d'ACL — en EVPN aucun VLAN de tenant ne circule sur le fil. La fabric redevient un transport IP. **Rien n'est éprouvé et rien n'est généré.** Le document fixe la cible, la projection et une séquence de spike en cinq points — dont la vérification du MTU, premier mur de VXLAN, et surtout la **tentative d'accès à l'underlay qui doit échouer**, puisque c'est le gain principal. Le principe du dépôt s'applique : éprouver l'outil avant d'écrire le rôle. ## 2026-08-02 (suite 19) — pas d'ACL sur cette fabric : on route, et c'est tout Les interfaces VLAN du Binardat n'offrent aucun `access-group` : impossible de lier une ACL à un SVI. Plutôt que d'émettre des règles qui ne seraient jamais liées — elles auraient l'air d'isoler sans jamais filtrer —, la capacité devient **déclarée** : `underlay.acl_inter_tenant: false`. Ce n'est pas lié au dialecte de CLI mais au **matériel** : un autre commutateur parlant la même CLI pourrait savoir lier des ACL. Par défaut la valeur reste `true`, donc rien ne change pour une fabric qui en est capable. À `false`, la section 3 du devis ne contient plus de règles mais **la raison** — et surtout ce qu'on perd : > Une VM émettant vers l'underlay voit son paquet **routé localement** par le commutateur — > mgmt des switches, mgmt Proxmox, OOB/IPMI. Les nftables des VM n'y peuvent rien (politique > `output` permissive), et l'IPMI n'est pas un hôte géré. L'isolation inter-tenant repose désormais entièrement sur les nftables d'hôte, en `policy drop`. C'est défendable — c'est déjà là que vit le zéro-confiance est-ouest — mais le plan de gestion de la fabric perd sa seule protection réseau. Des **VRF** auraient donné cette isolation sans ACL, par séparation des tables de routage. Ce matériel n'en a pas : c'est le critère à retenir au prochain renouvellement. Parade structurelle disponible d'ici là : sortir le management de la fabric routée des tenants, comme l'est déjà le stockage. ## 2026-08-02 (suite 18) — la liaison des ACL n'existe pas sur une interface VLAN `ip ?` sur une interface VLAN du Binardat n'offre **aucun `access-group`**, et la liste complète des commandes de ce mode n'en contient pas davantage. La ligne `ip access-group in` que le devis pose sur les douze SVI n'existe donc pas sur cette plateforme. **C'est la ligne qui rend l'isolation effective.** Sans elle, les ACL de la section 3 sont parfaitement définies et jamais liées : `show access-lists` afficherait « used 0 time(s) », et rien d'autre ne signalerait que l'isolation inter-tenant ne filtre rien. Même signature que le défaut du trunk — une configuration qui a l'air juste et n'agit pas. Le `firewall disable` aperçu dans un `show running-config` prend rétrospectivement du sens : le filtrage semble conditionné globalement. **Aucune forme de remplacement n'est devinée.** Le devis porte un avertissement à cet endroit, en dialecte `binardat` uniquement — après trois syntaxes supposées dont deux fausses, marquer l'incertitude vaut mieux qu'un quatrième pari. Reste à trancher sur une interface **physique** (`ip ?`, `access-group ?`) et sur le rôle de `firewall enable`. ## 2026-08-02 (suite 17) — spanning-tree vérifié : les six familles sont closes `spanning-tree ?` en mode configuration tranche la dernière inconnue, **en faveur de la forme émise** : `mode` et `priority` s'acceptent au niveau **global**. La priorité n'a pas besoin d'être portée par une instance, même en MSTP — la réserve inverse, notée la veille, était infondée et le devis ne la porte plus. Une mise en garde fausse est aussi nuisible qu'une syntaxe fausse. Ajouté : `spanning-tree` seul, qui **garantit l'état actif**. Sans lui, `mode` et `priority` sur un boîtier où le protocole aurait été désactivé configureraient un arbre qui ne tourne pas. Sans effet s'il est déjà actif — même logique déclarative que pour les trunks. Vérifié contre le matériel : VLAN, SVI, trunks, routes, définition des ACL, spanning-tree **global**. Deux ont révélé un défaut réel plutôt que de confirmer l'existant — les routes (notation CIDR) et surtout les trunks, dont la forme `add` ne retranchait rien. > **Rectification (même jour).** L'entrée ci-dessus a d'abord annoncé « les six familles sont > closes ». C'était faux : l'aide consultée était celle du mode configuration **globale**, et > trois lignes du devis vivent ailleurs — `spanning-tree portfast trunk` et `ip access-group > … in` au niveau **interface**, `ip default-gateway` en global mais absent de cette aide. > Elles restent non vérifiées, et la plus douteuse est `portfast trunk` : `trunk` est un > mot-clé Cisco. ## 2026-08-02 (suite 16) — la syntaxe des ACL vérifiée sur le matériel `show access-lists` confirme la dernière famille de syntaxe restée ouverte : une ACL générée s'applique **telle quelle** sur le Binardat, ses règles dans l'ordre émis — `ip access-list extended `, masques normaux, `any`. Détail de lecture consigné : le boîtier **affiche** `any-destination` là où l'on saisit `any`. Comparer un `show access-lists` au devis ferait apparaître une différence qui n'en est pas une — c'est le genre de faux positif qui fait perdre une heure. **Bilan des dialectes.** Cinq familles sur six sont désormais vérifiées contre le matériel : VLAN, SVI, trunks, routes, ACL. Ne reste que la **forme d'entrée** des commandes de spanning-tree, que `show` ne révèle pas. ## 2026-08-02 (suite 15) — `underlay.stp.mode` aligné sur le matériel `mode: mstp`, parce que c'est le mode d'usine du commutateur (`show spanning-tree` : IEEE 802.1s, *Force Version 3*). Sur une étoile sans lien redondant, l'instance 0 de MSTP se comporte comme un RSTP : changer de mode aurait donné le même résultat, au risque près de toucher à un protocole qui fonctionne déjà. Le devis énonce désormais sa propre incertitude là où elle est : **en MSTP la priorité se règle souvent par instance**, alors que la forme émise est globale. Le générateur le dit plutôt que de laisser croire à une syntaxe vérifiée — `show spanning-tree` donne l'état du protocole, jamais la forme d'entrée des commandes. Corrigé au passage : le commentaire de la section 6 disait « RSTP » en dur alors que le mode est déclaré. Il le dérive. ## 2026-08-02 (suite 14) — le spanning-tree du matériel : MSTP, actif, priorité par défaut `show spanning-tree` sur le commutateur Binardat corrige une déduction fausse et en apporte deux faits. **Correction.** J'avais déduit de son absence du `show running-config` que le spanning-tree était probablement désactivé. Il est **actif** : il n'y figurait pas parce qu'il est aux valeurs d'usine. Une absence dans une configuration ne veut pas dire une absence de fonction. **La plateforme est en MSTP** (IEEE 802.1s, *Force Version 3*), alors que `underlay.stp.mode` déclare `rstp`. Sur une étoile sans lien redondant les deux se comportent identiquement — la question est de savoir si l'on aligne la déclaration sur le matériel ou le matériel sur la déclaration. **La priorité de pont est déjà 32768**, donc la ligne émise pour les switches d'accès est un non-opérant : elle écrit ce qui est déjà vrai. Reste non vérifiée la **forme d'entrée** des commandes de spanning-tree : `show` en donne l'état, pas la syntaxe. En MSTP la priorité se règle en général **par instance**, ce que la forme actuellement émise ne fait pas. ## 2026-08-02 (suite 13) — le trunk ne restreignait rien `switchport trunk allowed vlan ?` sur le matériel réel confirme la syntaxe **et** révèle un défaut : `add` *ajoute* à la liste courante, la forme sans mot-clé la *définit*. Le devis émettait `add`. Or un port trunk neuf autorise **tous** les VLAN — dans une configuration réelle, les ports n'avaient aucune ligne `allowed vlan`, ce qui signifie exactement cela. Y ajouter la liste des VLAN voulus n'en retranchait aucun : le trunk continuait de tout transporter, et le devis donnait **l'illusion de restreindre**. C'est le pire genre de défaut — une configuration qui a l'air juste, s'applique sans erreur, et ne fait pas ce qu'elle annonce. La forme sans mot-clé est retenue, et elle est aussi **atomique** : `none` puis `add` aurait coupé le trunk entre les deux commandes, ce qui suffit à perdre la session si on l'applique sur le port de gestion. ## 2026-08-02 (suite 12) — la syntaxe des routes, vérifiée sur le matériel Un `show running-config` du commutateur Binardat tranche la question restée ouverte : la plateforme écrit ses routes en **notation CIDR** — `ip route 0.0.0.0/0 192.168.10.254` — et non en masque séparé comme Cisco. Le générateur produisait du Cisco quel que soit le dialecte. `route_statique()` suit désormais le dialecte, au même titre que les masques d'ACL. Vérifié dans les deux formes. Restent non vérifiés faute d'apparaître dans la configuration réelle : la syntaxe des ACL, celle de `switchport trunk allowed vlan add`, et le **spanning-tree** — totalement absent du `show running-config`, ce qui suggère qu'il est désactivé par défaut sur cette plateforme. ## 2026-08-02 (suite 11) — le responsable prend sa section, et la symétrie est dite ### Corrigé — un renvoi ambigu « Il ne dégèle qu'en cas de retour arrière (§6) » figurait **dans l'étape 6**. Deux « 6 » ne désignant pas la même chose dans une seule phrase, alors que tout le document distingue soigneusement sections et étapes. La section est désormais nommée plutôt que numérotée. ### Déplacé — le responsable désigné devient le §3 Il vivait dans « Le modèle : le transfert de nom de domaine », alors que ce n'est **pas un emprunt aux registraires** : c'est une décision de modèle, valable migration ou pas. Le §2 ne traite plus que de ce qui est emprunté et de là où l'analogie casse. ### Ajouté — ce qui suit le tenant, en regard de ce qui reste Le §8 énumérait ce que la migration ne déplace **pas** — fabric, frontière, index — sans dire ce qu'elle déplace. Or le responsable désigné, lui, **suit le tenant** : c'est exactement l'inverse, et le dire renforce la ligne de partage. > Si quelque chose appartenant à l'organisation ne peut pas partir, elle n'est pas vraiment > souveraine ; si quelque chose appartenant à l'hébergeur devait partir, c'est que la > frontière entre les deux est mal tracée. Cette symétrie est le test le plus simple d'une migration bien conçue. Sections renumérotées en conséquence (9 au lieu de 8) ; les six renvois internes vérifiés un par un. ## 2026-08-02 (suite 10) — le responsable désigné, et la réversibilité nuancée ### Décidé — chaque tenant a un responsable désigné Un domaine a un titulaire ; un tenant a un **responsable désigné** — la personne qui engage l'organisation, et dont la signature seule vaut mandat de migration. Ce n'est pas une formalité. Sans responsable nommé **d'avance**, la question « qui peut décider de déménager cette organisation ? » se pose au pire moment : quand les deux hébergeurs ont un intérêt dans la réponse. Un employé de bonne foi ne peut pas mandater le déménagement de son employeur. ### Corrigé — la table des états laissait croire que revenir est facile jusqu'au bout Elle annonçait une réversibilité « gratuite » entre `préparé` et `libéré`, alors que le §6 établit qu'elle change de nature à la bascule. Les deux ne se contredisent pas — on peut effectivement revenir jusqu'à `libéré` — mais un lecteur pressé s'arrêtant au tableau en retirait une fausse impression. Ce qui reste gratuit est l'adressage, pas le retour : tout dérive d'un seul chiffre, dans les deux sens. ### Ajouté aux points à trancher — trois questions de gouvernance **Où le responsable désigné est déclaré**, et surtout **comment on en change** : c'est un acte au moins aussi sensible que la migration, puisqu'il décide qui pourra la mandater ensuite. **Le recouvrement de la clé du responsable** — elle se perd, se compromet, ou la personne quitte l'organisation. Sans procédure, un tenant devient **inmigrable** : captif non par contrat mais par accident, exactement ce que la recette existe pour empêcher. Deux écueils symétriques y sont consignés. Trop lourde, la procédure n'aboutit jamais et le tenant reste bloqué. Trop légère, elle devient le **chemin de moindre résistance** pour contourner la signature — inutile de forger un mandat si l'on peut se faire attribuer la clé. Le recouvrement doit être au moins aussi difficile que ce qu'il protège. Piste retenue, la plus transposable des registraires : un **contact de secours nommé en même temps que le responsable**, tant que personne n'est en situation d'urgence. **La durée de rétention** : convenue avec qui, consignée où, attestée par qui. Sur une séparation d'hébergeur, un flou ici finit en litige. ## 2026-08-02 (suite 9) — le retour arrière de la migration La recette affirmait la réversibilité sans jamais décrire le retour. Or elle **change de nature à la bascule**, et le geste évident — repointer le DNS — devient faux à cet instant. Trois régimes, désormais écrits : - **avant le gel** : sans conséquence, le sortant n'a jamais cessé de servir. C'est ce que la règle d'ordre achète — tout ce qui peut échouer sans coût échoue là ; - **pendant le gel** : dégeler, l'interruption se limite à la durée du gel ; - **après la bascule** : ce n'est plus un retour mais une **migration inverse**. Les utilisateurs ont écrit chez l'entrant — courriels, fichiers, commits — et ces données n'existent nulle part ailleurs. Repointer le DNS les perdrait, et silencieusement. Point rendu explicite : **le sortant reste gelé après la bascule**, jusqu'à confirmation. Le dégeler « au cas où » créerait deux copies vivantes du même tenant et plus aucune vérité. En contrepartie il n'a pas divergé, donc le delta d'un retour reste à sens unique. Deux points de non-retour à ne pas confondre : la **bascule** fait perdre le retour *gratuit* (il reste la migration inverse) ; la **purge** fait tout perdre. D'où une exigence ajoutée aux points à trancher : les **critères de confirmation** de l'étape 7 se fixent par écrit **avant** la première bascule. Décider après coup ce qui compte comme « ça marche » revient à se donner raison. Corrigés au passage : deux renvois d'étape faux (le rattrapage est à l'étape 5, non 4 ; le chemin de vérification hors DNS public est un prérequis de l'étape 3 elle-même). ## 2026-08-02 (suite 8) — recette de migration d'un tenant entre hébergeurs `docs/migration-tenant.md` : la séquence, les états et les gardes. Écrite avant tout code, délibérément — figer un enchaînement qu'on n'a jamais joué serait prématuré. **Le modèle est le transfert de nom de domaine**, qui résout depuis trente ans les mêmes problèmes : le mandat appartient au **client** (ni l'hébergeur sortant ni l'entrant ne peut déplacer un tenant seul), verrou par défaut, deux actes délibérés et traçables. Là où l'analogie casse, elle est remplacée plutôt qu'étirée : il n'y a **pas de registre** central pour arbitrer, donc le mandat est **signé** par le tenant et vérifié contre une clé publique de son plan — un secret partagé ne prouverait rien à l'entrant, il pourrait venir du sortant. **L'ordre est commandé par une règle unique** : *le receveur doit être prouvé prêt avant que quoi que ce soit ne gèle.* L'entrant se construit et se prouve pendant que le sortant sert normalement ; l'interruption se réduit au rattrapage du delta plus la propagation DNS. Ce n'est pas un conseil mais une **garde de transition** — l'état `gelé` est inaccessible tant que `préparé` n'est pas prouvé. Deux pièges consignés parce qu'ils ne vont pas de soi : le **TTL** s'abaisse à l'étape 1, pas à la bascule, sinon tout le bénéfice de l'ordre est perdu ; et l'entrant doit être **vérifiable sans être public**, sinon le tenant sert des deux côtés et l'identité se dédouble. Enfin, la libération est une **révocation, pas une transmission** : re-clétage de la voûte et rotation des secrets chez l'entrant. Transmettre le mot de passe laisserait à l'ancien hébergeur un accès à vie aux secrets d'un client parti — rien ne rattrape ça après coup. ## 2026-08-02 (suite 7) — la frontière appartient à l'hébergeur, pas au tenant actif Un hébergeur sert **plusieurs tenants** et n'a qu'**une** frontière. Or ses intrants (`group_vars/opnsense.yml`) étaient lus chez le **tenant actif** : basculer sur un invité — Technolibre, qui n'a pas de frontière à lui — faisait perdre au devis l'URL de gestion, l'adresse publique et les deux interfaces. Il repartait en marqueurs, comme si le boîtier n'existait pas. Vérifié en simulant la bascule : `AUCUN` intrant lu. Même famille que le défaut de l'underlay corrigé plus tôt, et c'est la distinction hébergeur/tenant qui le fait apparaître. Le devis **et** le panneau lisent désormais la frontière chez l'hébergeur. Celui-ci n'est pas déclaré pour autant : le symlink `underlay.yml` le **désigne déjà**, et une seconde déclaration ouvrirait la porte à deux valeurs contradictoires. Repli sur l'instance active quand aucun underlay n'est monté — un site sans fabric déclarée continue de fonctionner. Vérifié : devis identique avec l'hébergeur actif, et intrants **conservés** avec un invité actif. `docs/frontiere-opnsense.md` gagne un §2 qui pose qui possède quoi. ## 2026-08-02 (suite 6) — l'underlay devient modélisable Un modèle décrivait jusqu'ici un **tenant** : ses services, ses zones, ses bases. Or tous les hébergeurs n'ont pas le même matériel, et l'infrastructure physique mérite le même traitement. ### Ajouté — `exemples/modeles/socle/underlay.yml` Le modèle public gagne un underlay **volontairement minimal** : un seul commutateur, pas de fabric de stockage séparée. C'est le point de départ honnête d'un petit hébergeur ; les montages plus riches (étoile à trois commutateurs, paire en MLAG, stockage jumbo dédié) sont d'autres modèles, conformément à la doctrine — un générique public, les étoffés en privé. Le modèle contient désormais **deux moitiés qui ne vont pas au même endroit** : `plan/` et `inventories/` chez le tenant, `underlay.yml` chez l'hébergeur. Chez un hébergeur qui est son propre tenant, les deux atterrissent au même dépôt — c'est le cas particulier, pas la règle. ### Étendu — `modeles.py verifier` valide l'underlay (preuve P17) La validation est **facultative** (un modèle sans underlay reste valide) et porte sur la cohérence **interne** seulement : VLAN sous la plage tenant, sous-réseaux disjoints, passerelle dans son réseau, routeur déclaré, ports non dupliqués, dernier octet partagé. Elle n'est **pas** confrontée aux tenants fédérés réels : un modèle est un gabarit, pas un site déployé. Il a fallu pour cela rendre paramétrables deux hypothèses du validateur, qui lisait la nomenclature de l'instance *active* et globait les dépôts frères — sur un modèle, les deux auraient été faux. `charger_depuis()` et `plan_nomenclature=` ; comportement par défaut inchangé. Cinq cas de rejet exercés sur un modèle fautif : VLAN empiétant sur la plage tenant, passerelle au mauvais dernier octet (lue dans la nomenclature **du modèle**), routeur inconnu, sortie hors du lien, port déclaré deux fois. ## 2026-08-02 (suite 5) — l'underlay rejoint le dépôt de l'hébergeur `underlay.yml` vivait **gitignoré** à la racine du moteur : consommé par deux générateurs, validé par la preuve P23, et versionné nulle part. La dizaine de modifications de la journée — transit, renumérotage du `/29`, séparation des fabrics, spanning-tree, dialecte, ports — n'était récupérable d'aucune façon, et un clone frais repartait du gabarit. Il appartient à l'**hébergeur** : ce sont ses switches, ses câbles, ses VLAN. Pas au moteur, qui est générique, ni à un tenant, qui n'en possède pas. Chezlepro est ici hébergeur *et* tenant, d'où la confusion : un tenant qui s'hébergerait sur son propre matériel aurait son propre underlay, dans son dépôt. Le moteur le monte par symlink, comme il monte le plan par `instance/` : ``` Set-OPS-public/underlay.yml -> ../OPS-Chezlepro/underlay.yml ``` **Ce lien ne suit pas `make instance-utiliser`.** Basculer l'instance active sur un autre tenant ne change pas la fabric : elle reste celle de l'hébergeur. Deux symlinks, deux durées de vie — c'est la conséquence directe de la distinction hébergeur/tenant. Vérifié : les deux devis sortent **identiques octet pour octet** avant et après, P23 reste verte, 24 preuves. Et un symlink **brisé** — le cas d'un clone du moteur sans le dépôt de l'hébergeur — dégrade proprement : `exists()` suit le lien, l'underlay est vu comme absent, et les devis omettent leurs sections au lieu d'échouer. Cas exercé. ## 2026-08-02 (suite 4) — deux devis qui se contredisaient, et une case à cocher ### Corrigé — le devis frontière certifiait des routes inexistantes Sa section 0 annonçait trois routes de retour « DÉJÀ ÉMISES par `make devis-reseau` ». Le devis switch n'en émettait **qu'une** : `devis_reseau` lisait `nftables_admin_ssh` de la seule instance active, alors que la frontière était passée multi-tenant la veille. Les deux réseaux d'administration de Technolibre n'étaient routés nulle part. Pire qu'un silence : une affirmation fausse désamorce la vérification. `admin_tous_tenants()` vit désormais dans `devis_reseau` et **`devis_opnsense` l'importe** au lieu d'en refaire une copie. Les routes de retour et les règles lisent les mêmes tenants, par construction. Vérifié : les deux listes sont identiques. ### Ajouté — l'avertissement « Block private networks » Le SSH d'administration a une source RFC1918 arrivant sur une interface **WAN**. OPNsense active par défaut ce filtre d'interface, qui s'applique **avant** les règles : coché, il jette le paquet sans qu'aucune règle ne soit consultée. La config paraît juste, le SSH ne passe pas. Le devis le signale dès qu'une source RFC1918 entre par le WAN — un réglage d'interface est invisible dans les règles, il fallait donc l'écrire à part. Le prédicat est **exactement** RFC1918, périmètre de cette case ; `ipaddress.is_private` aurait été trop large (plages de documentation, CGNAT), et l'avertissement se serait déclenché à tort. Les trois cas exercés : RFC1918 → averti ; `8.8.8.8/32` → muet ; `203.0.113.7/32` (documentation) → muet. ## 2026-08-02 (suite 3) — chaque règle porte son interface, et l'octet est gardé ### Ajouté — l'interface d'arrivée, dérivée du sens du flux Dans OPNsense une règle est **toujours `in` sur l'interface d'arrivée** : posée ailleurs, elle ne s'applique jamais et le trafic est bloqué sans que la configuration paraisse anormale. Les règles n'en portaient aucune, alors que le champ est obligatoire dans l'API. L'attribution se dérive : un flux `ingress`/`externe` arrive par le **WAN**, un flux `egress`/`externe` par le **lien de transit**. Le SSH d'administration suit la première ligne — le VPN est hébergé sur le pfSense voisin et revient par l'adresse publique de la frontière. C'était la dernière inconnue, et le montage parallèle décrit le 2026-08-02 la lève. Le rendu abandonne `pass out` pour `pass in on `, qui est l'idiome réel d'OPNsense et ce que le futur client d'API devra envoyer. ### Ajouté — `opnsense_wan_ip`, la face publique L'adresse publique de la frontière (`69.70.26.62`, reprise du pfSense) est un intrant de la section *Frontière* et s'affiche en section 1 du devis. ### Ajouté — invariant du dernier octet (preuve P23) Convention d'exploitation : un point de routage porte **le même dernier octet sur tous les sous-réseaux où il participe** — on retient une adresse, pas treize. `sleipnir-01` est `.1` partout. Le chiffre n'est pas codé en dur : il vient de `reservations.passerelle` dans la nomenclature, et `make underlay` refuse une passerelle qui s'en écarte. Exemption assumée : les liens plus étroits qu'un `/24`. Sur le `/29` de transit, l'adressage est dicté par les participants — les deux frontières prennent `.1` et `.2`, le switch `.6`. Vérifié que l'invariant était **déjà respecté** sur les 13 sous-réseaux routés avant d'écrire la garde. ## 2026-08-02 (suite 2) — la frontière porte les règles de TOUS les tenants Le devis était multi-tenant pour ses routes et mono-tenant pour ses règles : il routait `10.21.0.0/16` et `10.27.0.0/16`, mais ne filtrait que l'instance active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les deux sens, SSH d'administration compris, sans qu'aucune ligne ne dise pourquoi. Chemin présent, politique absente — le mode de panne du 2026-07-29, transposé. La résolution est désormais paramétrée par tenant : `inventaire_de()` lit le `hosts.yml` de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en argument, et les alias d'hôtes sont préfixés (`SETOPS_CHEZ17_SERVEUR_NGINX`). 11 règles par tenant, 22 au total. ### Cloisonnement du plan de gestion Première version de ce correctif : `SETOPS_ADMIN` devenait l'**union** des réseaux d'administration. Le plan de gestion de Technolibre aurait alors pu entrer en SSH chez Chezlepro — la bordure rouvrait ce que les ACL de switch ferment. Corrigé avant livraison : **un alias par tenant**, `SETOPS_ADMIN_`, n'ouvrant que son propre supernet. L'union est conservée pour les routes de **retour** côté switch et la garde P24 : router n'est pas autoriser, et le switch doit savoir revenir vers tous les plans de gestion. ### Deux omissions annoncées au lieu d'être tues Un tenant sans inventaire généré : aucune règle, et le devis le dit. Un tenant dont `nftables_admin_ssh` est vide : la règle SSH est **omise** plutôt qu'ouverte à `any`, ce qui exposerait le SSH à Internet. Cas dégradé exercé. ## 2026-08-02 (suite) — la sortie générale est déclarée, pas subie Le devis frontière se terminait par `block out log all` avec **une seule** règle sortante (le relais SMTP). Appliqué tel quel, il coupait la flotte d'Internet : plus de `apt`, plus de NTP, plus de récursion DNS. Rien ne le signalait — c'était la ligne la plus lourde de conséquences du devis, posée à la suite des autres. La sortie est désormais **déclarée dans le registre**, donc dérivée comme tout le reste : - `serveur_debian` (le socle, porté par les 14 hôtes) — `443/tcp` et `80/tcp` pour les dépôts apt, `123/udp` pour l'horloge. Une dérive d'horloge fait échouer la validation des certificats step-ca et le SSO, des semaines après la cause. - `client_unbound` — `53/udp` et `53/tcp` : `client_unbound_transitaires` est vide, donc Unbound interroge lui-même la racine. C'est le choix souverain ; il a un coût réseau qu'il faut déclarer. Le TCP n'est pas optionnel — c'est le repli obligatoire dès qu'une réponse DNSSEC dépasse la taille UDP. Le devis passe de 6 à 11 règles, et sa section 5 **énonce** le default-deny sortant, le nombre de règles qui l'accompagnent, et où déclarer un besoin oublié — jamais à la main dans le pare-feu, la règle serait perdue à la génération suivante. Vérifié : les nftables d'hôte sont **inchangés**, octet pour octet. Le pair `externe` reste sauté par `resoudre_flux.py` — ces flux relèvent de la bordure, et d'elle seule. Non déclaré volontairement : le rôle `chrony` existe mais n'est référencé par aucun groupe, aucun playbook ni le graphe de dépendances. Lui écrire un flux aurait créé une règle morte. ## 2026-08-02 — les interfaces se nomment par leur identifiant `opt1`, `igb1` et `TENANTS` désignent le même port dans OPNsense : l'identifiant interne, le périphérique FreeBSD et le libellé affiché. L'API REST ne parle que du **premier**, et c'est lui que veulent `opnsense_if_wan` et `opnsense_if_transit`. Le libellé de ces deux intrants ne le disait pas — la question s'est posée en pratique. Les libellés le disent maintenant explicitement, et `docs/frontiere-opnsense.md` §6 explique les trois couches ainsi que le motif du choix : `opt1` est le plus stable des trois, il survit à un changement de carte réseau comme à un renommage. La note « `opnsense_prochain_saut` dérive de l'underlay » est repliée dans l'en-tête que le panneau régénère : une sauvegarde l'effaçait, puisque le fichier est réécrit depuis le YAML analysé. Vérifié qu'une sauvegarde préserve valeurs **et** références de voûte. ### Corrigé — la doc portait encore l'ancien plan du `/29` Après le renumérotage (`bifrost-1/-2` en `.1`/`.2`, SVI en `.6`), deux passages de `docs/frontiere-opnsense.md` annonçaient toujours `10.0.4.2` comme prochain saut. La doc contredisait le devis généré ; les `ip route` des deux coïncident désormais. ## 2026-08-01 (suite 14) — l'empreinte du root CA n'est pas un secret de voûte `client_pki` **dérive l'empreinte à chaud** depuis l'autorité (`step certificate fingerprint` en `delegate_to` sur `serveur_step_ca`) — précisément parce qu'un `from-zero` régénère l'AC avec une empreinte neuve. Une empreinte figée en voûte serait périmée dès la première reconstruction, et une empreinte périmée fait échouer le `bootstrap` de chaque hôte. Or `defaults/main.yml` portait encore `client_pki_ca_fingerprint: "{{ vault_step_ca_fingerprint | default('') }}"`. Ce défaut était **mort** : la tâche suivante écrase le fait sans condition. La valeur de la voûte n'avait aucun effet, quel qu'en soit le contenu. Le recensement de `voute.py` s'y laissait prendre — il cherche la chaîne `vault_*` dans les fichiers, sans pouvoir savoir qu'un défaut n'est jamais lu. La « source unique » avait donc hérité de l'erreur, et le panneau réclamait un secret impossible à fournir avant que l'AC n'existe. Le défaut mort est retiré, et la clé disparaît d'elle-même du recensement (25 → 24 exigés), du gabarit et du panneau. `client_pki_ca_fingerprint_override` reste le moyen documenté d'épingler une empreinte (AC externe, migration). ### Corrigé — une troisième copie manuelle de la liste des secrets `docs/intrants-communs.md` §H énumérait les secrets à la main, avec les deux mêmes erreurs. Elle renvoie désormais à `scripts/voute.py lister` et à la preuve P18 plutôt que d'entretenir une copie de plus. ## 2026-08-01 (suite 13) — le rappel des secrets dérive de `voute.py` `SECRETS_ATTENDUS` était une liste écrite à la main dans `inventory_gui.py`, en parallèle du recensement que `scripts/voute.py` fait déjà depuis le plan, les rôles des groupes actifs et les `group_vars`. Deux sources pour la même vérité, et la manuelle avait divergé : | Clé | Panneau | Gabarit | Référencée | |---|---|---|---| | `vault_ldap_sssd` | annoncée | absente | **nulle part** — aucun rôle `sssd` n'existe | | `vault_step_ca_fingerprint` | **omise** | présente | `roles/client_pki/defaults/main.yml:24` | Un opérateur qui suivait le panneau créait donc un secret que rien ne consomme, et oubliait celui dont `client_pki` a besoin pour vérifier l'empreinte de l'AC racine. Le rappel dérive désormais de `voute.secrets_exiges()` — la **même source que la preuve P18** — augmentée de `SECRETS_HORS_MOTIF` pour les jetons Proxmox, qui ne portent pas le préfixe `vault_`. Vérifié : **27 noms, écart nul avec le gabarit**. Sur un dépôt public nu, la liste est vide plutôt qu'en erreur. ## 2026-08-01 (suite 12) — la fabric se règle depuis la console Tout le modèle d'underlay bâti aujourd'hui — routeur, spanning-tree, fabrics, transit — s'éditait à la main dans un YAML, pendant que la doctrine dit qu'un sysadmin doit exploiter l'outil **sans IA**. La frontière avait eu sa section dans le panneau ; l'underlay, non. Une section **Fabric** couvre désormais les valeurs plates : le switch routeur, le dialecte de CLI, le mode et la topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, et donc les ports) restent hors de portée du panneau — elles demandent une vue dédiée, comme celle des serveurs. ### Le dialecte devient un intrant déclaré Il ne vivait que dans `SETOPS_DIALECTE`, une variable d'environnement. C'est une propriété du **matériel**, donc de la fabric : elle se déclare dans `underlay.yml`. Précédence désormais explicite : drapeau `--dialecte` > variable d'environnement > intrant déclaré > `cisco`. `make underlay` refuse un dialecte inconnu. ### Écriture chirurgicale, pas de `safe_dump` `underlay.yml` porte 23 lignes de commentaires qui expliquent des décisions d'architecture — pourquoi un seul routeur, pourquoi le transit vit dans l'underlay, pourquoi les rayons ne sont pas des ports de bord. Un `safe_dump` les aurait toutes effacées, comme c'est arrivé aux commentaires de `plan/applications.yml`. Le panneau remplace donc la ligne existante en respectant son indentation. Vérifié : trois valeurs modifiées, **73 lignes et 23 commentaires avant comme après**. Une clef absente du fichier n'est pas créée : le panneau refuse explicitement plutôt que de l'inventer à un endroit arbitraire. ## 2026-08-01 (suite 11) — les ports physiques entrent dans le modèle Les noms de ports n'étaient modélisés **nulle part** : `` et consorts étaient des marqueurs littéraux dans le générateur. L'opérateur les remplaçait à la main dans la sortie, et recommençait **à chaque régénération**. C'était le seul endroit du devis où le travail était perdu à répétition. Ils se déclarent désormais par équipement dans `underlay.yml`, sous quatre clefs qui correspondent aux quatre natures de lien : ```yaml ports: hyperviseurs: [Te1/0/1, Te1/0/2, Te1/0/3] # ports terminaux (portfast) frontiere: [Gi1/0/23] # vers le pare-feu (portfast) rayons: { sleipnir-02: Te1/0/47, … } # côté ROUTEUR montante: Te1/0/48 # côté SWITCH D'ACCÈS ``` Le devis émet alors les vrais ports, y compris **plusieurs** vers les hyperviseurs — il n'en supposait qu'un seul jusqu'ici, alors que le cluster en compte trois. Non déclarés, les marqueurs reviennent : la dégradation est par équipement, un switch renseigné et un autre non cohabitent sans problème. La partie B devient **par switch** : adresse de gestion, montante et ports terminaux différant d'une machine à l'autre, un bloc commun n'avait plus de sens. `make underlay` refuse un port déclaré deux fois sur un même équipement, un rayon vers un switch inconnu, des `rayons` sur autre chose que le routeur, une `montante` sur le routeur lui-même. Les quatre cas exercés. ## 2026-08-01 (suite 10) — une interface, un bloc La partie A déclarait ses ports de bord deux fois : l'interface en section 4, son `portfast` en section 6. La partie B, elle, posait tout dans le même bloc. Deux conventions pour la même chose dans un seul document — un opérateur qui applique la partie A section par section configurait la même interface à deux endroits. Le `portfast` est désormais posé **avec son interface** (sections 4 et 4b). La section 6 se réduit à ce qui est global — mode, priorité du pont racine — plus les deux avertissements : que les rayons de la section 4c n'en sont volontairement pas, et pourquoi BPDU guard n'est pas émis. Vérifié : aucune interface n'est déclarée deux fois dans une même partie, trois `portfast` avec `stp`, zéro sans lui, zéro sur un rayon. ## 2026-08-01 (suite 9) — rayons de l'étoile séparés des ports terminaux L'ajout du spanning-tree venait de rendre dangereuse une imprécision qui, jusque-là, n'était qu'un titre approximatif. La section 4 s'appelait « Trunk vers Proxmox **+ inter-switch** » et n'émettait qu'un port, que la section 6 déclarait en bord de réseau. Réutiliser ce placeholder pour les rayons vers les switches d'accès revenait à mettre `portfast` sur les liens qui portent les BPDU — c'est-à-dire à désactiver la protection anti-boucle exactement là où elle sert. Les rayons sont désormais **dérivés** et émis à part (section 4c côté routeur, B3a côté accès), un par switch d'accès, avec l'avertissement qu'ils ne sont pas des ports de bord. La section 4 ne désigne plus que les hyperviseurs, et la partie B distingue sa montante (`B3a`) de son trunk terminal (`B3b`). `underlay.switches_acces()` devient la source unique du « qui est un switch d'accès » — utilisée pour leur devis **et** pour les rayons côté routeur : les deux ne peuvent pas diverger. Vérifié : trois commandes `portfast` émises avec `stp` déclaré, **aucune** sans lui, et **aucune** sur un rayon. ## 2026-08-01 (suite 8) — spanning-tree dérivé de la topologie déclarée ### Ajouté — `underlay.stp` et les sections 6 / B4 Le devis ne disait rien du spanning-tree. Sur une fabric convergée à trois switches, une boucle par brassage accidentel n'est pas discrète : c'est une tempête de diffusion. La topologie se déclare (`mode: rstp`, `topologie: etoile`) et le devis en tire la configuration. Le **routeur est désigné pont racine** — il est le centre de l'étoile, tous les chemins passent déjà par lui, donc l'arbre logique suit le câblage physique au lieu de sortir d'une élection arbitraire. Les switches d'accès reçoivent une priorité volontairement haute : ils ne doivent jamais devenir racine. Les ports terminaux (hyperviseurs, frontière) sont déclarés en bord de réseau. **BPDU guard n'est délibérément pas émis** : un pont Linux dont le STP serait activé enverrait des BPDU et ferait tomber le port côté hyperviseur. Le devis dit pourquoi, et à quelle condition l'ajouter. En étoile, aucun lien n'est redondant : RSTP est un filet, pas une nécessité — le devis le dit plutôt que de laisser croire à une protection indispensable. `make underlay` valide `mode` et `topologie`, et refuse un `stp` déclaré sans `routeur` : sans lui, aucun pont racine ne peut être désigné. Sans `stp`, la section signale l'absence de protection au lieu de disparaître. Réserve consignée : la forme `binardat` des lignes de spanning-tree n'a pas été confrontée au matériel, comme les `ip route`. ## 2026-08-01 (suite 7) — l'underlay connaît ses fabrics physiques Le stockage jumbo (iSCSI, Ceph) est porté par un **réseau indépendant de deux switches 10G**, sans câble commun avec la fabric convergée des `sleipnir`. Le modèle l'ignorait : le devis déclarait les VLAN 20/30/31 sur les switches convergés et les mettait dans leurs trunks. C'était faux. Chaque réseau de l'underlay porte désormais une `fabric` (`principal` par défaut). Le devis ne configure que celle du routeur, et **énonce ce qu'il ne couvre pas** plutôt que de le taire : ``` ! HORS PERIMETRE — fabric 'stockage' : stockage-iscsi (VLAN 20), ceph-public (VLAN 30), … ! Portee par des switches distincts, sans cable commun avec celle-ci : ! ni VLAN a declarer ici, ni trunk, ni spanning-tree partage. ``` Conséquence directe : la question du spanning-tree ne se pose que sur la fabric principale — trois switches — et pas sur le stockage, dont les deux switches forment un domaine séparé. Le roster d'hôtes de la section 0 est filtré de la même façon : un équipement d'une autre fabric n'apparaît pas, même en commentaire. Un devis est une configuration qu'on applique, pas un inventaire. Vérifié en déclarant un hôte de la fabric stockage — il reste absent, et la sortie du jour est inchangée. Les `deny` de l'ACL couvrent en revanche **toutes** les fabrics, y compris celles hors périmètre : la règle porte sur l'adresse de destination, pas sur le câblage. Si un chemin s'ouvre un jour vers le stockage, il est déjà fermé. ## 2026-08-01 (suite 6) — nommage : `bifrost` aux frontières, `sleipnir` à la fabric `bifrost-1` et `bifrost-2` sont réservés aux deux frontières OPNsense — Bifröst est le pont vers l'extérieur. Les switches internes deviennent `sleipnir-01…03` : le cheval qui traverse les mondes, pas le pont qui en sort. La division du nom suit celle de l'architecture. Les deux boîtiers sont déclarés comme hôtes du lien de transit (`10.0.4.1`, `10.0.4.2`) : hors flotte Ansible, la déclaration documente le lien et réserve les noms. Le `/29` choisi plus tôt les loge tous les deux, comme prévu. Le plan du `/29` est réorganisé en conséquence : frontières en bas (`.1`, `.2`), SVI du switch en haut (`.6`), et `.3` laissée libre pour une future IP virtuelle CARP si les deux OPNsense passent en haute disponibilité. Ce jour-là, `passerelle_sortie` pointera sur la VIP plutôt que sur un boîtier nommé — un seul endroit à changer. Conséquence à traiter : la partie B du devis switch aurait listé les deux pare-feux parmi les « switches d'accès ». Elle ne retient désormais que les hôtes du **réseau de management** — un équipement déclaré ailleurs ne reçoit aucune ligne de configuration de switch. Le devis frontière nomme le boîtier quand il est déclaré : `frontiere 10.0.4.1 (bifrost-1)`. ## 2026-08-01 (suite 5) — deux incohérences du devis switch ### Corrigé — le routeur avait deux adresses de gestion contradictoires `bifrost-01` portait le SVI `Vlan10 → 10.0.0.1` **et** était déclaré dans `underlay.hotes` à `10.0.0.2`. Une interface VLAN n'a qu'une adresse primaire : les deux ne pouvaient pas être vraies. L'entrée datait d'avant la désignation du routeur, quand `10.0.0.1` était une passerelle abstraite. Le routeur est désormais déclaré à l'adresse du SVI qu'il porte, et `make underlay` **refuse** la divergence : `ip 10.0.0.2 != passerelle 10.0.0.1 — le routeur porte ce SVI, les deux doivent coïncider`. Le roster des trois switches reste complet. ### Corrigé — VLAN de transit déclaré sur les switches d'accès La partie B créait `vlan 40` alors que le trunk B3 ne le transporte pas — le transit ne relie que le routeur à la frontière. Un VLAN qui n'aurait jamais vu de trame. Il est exclu de la partie B, comme il l'est déjà des trunks généraux. ## 2026-08-01 (suite 4) — un seul switch route, les autres en L2 pur ### Décidé — `underlay.routeur` Sans MLAG, le routage est porté par un **unique** switch (`bifrost-01` chez Chezlepro) ; les autres restent en L2 pur. Le devis émettait jusqu'ici un jeu unique de SVI sans dire à quel switch il s'adressait : appliqué sur les trois, il aurait créé autant de conflits d'adresses qu'il y a de zones. ### Ajouté — le devis se scinde en deux parties - **Partie A — switch routeur** : VLANs, SVI, ACL d'isolation, trunks, routes. Va sur lui seul, et l'en-tête le nomme. - **Partie B — switches d'accès (L2 pur)** : mêmes VLANs pour commuter les trames étiquetées, une adresse de gestion par switch (tirée de `underlay.hotes`) avec `ip default-gateway` vers le routeur, et les trunks. Aucun SVI de zone, aucune ACL, aucune route. Deux gardes : `make underlay` refuse un `routeur` qui ne nomme aucun hôte déclaré ; et la partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine — les coller tous écraserait l'adresse. Sans `routeur` désigné, l'en-tête signale explicitement le risque de duplication au lieu de laisser croire que le devis est applicable partout. Reste ouvert et consigné : la syntaxe des `ip route` n'est pas dialecte-consciente, contrairement aux ACL. ## 2026-08-01 (suite 3) — les tenants n'atteignent plus l'underlay ### Corrigé — ACL de switch : `deny` vers la fabric physique L'ACL d'isolation bloquait l'autre tenant, puis se terminait par `permit ip any`. Ce `any` autorisait `10.27.x` → `10.0.0.0/24` : le management des switches, celui de Proxmox et l'**OOB/IPMI**, plus les réseaux iSCSI et Ceph. Une VM compromise atteignait la console physique des hyperviseurs. Le commentaire du générateur disait « Reste → passerelle OPNsense », mais c'est faux pour l'underlay : ce trafic est routé **localement** par le switch et ne passe jamais par la frontière, donc il n'est jamais filtré par elle. `devis_reseau.py` émet désormais un `deny` par sous-réseau underlay avant le `permit` final, dérivé de `underlay.yml` — dialecte respecté (masque normal ou wildcard). Aucun flux du registre ne vise l'underlay : le blocage ne casse rien de déclaré. Deux limites consignées dans `docs/frontiere-opnsense.md` §6 : le registre des flux n'a pas de mot-clé `underlay`, donc un besoin légitime (superviser l'hyperviseur) ne pourrait pas être déclaré ; et `devis_reseau.py` émet un jeu unique de SVI pour trois switches sans MLAG, ce qui reste une décision d'architecture ouverte. ## 2026-08-01 (suite 2) — le port vers la frontière, et l'ordre d'application ### Ajouté — section 4b : le port du switch vers la frontière Le devis étiquetait le VLAN de transit sur le trunk `` et n'émettait aucune interface vers le pare-feu. Deux erreurs en une : les hyperviseurs n'ont pas d'interface sur le transit, et le pare-feu n'a rien à faire des VLAN tenants — il route vers eux, il ne les étiquette pas. Le VLAN de transit sort donc du trunk Proxmox et prend son propre port, dérivé du transit déclaré. ### Ajouté — avertissement d'ordre en tête de la section 5 Les routes de la section 5 déplacent la sortie du switch, **y compris celle de ses propres réponses**. Tant que l'adresse de la frontière ne répond pas, elles coupent l'accès d'administration au switch lui-même — le mécanisme qui a rendu une VM muette le 2026-07-29, appliqué cette fois à l'équipement depuis lequel on travaille. Le devis crachait ces lignes à la suite des autres comme si elles étaient équivalentes. Il énonce désormais les préalables (boîtier câblé, adressé, joignable depuis le switch, session console ouverte) — inacceptable autrement pour un outil censé être exploitable sans IA. ## 2026-08-01 (suite) — le prochain saut dérive du transit `opnsense_prochain_saut` n'est plus un intrant du panneau : il **dérive** du réseau de transit de l'underlay (la `passerelle` du réseau portant `passerelle_sortie`). Un seul bloc de six lignes alimente désormais les deux devis : `10.0.4.1` devient le SVI côté switch **et** le prochain saut des routes tenants côté frontière ; `10.0.4.2` devient la route par défaut du switch. Le saisir en doublon dans le panneau rouvrait la possibilité de deux valeurs contradictoires pour un seul lien — précisément le mode de panne qu'on venait de fermer pour les réseaux d'administration. Le devis frontière gagne au passage un bloc `transit` (JSON compris) et cesse de réclamer en section 0 des routes que `make devis-reseau` émet maintenant : il dit lesquelles sont **déjà émises**, ou signale l'absence de transit déclaré. Sans underlay, le marqueur `` revient — le repli reste explicite. Reste un seul intrant à figer au câblage : `opnsense_if_transit`, le nom de l'interface qui porte le VLAN 40 sur le boîtier — la seule valeur que rien ne peut deviner. ## 2026-08-01 — le lien de transit et les deux routes (la boucle est fermée) ### Ajouté — réseau de transit dans l'underlay (clé `passerelle_sortie`) Le lien entre le routeur est-ouest (switches L3) et la frontière nord/sud manquait dans **tous** les fichiers : le devis switch ne contenait pas une seule `ip route`. Il vit dans l'**underlay** et non dans un tenant, pour une raison qui tranche : la frontière route vers tous les supernets tenants par le **même** prochain saut. Le lien est donc partagé et ne peut dériver d'aucun `index`. Un réseau underlay portant `passerelle_sortie` (l'adresse du pare-feu sur le lien) le déclare — chez Chezlepro : VLAN 40, `10.0.4.0/29`, SVI `10.0.4.1`, frontière `10.0.4.2`. Un `/29` et non un `/30` parce que deux pare-feux cohabitent pendant la transition. `underlay.py` valide la nouvelle clé (sortie sur le lien, distincte du SVI, SVI obligatoire, un seul transit) — **preuve P23**, cas de rejet exercés un par un. ### Ajouté — `devis-reseau` émet les routes (section 5) Deux routes dérivées, et il en faut impérativement deux : - **l'aller** : `ip route 0.0.0.0 0.0.0.0 ` — sans elle, aucun hôte n'a de sortie ; - **le retour** : une route par réseau d'administration — sans elle, la réponse d'une VM revient au pare-feu par une autre interface que celle où l'état a été créé, et se fait jeter en silence. C'est le piège qui a coûté la passe de déploiement du 2026-07-29. Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` : **même source unique** que la garde anti-lockout des nftables et l'alias `SETOPS_ADMIN` de la frontière — les trois pare-feux et les routes ne peuvent pas diverger. Sans transit déclaré, la section s'affiche en clair comme manquante plutôt que de disparaître silencieusement. ### Corrigé — le panneau refusait d'enregistrer les intrants de la frontière `group_vars/opnsense.yml` porte à la fois des paramètres anodins et deux **références** de voûte (`{{ vault_opnsense_api_key }}`). La fusion « préserve les clés non gérées » les relisait, et le garde-fou, qui ne regardait que les **noms**, les prenait pour des secrets soumis. Il regarde désormais la **valeur** : une référence de voûte est un pointeur et passe ; toute valeur réelle sur ces clés fait toujours échouer l'écriture. Ajout d'un refus explicite à l'entrée pour une requête forgée, au lieu d'un abandon silencieux. ### Retiré — reliquat `proxmox.vault.yml` La voûte est **unique** (`group_vars/all/vault.yml`) ; l'ancien fichier séparé n'était plus chargé automatiquement (aucun groupe `proxmox` dans l'inventaire) et entretenait la confusion. Supprimé de l'instance, avec son gabarit. Au passage : `supprimer_vm_debian.yml` ne chargeait **que** ce reliquat pour ses secrets. Le supprimer tel quel aurait cassé `make detruire`, l'outil même du rebuild from-zero. Sa liste est alignée sur celle du playbook de clonage (`all/vault.yml` en dernier, il l'emporte), et la résolution du jeton depuis la voûte unique est vérifiée en exécution réelle. ## 2026-07-29 (suite) — la frontière OPNsense, dérivée du registre des flux ### Ajouté — `make devis-opnsense` (+ preuve P24) La bordure nord/sud devient un **artefact dérivé**, comme le devis switch. Rien de saisi à la main : ni port, ni adresse, ni nom d'hôte n'apparaît dans le générateur. Le constat qui rend la chose évidente : `resoudre_flux.py` **saute volontairement** les flux `pair: externe` (`scripts/resoudre_flux.py:184`) parce qu'ils ne concernent pas le pare-feu d'hôte. Plusieurs `raison` du registre disent déjà « filtré à l'OPNsense ». **La politique de la frontière était donc déjà écrite** — il ne restait qu'à la dériver. - **`scripts/devis_opnsense.py`** — agrège les flux `externe`, résout les destinations depuis l'inventaire (hôtes actifs **et** planifiés : la frontière se prépare avant les VM), les supernets depuis les nomenclatures fédérées, et l'accès d'administration depuis l'intrant `nftables_admin_ssh`. Sort un devis relisible ou `--json` (destiné à l'API OPNsense). - **Garde anti-lockout (P24)** — `--verifier` **refuse** un devis dont `nftables_admin_ssh` est vide : la règle SSH entrante n'aurait aucune source et le `block in` final fermerait l'accès d'administration. Même intrant que les nftables d'hôte : source unique, donc pas de divergence possible entre la bordure et les hôtes. - **Section 0 du devis : les routes de retour à poser sur les switches.** C'est le piège qui a coûté la passe de déploiement du jour — la passerelle de zone répond au ping, mais aucun hôte derrière elle n'est joignable, parce que la réponse revient au pare-feu par une autre interface que celle où l'état a été créé. - **`docs/frontiere-opnsense.md`** — les décisions d'architecture (frontière nord/sud, les SVI restent sur les switches L3), le partage des rôles entre les trois pare-feux, le chemin d'application par l'API, et ce qui reste ouvert (câblage, second VPN, sortie générale). ### Corrigé — intrant `nftables_admin_ssh` vide sur l'instance Chezlepro Il valait `[]` alors que `group_vars/hotes_actifs.yml` active `nftables_baseline_enabled`. Autrement dit : la flotte se serait mise en `policy drop` sans **aucune** règle autorisant le contrôleur, qui arrive par le VPN hors du sous-réseau de la flotte. Renseigné à `192.168.255.0/24`. Le sous-réseau du second VPN (OPNsense) devra y être ajouté. ### Ajouté — la frontière devient réglable depuis la console (section *Frontière*) Les valeurs non sensibles du pare-feu de bordure sont de **vrais intrants**, pas un fichier YAML tenu à la main : un opérateur règle la frontière depuis le panneau « Intrants de base », sans IA et sans éditeur. - **`INTRANTS_SCHEMA`** — nouvelle section *Frontière* : `opnsense_api_url`, `opnsense_api_verifier_certs`, `opnsense_if_wan`, `opnsense_if_transit`, `opnsense_prochain_saut`. Cible d'écriture `group_vars/opnsense.yml`, en **fusion** (comme Proxmox) pour préserver les références de voûte du fichier. Le panneau rend ses sections génériquement : aucune modification d'interface n'a été nécessaire. - **Garde-fou** — `opnsense_api_key` / `opnsense_api_secret` ajoutés à `INTRANTS_CLES_INTERDITES` : le GUI **refuse** de les écrire, donc impossible de coller un secret d'API dans le panneau par mégarde. Ils n'apparaissent qu'en lecture seule, par leur nom, dans `SECRETS_ATTENDUS`. - **`devis_opnsense.py`** lit désormais ces intrants et n'affiche ses marqueurs (``, ``) qu'en repli : le devis se complète de lui-même dès que la console est renseignée. ### Ajouté — les identifiants d'API de la frontière, dans la voûte Même partage que Proxmox : l'anodin en clair, le secret dans la voûte unique de l'instance. - **`group_vars/opnsense.yml`** (instance, en clair) — URL de gestion, interfaces et prochain saut à figer au câblage, plus les **références par nom** `{{ vault_opnsense_api_key }}` et `{{ vault_opnsense_api_secret }}`. Aucune valeur de secret n'y figure. - **Gabarit de voûte** — les deux clés ajoutées à `vault.yml.example`. `scripts/voute.py` les a recensées **tout seul** depuis les `group_vars` (il ne lit jamais la voûte, il ne compare que des noms) : le gabarit passe de 23 à 25 secrets, et la **preuve P18** reste verte. Validation : `make verifier` vert — **24 preuves CONFORME**, 0 échec, 0 sauté. ## 2026-07-29 — dette documentaire soldée (un README par rôle) + carte revue ### Ajouté — les 12 README de rôles manquants **Tous les rôles ont désormais un README.** Les 12 restants sont écrits, au format maison (intention → rôle → variables → notes/limites → prérequis), et documentent surtout ce qui ne se lit pas dans les tâches : - **Socle et résolution** — `serveur_debian` (rôle-catégorie sans tâches : il ne porte que le flux SSH du plan de gestion, *sans quoi les nftables couperaient l'accès Ansible*), `hosts_statiques` (le plancher `/etc/hosts` qui rend l'ordre de reconstruction possible). - **Rôles utilitaires** — `resoudre_base` et `resoudre_annuaire` : entrées/sorties (facts), et *pourquoi* le FQDN plutôt que le nom court (fédération + `verify-full`). - **Courriel** — `serveur_dovecot` (les trois réglages Dovecot 2.4 qui conditionnent la remise ; le local-part seul comme chemin commun LMTP/IMAP), `serveur_postfix` (liens `mailstore`/`milter`, recopie de `/etc/hosts` dans le chroot), `serveur_rspamd` (clé DKIM idempotente ; le domaine signé doit être **public** en prod). - **Sauvegardes** — `client_backup` (jobs déclaratifs, chiffrement côté client, *le dépôt neuf est vide tant qu'une première sauvegarde n'a pas tourné*) et `serveur_backup` (hors-nœud ≠ hors-site). - **SSO et supervision** — `serveur_oauth2_proxy` (le patron réutilisable Keycloak-devant- n'importe-quoi ; pourquoi `allow_unverified_email` est nécessaire avec un annuaire LDAP) et `serveur_icingaweb2` (modes `ldap` vs `external`, et l'écoute à restreindre en SSO). - **`client_unbound`** — le garde-fou de bascule du résolveur (`apply` **et** `confirm`, validation avant de toucher `/etc/resolv.conf`). ### Corrigé — `docs/carte-set-ops.md` ne décrivait plus l'état du code - **`expose` est consommé au déploiement** (la carte l'annonçait encore comme « Phase 3 à venir ») : vhosts nginx dérivés via `expositions_des_applications` + `expositions.conf.j2`, alias `/etc/hosts`, SANs d'edge dérivés par `instancier.py`. - **Trois mécanismes transverses ajoutés au catalogue** : résolution d'annuaire (`resoudre_annuaire`), plancher de résolution (`hosts_statiques`), et `resoudre_base` nommé dans la ligne des bindings app→base. - **Cinq entrées d'index ajoutées** : réseau/pare-feu, ordre de déploiement, preuve/recette, exploitation courante, wiki — des pans entiers du corpus n'étaient pas indexés. - Reste ouvert, explicitement : `meta/liens.yml` sur le seul `serveur_postfix`, et `requiert` non consommé au déploiement (indice applicatif ; l'ordre, lui, vient des couches+graphe). Validation : `make verifier` vert (ansible-lint 514 fichiers, tests, syntax-check, **23 preuves CONFORME**). ## 2026-07-28 — figures annotées dans le wiki (console d'exploitation) ### Ajouté — les 8 vues de la console illustrées, dans le wiki La série des figures annotées (une par vue du GUI, en **SVG auto-contenu** : capture + repères intégrés en base64) est désormais **intégrée** à l'unité wiki [`Le GUI (console d'exploitation)`](wiki/Le-GUI-console-d-exploitation.md), en fin de section ②. Vues éditables (Serveurs, Applications, Bases × 2, Domaines) puis dérivées (Flux, Couches, Réseau). - **Symlink `wiki/img → ../docs/img`** — foyer unique des figures dans `docs/img/` ; les pages wiki y réfèrent en relatif (`img/*.svg`) sans duplication dans l'arbre source. - **`make wiki-publier`** embarque désormais les `docs/img/*-annote.svg` (déréférencés) dans le wiki Forgejo publié — c'étaient jusqu'ici les seules `.md` qui voyageaient, donc aucune image. ## 2026-07-24 — underlay (fabric physique, cluster-global) ### Ajouté — l'underlay comme concept de premier plan Le modèle dérive l'adressage **par tenant** (VLAN `1000+index×10+zone`), mais la **fabric physique** qui porte la flotte — mgmt des switches, mgmt Proxmox/OOB, iSCSI, Ceph — n'appartient à aucun tenant et ne dérive d'aucun `index`. Elle vit dans le *sous-sol* du modèle. Jusqu'ici elle n'était pas codifiée. Elle l'est. - **`scripts/underlay.py`** + **`make underlay`** — charge/affiche/**valide** `underlay.yml` : réseaux (nom, VLAN, sous-réseau, passerelle optionnelle → SVI, MTU/jumbo) et hôtes fixes documentés (les switches). La validation refuse toute **collision avec la plage tenant** : VLAN < 1000, sous-réseaux hors des supernets `10.(10+index).0.0/16`. - **`underlay.yml`** (racine du moteur, **gitignore** comme le vault ; gabarit public `underlay.yml.example`), surchargeable par `SETOPS_UNDERLAY`. Absent → tout reste inchangé. - **`make devis-reseau`** émet désormais une **section 0. Underlay** (VLANs, SVI, hints jumbo, IP des switches en commentaire) et ajoute les VLAN underlay au **trunk** Proxmox, avant les tenants. Respecte le dialecte (`cisco`/`binardat`). - **Preuve P23** — `underlay.py --verifier` : la fabric n'empiète pas sur la plage tenant. **Sautée** (⚪) si `underlay.yml` est absent (dépôt public), comme P16 sans vault. Le plafond tenant (245) est inchangé : l'underlay occupe `10.0.0.0/16 .. 10.10.0.0/16`, laissé libre par la dérivation (index ≥ 1 → `10.11+`). ## 2026-07-23 (suite 7) ### Ajouté — dialecte de CLI du commutateur (`devis-reseau`) Constat de l'opérateur : son switch est un **Binardat**, dont la CLI diffère de Cisco sur deux points que le devis généré ignorait — et deux pièges qui font passer un VLAN mais fuir un tenant : - **Masque d'ACL** — Cisco veut un masque **inversé** (wildcard `0.0.255.255`), Binardat un masque **normal** (`255.255.0.0`). Le SVI (`ip address … 255.255.255.0`) était déjà normal, donc valide sur les deux. - **`remark`** — Binardat n'a pas la commande de commentaire d'ACL de Cisco. Ces lignes faisaient rejeter le bloc. **`scripts/devis_reseau.py`** gagne un **dialecte** (`cisco` par défaut, `binardat`) : `masque_acl()` choisit wildcard ou masque normal ; `remarque()` omet les `remark` en Binardat. Réglable par `--dialecte`, par `SETOPS_DIALECTE`, ou `make devis-reseau DIALECTE=binardat`. Le GUI (lecture seule) suit l'env. Le code public reste **générique** (défaut `cisco`). ## 2026-07-23 (suite 6) ### Ajouté — plan de recette (le pendant manuel de `make prouver`) Constat de l'opérateur : les 78 exercices « ④ À toi de jouer » du wiki forment, ensemble, un **plan de tests d'acceptation**. Formalisé, sans dupliquer : - **`scripts/plan_recette.py`** + **`make plan-recette`** — **génère** `docs/audit/plan-de-recette.md` depuis les exercices du wiki : une grille auto-contenue (le geste inline) par unité, colonnes *Ce qu'on éprouve · Le geste · Type (observe/casse-répare) · **Preuve auto***. La dernière colonne est extraite du texte (le `Pxx` que l'exercice mentionne) : elle montre quels gestes manuels sont **aussi** gardés par la machine. Étant générée, la grille **ne peut pas dériver** du wiki. - **Preuve P22** — `plan_recette.py --verifier` échoue si le fichier committé n'est plus à jour (le wiki a changé sans régénérer). Le plan de recette devient un artefact **auto-gardé**. - **Honnêteté de couverture** assumée dans le document : un « — » = **manuel seul** (aucune preuve machine ne le double) ; le plan ne prétend pas à l'exhaustivité au-delà des exercices du wiki. C'est le **pendant humain** de `make prouver` : le harnais prouve le *moteur* (P01–P21), la recette valide l'*exploitation* — et sert de checklist au `protocole-operateur-independant.md` (« exploitable sans IA »). Validé : 78 gestes sur 19 unités, 5 doublés d'une preuve `Pxx` ; P22 testée (détecte une dérive) ; `make verifier` → **CONFORME 22/22** (contre une instance cohérente). ## 2026-07-23 (suite 5) ### Ajouté — wiki : l'axe « méthode » (KB enrichie) Le wiki enseignait les fondamentaux *services* (identité, PKI, courriel…) mais pas la *méthode* de Set-OPS. Cinq pages ajoutées, au moule à 4 temps (concept → Set-OPS → transférable → à toi de jouer), avec exercices concrets : - **Le plan & l'adressage dérivé** — un seed (`index`), tout en découle (DRY, source unique). - **Multi-instance & fédération** — un moteur, N écosystèmes ; découverte par convention. - **La preuve** — « ne jamais affirmer plus que ce qu'on prouve » ; le registre, `make prouver`, P01–P21. - **Le GUI (console d'exploitation)** — éditer la source, dry-run avant apply, l'invalide impossible à saisir (le `