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>
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>
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>
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>
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>
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>
`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>
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>
Les frontières occupent le bas du /29, le SVI du switch le haut. `.3` reste
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é, et ce sera le seul changement.
Plan du lien :
10.0.4.1 bifrost-1 frontière active, sortie par défaut de la flotte
10.0.4.2 bifrost-2 seconde frontière
10.0.4.3 libre réservée VIP CARP
10.0.4.6 sleipnir-01 SVI du switch routeur
Les deux devis suivent sans intervention : routes du switch vers 10.0.4.1,
routes tenants de la frontière via 10.0.4.6.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`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>
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>
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>
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>
`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>
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>