Commit graph

103 commits

Author SHA1 Message Date
3b1d9b6660 SDN : make devis-sdn — zone, VNets et sous-réseaux dérivés du seed (D-43/44, P30)
Ajouter un tenant implique 1 zone EVPN + 6 VNets + 6 sous-réseaux sur le cluster.
Aucun générateur ne les produisait : dernière lacune dans un dépôt où tout dérive.
26 objets pour les deux tenants, VNI = 1000 + index×10 + zone, sous-réseau et
passerelle par les mêmes fonctions que l'inventaire.

Nommage, en deux temps. D'abord VRF0017 / v1174, alignés sur ce que le cluster
portait — réflexe inverse du bon : cette convention venait d'une création à la
main et ne disait pas de quel tenant il s'agissait. Forme retenue : t<index> pour
la zone, t<index><zone abrégée> pour le VNet (t17, t17serv). C'est le préfixe que
le pare-feu Proxmox utilisait déjà (t17-cli-metrique), donc un seul schéma dans
tout le dépôt. Abréviation = 4 premières lettres du libellé, accents retirés.

Pas de tiret entre index et zone, contrairement aux IPSets : t245-serv ferait 9
caractères, t245serv en fait 8 — la forme reste uniforme jusqu'au dernier index.

Contrainte cadrante : zones ET VNets sont limités à 8 caractères par Proxmox.
sdn-evpn.md annonçait chez17-services-infra (21) : il aurait été refusé à
l'application. P30 refuse tout dépassement et toute collision de nom, de VNI ou
de sous-réseau. Éprouvé aux bornes et par sabotage.

Vérification la plus forte : avant renommage, la dérivation reproduisait à
l'identique les deux zones créées à la main — nom, VNI de VRF, MTU, contrôleur.

voute.py saisir : le pendant de la génération. On génère un secret dont le dépôt
est la source, on saisit celui dont un tiers est la source — inventer une clé
d'API OPNsense donnerait une valeur refusée à la première requête, avec P18 au
vert sur une voûte inutilisable. Sans écho, double confirmation, rien sur la
ligne de commande.

Reste ouvert : aucun nœud de sortie déclaré. Le devis émet un marqueur, pas une
valeur plausible. Deux points à trancher — le nœud de sortie route selon sa
propre table (défaut actuel : 192.168.11.254, pas la frontière), et l'entrée
n'est pas redondante puisqu'elle dépend d'une route statique d'OPNsense vers un
seul nœud.

D-45 : l'affinité de VM attend Proxmox 9 (cluster en 8.4.19), tenue à la main.

30 preuves OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:06:42 -04:00
22ef279464 authentification : chaque rôle déclare sa position, gardé par P29
Une règle qu'aucune garde ne vérifie finit par ne plus être vraie — c'est ce qui
était arrivé aux 28 lignes d'intégration recopiées. Chaque rôle serveur_* porte
un meta/authentification.yml, confronté à son code par P29.

web-sso 5, socle-identite 2 (keycloak/openldap : ils SONT la chaîne d'identité),
ldap-direct 2, interne-sans-auth 2, sans-auth-humaine 12.

La preuve refuse l'oubli ET le mensonge. Éprouvée par sabotage sur sept cas :
déclaration supprimée, portée inventée, secours retiré, posture de formulaire
retirée, raison retirée, ldap-direct mensonger, réglage retiré des defaults.

Les deux derniers passaient dans la première version :

- le mensonge passait à cause d'un commentaire. Je cherchais le mot « ldap » dans
  le rôle, et serveur_grafana/defaults/main.yml contient « désactiver quelqu'un
  dans LDAP » : de la prose validait une déclaration fausse. La preuve exige
  maintenant un indice nommé — variable <rôle>_oidc / <rôle>_ldap, ou URI ldap://
- le réglage retiré passait parce que le gabarit citait encore la variable alors
  que plus rien ne lui donnait de valeur. La preuve lit defaults/main.yml en YAML
  et exige que la clé y soit définie, pas mentionnée.

Elle a aussi forcé une valeur : oauth2-proxy était déclaré « formulaire local
fermé » alors qu'il n'a aucun compte local. D'où formulaire_local: aucun, qui
distingue « il n'y en a jamais eu » de « il y en a un, il est fermé ».

Correction d'une note de la veille : Prometheus et Loki ne sont PAS exposés
publiquement (aucun expose au plan). Seuls six groupes le sont. Le risque est
intra-tenant, pas frontalier. Les deux lacunes sont comptées à chaque exécution,
pas masquées.

AFF-111, D-42. 29 preuves OK, ansible-lint (production) sur 375 fichiers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:05:43 -04:00
4dd3d46412 proxmox : pools créés, jeton normalisé, reliquat de voûte supprimé
Reconnaissance en lecture seule de l'API du cluster, avec le jeton de la voûte.
Trois valeurs devinées étaient fausses, et deux défauts bloquants sont apparus.

Corrigé d'après le cluster
- stockages : truenas-dbsql manquait ; le catalogue ne garde que ceux qui
  portent `images` (PBS, cephFS, local et truenas iSCSI n'accueillent pas de
  disque de VM) ;
- ponts : vmbr0 avait été omis, et l'uniformité sur les trois nœuds n'avait pas
  été vérifiée — un pont partiel empêche la VM de démarrer sur certains nœuds.

Pools
Chezlepro-17 et Technolibre-11 créés, dérivés comme le reste. Les pools
Env.Tenant antérieurs (Prod.Chezlepro, Lab.KBR...) sont l'ancien monde : on n'y
touche pas et on n'y verse pas la flotte générée. Diff réel : 2 pools ajoutés,
0 retiré, 1 VM sur 38 déplacée — infra-pki-01, qui n'avait aucun pool.

Défaut bloquant : make creer-vm aurait échoué en 401
proxmoxer recompose `utilisateur!nom` à partir d'api_user et d'api_token_id. La
voûte stocke la forme complète, que les playbooks passaient telle quelle, d'où
un 401 muet — alors que le même jeton fonctionne en curl. Mesuré des deux côtés
avec un module en lecture seule : forme complète = 401, forme courte = OK.
Normalisation par split('!') | last, qui accepte les deux écritures.

Reliquat proxmox.vault.yml supprimé
Toléré « en compatibilité », il restait le seul porteur du jeton chez
Technolibre — et comme *.vault.yml est gitignoré, ce jeton ne voyageait avec
aucun dépôt : une voûte unique (D-19) qui ne l'était pas. Migration faite en
mémoire, avec relecture et aller-retour de chiffrement vérifiés avant écriture ;
fichier supprimé, listes de chargement des playbooks nettoyées, validé par un
appel API réel ne chargeant que all/vault.yml.

La garde qui manquait
voute.py verifier ne comparait que le gabarit — il disait « complet » pendant
qu'un secret vivait ailleurs. Il contrôle maintenant aussi la voûte réelle quand
ANSIBLE_VAULT_PASSWORD_FILE la rend déchiffrable : noms de clés seulement,
jamais de valeur, et vérification sautée sans mot de passe.

Elle a trouvé un second trou dès son premier passage : la voûte réelle de
Technolibre n'a ni vault_nextcloud_admin ni vault_nextcloud_oidc, que le plan
exige. Non corrigé — générer ces secrets est une décision, et celui d'OIDC doit
correspondre à ce que Keycloak connaîtra.

27 preuves OK. --syntax-check et ansible-lint (production) sur les playbooks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:52:44 -04:00
60a60b6fb1 pools Proxmox : un par tenant, dérivé de l'index (D-37, P28)
Onze des quatorze serveurs portent le même nom court chez Chezlepro et chez
Technolibre. Vérifié un par un, ce n'est pas un problème technique : tout le
reste dérive du seed et diverge (10.27.19.21 contre 10.21.19.21, VMID 117402101
contre 111402101, VLAN 1174 contre 1114, deux domaines internes), et rien n'est
indexé sur le nom court — les opérations Proxmox portent toutes un vmid, les
certificats un FQDN, et client_backup_repo vise backup-01.{{ domaine_interne }}.

Le coût est humain : la console Proxmox affiche le nom, et deux infra-pki-01 y
sont indiscernables à l'œil. Le VMID porte le tenant, encore faut-il connaître le
codage.

Un pool par tenant, dérivé du dossier d'instance et de l'index — déjà unique par
P21, donc aucun registre de plus : Chezlepro-17, Technolibre-11.

make devis-proxmox-pools rattrape la flotte existante (création du pool, puis
affectation des VM actives). Les VM créées ensuite entrent d'elles-mêmes :
make creer-vm dérive le pool par la même fonction et le passe à la création. Le
playbook crée le pool au préalable — proxmox_kvm échoue sur un pool inconnu, et
l'API ne sait pas changer le pool d'une VM existante ; c'est aussi pourquoi le
rattrapage passe par les membres.

P28 garde deux collisions : même nom de pool entre tenants, et surtout même VMID
— une machine appartenant à deux tenants serait pire qu'une homonymie.

Rien n'est renommé : les homonymes sont la preuve que la nomenclature est un vrai
gabarit. Le devis ne lit pas le cluster, il dit l'état cible et non l'écart.

27 preuves OK, 0 échec. --syntax-check du playbook de clonage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:15:14 -04:00
b0e56cbfc0 intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.

1. Intégrations universelles (D-33/D-34, P26)

Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.

Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.

Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, 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érifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.

2. Vue Intégrations : la matrice

La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.

3. Propriété des intrants (D-35/D-36, P27)

Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.

Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.

26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
e9786700aa pare-feu Proxmox : le SSH inter-nœud était perdu
Je sautais le flux entier dès qu'un de ses pairs valait `externe`. Or le SSH
du socle est déclaré `[flotte, externe]` : la moitié `externe` relève 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 sauté pair par pair, jamais le flux entier. 36 groupes,
56 règles.

Ajouté : la liste des rôles sans règle entrante, avec leur motif. Onze rôles
sont injoignables sous DROP, et c'est voulu dans les onze cas — boucle locale
pour Prometheus, Redis, rspamd, Icinga et Unbound ; frontière seule pour
nginx ; aucun service pour `serveur_durci` et les clients. Un douzième motif
existe, marqué d'un avertissement : « flux entrants déclarés mais aucune
source résolue ici » — celui-là serait un vrai trou.

Preuves : 25 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 12:04:16 -04:00
7b2c9272d7 pare-feu Proxmox : une règle par rôle source, plus aucune adresse en dur
Un flux dont le pair nomme quatre rôles donne maintenant quatre règles,
chacune renvoyant à l'IPSet de son rôle. 52 règles, toutes par IPSet, zéro
littérale.

Le gain n'est pas cosmétique : une règle porte qui elle autorise.
`-source +t17-srv-keycloak` se lit ; une liste de quatre adresses demande de
retrouver à qui chacune appartient.

La raison appartient au flux, pas à chacune de ses règles : elle est écrite
une fois au-dessus du paquet qu'elle explique plutôt que répétée quatre fois.

Vérifié : aucun renvoi orphelin, aucun IPSet inutilisé.

Preuves : 25 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:59:58 -04:00
dbb4465602 pare-feu Proxmox : des IPSets partout où c'est possible, et rien d'inutile
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, `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éférencés sont émis — 6 par tenant. Un
devis crée ce qu'il liste.

Restent quatre règles en liste explicite, celles dont la source est plusieurs
rôles à la fois. Aucun IPSet unique ne les couvre et Proxmox n'accepte qu'une
référence par règle ; les éclater gonflerait le devis pour un gain
discutable.

Preuves : 25 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:53:38 -04:00
ece250ae49 pare-feu Proxmox : un seul schéma de nommage
Les IPSets portaient l'étiquette longue (`chez17_serveur_postgresql`), les
groupes l'index court (`t17-srv-postgresql`) : deux conventions dans un même
document. Tout porte maintenant `t<index>-` et la même forme abrégée.

La troncature reste propre à chaque objet — Proxmox est large sur les IPSets,
étroit sur les groupes. Un nom peut être entier d'un côté et abrégé de
l'autre ; chacun respecte sa contrainte, le préfixe reste commun.

Vérifié : aucune collision d'IPSet, et tout renvoi `+X` d'une règle pointe
vers un IPSet existant — 58 IPSets, 34 groupes, aucun orphelin.

Preuves : 25 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:50:50 -04:00
6de44736ff pare-feu Proxmox : 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. Technolibre listait donc ses 14 VM (zéro actif, repli déclenché)
et 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 la même règle. 14 de chaque côté, toutes avec leur VMID.

Preuves : 25 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:47:13 -04:00
fd62b59989 pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.

Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.

Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.

Garde ajoutée après coup : 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
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.

Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.

Preuves : 25 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
89dc5ed3d4 underlay : le MTU du modèle était faux, et la garde le validait
En préparant le déplacement des VTEP, la lecture des interfaces a montré que
`underlay.yml` annonçait 9000 alors que vmbr3 est à 1500. La garde P23
exigeait >= 1550 et passait parce que le fichier mentait. Une garde qui
valide une déclaration plutôt qu'une réalité donne un faux confort — pire
qu'une garde absente, qui au moins n'endort personne.

Le seuil ne peut pas être fixe non plus : 1550 aurait rejeté à tort un
transport à 1500 portant un overlay à 1450, qui tient exactement. Il dérive
d'un `mtu_overlay` déclaré : transport >= overlay + 50. Exercé.

Trouvé aussi : vmbr3 n'est pas VLAN-aware. L'adresse du VTEP y est non
étiquetée et vit dans le VLAN natif du port. Déplacer le VTEP n'est donc pas
un changement d'adresse — il faut un VLAN natif 10 ou une interface étiquetée
dédiée. C'est pourquoi le déplacement n'a pas été effectué.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:28:16 -04:00
6a80e5b55c underlay : un rôle par hôte, et les hyperviseurs au modèle
Redresser les pairs EVPN vers l'underlay suppose que les hyperviseurs
existent dans le modèle. Ils n'y étaient pas.

La reconnaissance a montré la cause : vmbr3 porte 10.27.19.{41,43,47} sur les
trois nœuds — l'adresse des VTEP est prise dans le supernet de Chezlepro. Le
modèle refuse d'exprimer cet état : déclarer 10.27.19.0/24 en underlay ferait
échouer P23. La garde détecte la faute avant qu'on ne la documente.

Ajouté un `role` sur les hôtes (switch par défaut, hyperviseur, frontiere) :
le réseau ne suffit pas à le déduire, et un hyperviseur déclaré recevait une
configuration de commutateur en partie B.

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 », mais
`partie_acces()` gardait sa copie locale du filtre et ne l'appelait jamais.
Les deux ont divergé au premier hôte non-commutateur. Écrire « source
unique » dans un commentaire ne la crée pas.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:21:30 -04:00
70fe6de557 flux : l'ICMP entre au registre — l'overlay à 1450 l'exige
Décision : l'overlay EVPN plafonne à 1450. Conséquence invisible : 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 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é : 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 ; le champ `port` y porte le type
(`frag-needed`). Le socle déclare les deux sens. Vérifié : nftables d'hôte
inchangés, le pair `externe` reste sauté.

Reconnaissance (lecture seule) : l'EVPN est à moitié construit — contrôleur
EVPN0017 (ASN 65000), zones VRF0011 et VRF0017, un VRF par tenant avec le VNI
égal à l'index. Aucun VNet, aucun nœud de sortie.

Signalé et non corrigé : les pairs BGP sont dans 10.27.19.0/24, le
sous-réseau Services-infra de Chezlepro. Le transport du cluster dérive de
l'index d'un tenant, et une VM de cette zone partage son sous-réseau avec les
VTEP — l'isolation est percée à l'endroit que l'EVPN devait fermer.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:07:30 -04:00
1917545126 audit : les preuves réseau étaient accrochées à la mauvaise affirmation
P21, P23 et P24 renvoyaient à AFF-001 — « Set-OPS est un moteur Ansible
générique » — sans rapport avec la fédération, l'underlay ni la frontière.
P17, P19 et P20 n'avaient aucune référence. Une preuve accrochée à la
mauvaise affirmation passe au vert et n'atteste de rien de ce qu'on croit.

Ajouté §10 du registre : six affirmations (AFF-101..106) pour l'architecture
réseau et la fédération. La couverture du plan par le panneau est en 🟡, avec
ses exceptions nommées — listes de tables de l'underlay, ports physiques,
nœud de sortie.

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 au
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é aussi : P03, P06, P12 et P13 portent maintenant les références que la
table leur attribuait déjà — la correspondance existait en double et seul le
document la tenait. Et la table attribuait AFF-030 (« inventaire complet ») à
P15, qui valide le modèle socle ; c'est P16 qui exécute
`ansible-inventory --list`.

35 affirmations référencées, aucune référence orpheline.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 09:36:30 -04:00
a85255dc35 GUI : le panneau présente les deux devis, et son aide n'est plus périmée
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. Import paresseux
et gardé — ce module lit l'underlay et les inventaires de tous les tenants,
une erreur aurait sinon vidé la vue entière.

Aide corrigée sur trois points périmés : les VLAN tenants annoncés « uniques
sur le trunk » (faux en SDN, aucun n'y circule) ; le dialecte renvoyé à une
variable d'environnement alors que c'est un intrant de la section Fabric ; et
la route par défaut dite « à adapter » alors que la section 5 l'émet.

Elle dit maintenant ce qui reste à nommer à la main : les ports physiques et,
en SDN, le nœud de sortie EVPN.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 08:52:22 -04:00
940e319285 frontière : la description du lien réutilisait le marqueur du prochain saut
Régression de la correction précédente. 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 <NOEUD-DE-SORTIE-EVPN>`.

Sur ce lien, le commutateur est à 10.0.4.6 — le nœud de sortie n'y figure
pas, il est joignable À TRAVERS. Le champ décrit maintenant le câblage,
indépendamment du routage.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 08:42:54 -04:00
58fdaa5540 frontière : le devis rattrape la bascule SDN, et deux devis se contredisaient
Le prochain saut des routes tenants pointait le SVI du commutateur. En EVPN
il ne route plus les tenants : la route arriverait sur un équipement sans
chemin vers le tenant — configuration qui s'applique sans erreur et ne
fonctionne pas. Le devis émet `<NOEUD-DE-SORTIE-EVPN>` et dit pourquoi.

`underlay.passerelle_sortie` garde son sens : adresse du pare-feu sur le lien
de transit, donc sortie de l'UNDERLAY. Deux choses distinctes.

Corrigé aussi une contradiction antérieure au SDN : la section 0 demandait de
router l'administration vers 10.0.4.6, le SVI du commutateur lui-même, alors
que `devis-reseau` émet 10.0.4.1, l'adresse du pare-feu — tout en affirmant
que l'autre devis « émet déjà ces routes ». Elles coïncident maintenant,
vérifié ligne à ligne.

Et deux commentaires qui affirmaient l'inverse de la décision se dérivent du
mode de routage.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 08:40:20 -04:00
87697597fb devis switch : 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 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 de retour en mode `switch`.

Corrigé aussi l'en-tête, qui annonçait les VLAN tenants « uniques globalement
sur le trunk » — ils ne sont plus sur aucun trunk en SDN.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 08:35:44 -04:00
e5ce2b93b1 devis switch : le SDN prend le routage tenant, le devis se vide
Trois décisions appliquées : une zone EVPN par tenant, routage ET filtrage
inter-zone à ce niveau, inter-tenant obligatoirement par l'OPNsense.

`underlay.routage_tenants` (`switch` par défaut, `sdn`) : en SDN le devis
cesse d'émettre VLAN tenants, SVI et ACL, et les retire des trunks. Chez
Chezlepro les trunks passent de quinze VLAN à deux — seul du VXLAN circule,
que le commutateur transporte sans le lire.

Le MTU devient une garde : `make underlay` refuse un transport sous 1550 en
mode SDN, en disant pourquoi — sous ce seuil le ping passe et les transferts
échouent.

Le partage des responsabilités est écrit dans docs/sdn-evpn.md : qui route,
qui filtre, pour chaque nature de trafic. Deux conséquences nommées —
l'inter-tenant ne peut plus être oublié (il traverse une bordure en block par
défaut), et le commutateur ne voit plus rien du trafic tenant.

Point ouvert : le registre des flux n'a aucun mot-clé pour un flux
inter-tenant. Défaut sûr, mais on ne peut pas déclarer d'exception légitime.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 08:26:21 -04:00
059d76a536 devis switch : l'ACL inter-tenant devient une capacité déclarée
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 jamais liées — qui
auraient l'air d'isoler sans jamais filtrer — la capacité se déclare :
`underlay.acl_inter_tenant`, `true` par défaut.

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.

À `false`, la section 3 ne contient plus de règles mais la raison, et surtout
ce qu'on perd : une VM émettant vers l'underlay est routée localement vers le
mgmt des switches, celui de Proxmox et l'OOB/IPMI. Les nftables des VM n'y
peuvent rien (politique `output` permissive), et l'IPMI n'est pas un hôte
géré.

Des VRF auraient donné cette isolation sans ACL — critère à retenir au
prochain renouvellement. Parade d'ici là : sortir le management de la fabric
routée des tenants, comme l'est déjà le stockage.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 00:45:56 -04:00
b5ca369ae7 devis switch : 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 <NOM> in` posée sur les douze SVI n'existe 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.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:11:15 -04:00
ed5fb25b14 devis switch : spanning-tree vérifié, les six familles de syntaxe 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 nuit autant 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.

Six familles vérifiées contre le matériel : VLAN, SVI, trunks, routes, ACL,
spanning-tree. Deux ont révélé un défaut réel plutôt que de confirmer
l'existant — les routes (CIDR) et les trunks, dont `add` ne retranchait rien.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 19:28:22 -04:00
0c55986e81 devis switch : le mode STP se dérive, et le devis dit son incertitude
`underlay.stp.mode` passe à `mstp` — le mode d'usine du commutateur, vérifié
par `show spanning-tree`. 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 son incertitude là où elle est : en MSTP la priorité se règle
souvent par instance, alors que la forme émise est globale. Il 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é : le commentaire de la section 6 disait « RSTP » en dur alors que le
mode est déclaré. Il le dérive.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 18:52:37 -04:00
34e65259ac devis switch : le trunk ne restreignait rien
`switchport trunk allowed vlan ?` sur le matériel 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 config réelle, les ports n'ont aucune ligne `allowed vlan`, ce qui
signifie exactement cela. Y ajouter la liste voulue n'en retranchait aucun :
le trunk continuait de tout transporter, et le devis donnait l'illusion de
restreindre. Le pire genre de défaut — ça a l'air juste, ça s'applique sans
erreur, et ça ne fait pas ce que ça annonce.

La forme sans mot-clé est aussi atomique : `none` puis `add` couperait le
trunk entre les deux commandes, ce qui suffit à perdre la session si on
l'applique sur le port de gestion.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 18:15:22 -04:00
24959359e2 devis switch : syntaxe des routes vérifiée sur le matériel Binardat
Un `show running-config` du commutateur 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 maintenant le dialecte, comme les masques d'ACL.
Vérifié dans les deux formes.

Restent non vérifiés faute d'apparaître dans la config 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.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 18:03:37 -04:00
517119c7ac frontière : ses intrants appartiennent à l'hébergeur, pas au tenant actif
Un hébergeur sert plusieurs tenants et n'a qu'une frontière. Ses intrants
é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.

Même famille que le défaut de l'underlay corrigé plus tôt ; c'est la
distinction hébergeur/tenant qui le fait apparaître.

Le devis et le panneau lisent maintenant la frontière chez l'hébergeur, qui
n'est pas déclaré pour autant : le symlink `underlay.yml` le désigne déjà.
Repli sur l'instance active sans underlay monté.

Vérifié : devis identique avec l'hébergeur actif, intrants conservés avec un
invité actif.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:17:10 -04:00
7256486c9c modèles : l'underlay devient modélisable, et P17 le valide
Un modèle décrivait un tenant — ses services, ses zones, ses bases. Tous les
hébergeurs n'ont pas le même matériel : l'infrastructure physique mérite le
même traitement.

Le modèle public gagne un underlay volontairement minimal (un commutateur,
pas de fabric de stockage séparée), point de départ honnête d'un petit
hébergeur. Les montages plus riches 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.

`modeles.py verifier` le valide (P17), facultativement et sur sa cohérence
INTERNE seulement — pas contre les tenants fédérés réels, un modèle étant un
gabarit et non un site déployé. Il a fallu rendre paramétrables deux
hypothèses du validateur, qui lisait la nomenclature de l'instance active et
globait les dépôts frères ; comportement par défaut inchangé.

Cinq cas de rejet exercés : 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.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:11:18 -04:00
0eae97c6ec underlay : monté par symlink depuis le dépôt de l'hébergeur
Il vivait gitignoré à la racine du moteur : consommé par deux générateurs,
validé par P23, versionné nulle part. La dizaine de modifications de la
journée n'était récupérable d'aucune façon, et un clone frais repartait du
gabarit.

Il appartient à l'hébergeur — ses switches, ses câbles — 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 initiale.

Le moteur le monte par symlink comme il monte le plan par `instance/`. Ce
lien NE SUIT PAS `make instance-utiliser` : la fabric reste celle de
l'hébergeur quel que soit le tenant actif.

Vérifié : les deux devis sortent identiques octet pour octet avant et après,
P23 verte, 24 preuves. Symlink brisé (clone sans dépôt frère) : dégrade
proprement, sections omises au lieu d'échouer. Cas exercé.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:05:12 -04:00
91bbad0cdf frontière : les routes de retour couvrent tous les tenants, et la case WAN
Le devis frontière annonçait trois routes de retour « déjà émises par
devis-reseau ». Le devis switch n'en émettait qu'une : il lisait
`nftables_admin_ssh` de la seule instance active, alors que la frontière
était passée multi-tenant. Les réseaux d'administration de Technolibre
n'étaient routés nulle part — et une affirmation fausse est pire qu'un
silence, elle désamorce la vérification.

`admin_tous_tenants()` vit dans devis_reseau et devis_opnsense l'importe au
lieu d'en refaire une copie : routes de retour et règles lisent les mêmes
tenants par construction. Vérifié identiques.

Ajouté : l'avertissement « Block private networks ». Le SSH d'administration
a une source RFC1918 arrivant sur une interface WAN, où ce filtre est actif
par défaut et s'applique AVANT les règles — coché, il jette le paquet sans
qu'aucune règle ne soit consultée. Un réglage d'interface est invisible dans
les règles, il fallait l'écrire à part.

Prédicat exactement RFC1918, périmètre de cette case ; `is_private` aurait
été trop large (documentation, CGNAT) et l'avertissement se serait déclenché
à tort. Trois cas exercés : RFC1918 averti, 8.8.8.8 muet, 203.0.113.7 muet.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:17:25 -04:00
26090e8acd frontière : chaque règle porte son interface d'arrivée
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 du sens du flux : `ingress`/`externe` arrive par le
WAN, `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. Le montage parallèle décrit ce jour lève la dernière
inconnue.

Le rendu abandonne `pass out` pour `pass in on <interface>`, l'idiome réel
d'OPNsense et ce que le client d'API devra envoyer.

Ajouté aussi :
- `opnsense_wan_ip`, la face publique, en section Frontière du panneau ;
- invariant du dernier octet (P23) : un point de routage porte le même
  dernier octet sur tous ses sous-réseaux. Le chiffre vient de
  `reservations.passerelle`, pas d'une constante. Exemption des liens plus
  étroits qu'un /24 — sur le /29 de transit l'adressage est dicté par les
  participants. Vérifié que l'invariant tenait déjà sur les 13 sous-réseaux
  routés avant d'écrire la garde.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:06:55 -04:00
53f6af3f1a frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, 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'une ligne dise pourquoi.

La résolution est 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, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.

Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.

Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
4d37570c0c frontière : la sortie générale est déclarée, pas subie
Le devis 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
d'apt, plus de NTP, plus de récursion DNS. Rien ne le signalait — la ligne
la plus lourde de conséquences du devis, posée à la suite des autres.

La sortie est déclarée dans le registre, donc dérivée :
- `serveur_debian` (socle, 14 hôtes) : 443 et 80 pour les dépôts apt, 123/udp
  pour l'horloge — une dérive fait échouer step-ca et le SSO des semaines
  après la cause ;
- `client_unbound` : 53 udp et tcp, la récursion depuis la racine qu'implique
  `client_unbound_transitaires: []`. Le TCP 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.

Vérifié : les nftables d'hôte sont inchangés, octet pour octet. Le pair
`externe` reste sauté par resoudre_flux.

Non déclaré volontairement : le rôle `chrony` n'est référencé par aucun
groupe ni playbook — un flux pour lui aurait été une règle morte.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 13:10:45 -04:00
9804573f12 frontière : les intrants d'interface demandent l'identifiant OPNsense
`opt1`, `igb1` et `TENANTS` désignent le même port — identifiant interne,
périphérique FreeBSD, libellé affiché. L'API REST ne parle que du premier.
Les libellés des deux intrants ne le disaient pas, et la question s'est
posée en pratique.

Ils le disent maintenant, et docs/frontiere-opnsense.md explique les trois
couches et pourquoi `opt1` est le plus stable : il survit à un changement de
carte comme à un renommage.

La note « prochain_saut dérive de l'underlay » est repliée dans l'en-tête
régénéré par le panneau : une sauvegarde l'effaçait, le fichier étant
réécrit depuis le YAML analysé. Vérifié qu'une sauvegarde préserve valeurs
et références de voûte.

Corrigé au passage : après le renumérotage du /29, deux passages de la doc
annonçaient encore 10.0.4.2 comme prochain saut et contredisaient le devis.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 13:02:27 -04:00
7dc7720b4b client_pki : l'empreinte du root CA n'est pas un secret de voûte
Le rôle dérive l'empreinte à chaud depuis l'autorité, parce qu'un from-zero
régénère l'AC avec une empreinte neuve. Figée en voûte, elle 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 un défaut lisant
`vault_step_ca_fingerprint`. Ce défaut était mort : la tâche suivante écrase
le fait sans condition, donc la valeur de la voûte n'avait aucun effet.

Le recensement de voute.py s'y laissait prendre — il cherche la chaîne
`vault_*` sans pouvoir savoir qu'un défaut n'est jamais lu. La « source
unique » avait hérité de l'erreur, et le panneau réclamait un secret
impossible à fournir avant que l'AC n'existe.

Défaut retiré : la clé disparaît du recensement (25 -> 24 exigés), du
gabarit et du panneau. `client_pki_ca_fingerprint_override` reste le moyen
d'épingler une empreinte.

`docs/intrants-communs.md` §H était une troisième copie manuelle de la liste
des secrets, avec les deux mêmes erreurs ; elle renvoie maintenant à
`voute.py lister` et à la preuve P18.

Preuves : 24 OK, 0 échec ; voute.py verifier --strict passe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:37:27 -04:00
8bc8589163 GUI : le rappel des secrets dérive de voute.py
`SECRETS_ATTENDUS` était une liste tenue à la main, en parallèle du
recensement que voute.py fait déjà depuis le plan, les rôles et les
group_vars. Deux sources pour la même vérité, et la manuelle avait divergé :
elle annonçait `vault_ldap_sssd`, que rien ne consomme — aucun rôle sssd
n'existe — et omettait `vault_step_ca_fingerprint`, réellement lu par
client_pki pour vérifier l'empreinte de l'AC racine.

Un opérateur qui suivait le panneau créait un secret inutile et en oubliait
un nécessaire.

Le rappel dérive maintenant de `voute.secrets_exiges()` — la même source que
la preuve P18 — augmentée de SECRETS_HORS_MOTIF pour les jetons Proxmox, qui
n'ont pas le préfixe `vault_`. Vérifié : 27 noms, écart nul avec le gabarit.
Dépôt public nu : liste vide plutôt qu'une erreur d'affichage.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:25:41 -04:00
d00b60149d GUI : section « Fabric » — l'underlay se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui 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 ; l'underlay, non.

La section couvre les valeurs plates : switch routeur, dialecte de CLI, mode
et topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, 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. C'est une propriété du matériel, donc de la fabric.
Précédence : `--dialecte` > environnement > intrant déclaré > cisco.

Écriture chirurgicale plutôt que safe_dump : underlay.yml porte 23 lignes de
commentaires qui expliquent des décisions d'architecture, et un dump les
aurait effacées comme c'est arrivé à plan/applications.yml. Vérifié : trois
valeurs modifiées, 73 lignes et 23 commentaires avant comme après. Une clef
absente du fichier est refusée plutôt qu'inventée à un endroit arbitraire.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:01:00 -04:00
76e82bfb96 underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.

Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).

Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.

La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.

Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
f80b53b592 devis switch : 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 — alors que la partie B posait tout dans le
même bloc. Deux conventions pour la même chose dans un seul document.

Le portfast est posé avec son interface (sections 4 et 4b). La section 6 se
réduit au global — mode, priorité du pont racine — plus les deux
avertissements : les rayons de la 4c n'en sont volontairement pas, et
pourquoi BPDU guard n'est pas émis.

Vérifié : aucune interface déclarée deux fois dans une même partie,
3 portfast avec `stp`, 0 sans lui, 0 sur un rayon.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:35:32 -04:00
92d31c0406 devis switch : les rayons de l'étoile ne sont pas des ports de bord
L'ajout du spanning-tree venait de rendre dangereuse une imprécision de
titre. 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 revenait à mettre portfast sur les
liens qui portent les BPDU — désactiver la protection anti-boucle là où elle
sert.

Les rayons sont dérivés et émis à part (4c côté routeur, B3a côté accès), un
par switch d'accès, marqués comme non-bord. La section 4 ne désigne plus que
les hyperviseurs ; la partie B distingue sa montante de son trunk terminal.

`underlay.switches_acces()` devient la source unique du « qui est un switch
d'accès » — pour leur devis ET pour les rayons côté routeur.

Vérifié : 3 commandes portfast avec `stp` déclaré, 0 sans lui, 0 sur un
rayon.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:31:31 -04:00
8f61721cb3 spanning-tree : RSTP dérivé de la topologie déclarée (étoile)
Le devis ne disait rien du spanning-tree. Sur une fabric convergée à trois
switches, une boucle par brassage accidentel est une tempête de diffusion.

La topologie se déclare (`stp: {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 plutôt qu'une élection arbitraire. Les
switches d'accès reçoivent une priorité haute — jamais racine.

Ports terminaux déclarés en bord de réseau. BPDU guard délibérément NON
é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 au lieu de laisser croire à une protection indispensable.

Validation : `mode` et `topologie` contrôlés, `stp` sans `routeur` refusé
(aucun pont racine désignable). Sans `stp`, la section signale l'absence de
protection au lieu de disparaître.

Réserve : la forme `binardat` du spanning-tree n'est pas vérifiée sur le
matériel, comme les `ip route`.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:50:55 -04:00
42cd269d29 devis switch : le roster d'hôtes se limite à la fabric configurée
Les VLAN et les trunks étaient déjà filtrés par fabric ; le roster de la
section 0, non. Un équipement d'une autre fabric — les switches de stockage,
le jour où ils seront déclarés — serait apparu en commentaire dans un devis
qui ne le concerne pas. 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.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:41:49 -04:00
31e5121d68 underlay : chaque réseau déclare sa fabric physique
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 porte une `fabric` (`principal` par défaut). Le devis ne
configure que celle du routeur, et énonce explicitement ce qu'il ne couvre
pas au lieu de le taire — VLAN, trunks et spanning-tree d'une autre fabric
ne le regardent pas.

Les `deny` de l'ACL couvrent en revanche toutes les fabrics : 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é.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:36:02 -04:00
b613873c3a nommage : bifrost aux frontières, sleipnir à la fabric interne
`bifrost-0` et `bifrost-1` 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.2,
10.0.4.3) : 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.

Conséquence traitée : la partie B du devis aurait listé les deux pare-feux
parmi les « switches d'accès ». Elle ne retient plus 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é.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:18:16 -04:00
7e852b5eef underlay : le routeur porte le SVI, et le transit sort de la partie B
Deux incohérences visibles à la relecture du devis complet.

`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éclaré à l'adresse du SVI qu'il porte, et `make underlay` refuse désormais
la divergence.

La partie B créait `vlan 40` alors que son trunk ne le transporte pas — le
transit ne relie que le routeur à la frontière. VLAN exclu de la partie B,
comme il l'est déjà des trunks généraux.

Preuves : 24 OK, 0 échec ; garde de cohérence exercée.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:10:42 -04:00
2d2c5c69fa devis switch : un seul routeur, les autres en L2 pur
Sans MLAG, le routage est porté par un unique switch (`underlay.routeur`).
Le devis émettait 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.

Il se scinde désormais en deux :
- partie A (switch routeur) : VLANs, SVI, ACL, trunks, routes ;
- 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.

Gardes : `make underlay` refuse un `routeur` inconnu des hôtes déclarés ; la
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine.
Sans routeur désigné, l'en-tête signale le risque de duplication au lieu de
laisser croire que le devis est applicable partout.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:04:38 -04:00
7b2ba07ffc ACL de switch : les tenants n'atteignent plus la fabric physique
L'ACL d'isolation bloquait l'autre tenant, puis se terminait par
`permit ip <tenant> any`. Ce `any` autorisait 10.27.x -> 10.0.0.0/24 : le
management des switches, celui de Proxmox et l'OOB/IPMI, plus iSCSI et Ceph.
Une VM compromise atteignait la console physique des hyperviseurs.

Le commentaire du générateur disait « Reste -> passerelle OPNsense », ce qui
est faux pour l'underlay : ce trafic est routé LOCALEMENT par le switch et ne
passe jamais par la frontière, donc elle ne le filtre jamais.

Un `deny` par sous-réseau underlay est désormais émis avant le `permit`
final, dérivé de underlay.yml, dialecte respecté (masque normal ou wildcard).
Vérifié qu'aucun flux du registre ne vise l'underlay : rien de déclaré ne
casse. Sans underlay déclaré, l'ACL retrouve sa forme d'avant.

Consigné en §6 : le registre n'a pas de mot-clé `underlay` (un besoin
légitime, superviser l'hyperviseur, ne pourrait pas être déclaré), et le
devis émet un jeu unique de SVI pour trois switches sans MLAG.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:59:03 -04:00
0d64d28402 devis switch : le port vers la frontière, et l'ordre d'application
Deux omissions que le devis faisait porter à l'opérateur.

Le VLAN de transit était étiqueté sur le trunk `<PORT-VERS-PROXMOX>`, et
aucune interface vers le pare-feu n'était émise. 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 transit sort du
trunk Proxmox et prend son propre port (section 4b), dérivé de l'underlay.

Les routes de la section 5 déplacent la sortie du switch, y compris celle de
ses propres réponses. Tant que 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é à l'équipement depuis lequel on travaille.
Le devis les crachait à la suite des autres comme si elles étaient
équivalentes ; il énonce maintenant les préalables et l'ordre.

Sans transit déclaré, les deux sections s'affichent en clair comme
manquantes plutôt que de disparaître.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:52:50 -04:00
069e549160 frontière : le prochain saut dérive du transit, il n'est plus saisi
`opnsense_prochain_saut` quitte le panneau Intrants. Il dérive du réseau de
transit de l'underlay — la `passerelle` du réseau portant `passerelle_sortie`.

Un seul bloc alimente les deux devis : 10.0.4.1 est le SVI côté switch ET le
prochain saut des routes tenants côté frontière ; 10.0.4.2 est la route par
défaut du switch. Le saisir en doublon rouvrait la possibilité de deux
valeurs contradictoires pour un seul lien — le mode de panne qu'on venait de
fermer pour les réseaux d'administration.

Le devis frontière expose le bloc `transit` (JSON compris) et cesse de
réclamer en section 0 les routes que `devis-reseau` émet désormais : il dit
lesquelles sont déjà émises, ou signale l'absence de transit déclaré. Sans
underlay, le marqueur revient — le repli reste explicite.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:38:17 -04:00
3698152b6a frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.

`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.

Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.

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. Les trois pare-feux et les routes ne peuvent plus diverger.

La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.

Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
  garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
  avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
  secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
  playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
  reliquat de compatibilité.

Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00