Set-OPS-Public/docs/frontiere-opnsense.md
Daniel Allaire 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

18 KiB

La frontière OPNsense (nord/sud)

Le pare-feu de bordure de l'écosystème. Ce document fixe les décisions d'architecture prises le 2026-07-29, et décrit le devis dérivé qui en découle (make devis-opnsense). Il complète docs/flux-conception.md (le modèle des flux) et docs/registre-flux.md (la matrice d'audit).

1. Le partage des rôles

Trois pare-feux coexistent dans le modèle, et chacun voit une chose différente :

Quoi Généré par
Sur chaque hôte nftables policy drop, moindre privilège par IP source make flux (resoudre_flux.py)
Sur le switch routeur ACL d'isolation inter-tenant, appliquées in sur les SVI make devis-reseau
À la frontière (OPNsense) ce qui entre et sort de l'écosystème make devis-opnsense

Décision : OPNsense est une frontière nord/sud, pas la passerelle des zones. Les SVI restent sur les switches L3, qui continuent d'assurer le routage inter-zone au débit ligne. OPNsense ne porte aucun SVI de tenant et ne voit pas le trafic est-ouest.

Décision (2026-08-01) : un seul switch route. Sans MLAG, le routage est porté par un unique switch, désigné par underlay.routeur ; les autres restent en L2 pur et n'ont d'adresse IP que pour leur propre gestion. make devis-reseau émet en conséquence deux configurations distinctes — partie A pour le routeur, partie B pour les switches d'accès — plutôt qu'un devis unique que l'on serait tenté d'appliquer partout, ce qui dupliquerait les SVI et créerait autant de conflits d'adresses qu'il y a de zones. make underlay refuse un routeur qui ne nomme aucun hôte déclaré.

Conséquence assumée : l'isolation inter-zone repose sur les ACL de switch, moins expressives que le registre des flux. Le zéro-confiance est-ouest reste porté par les nftables d'hôte et le TLS mutuel — pas par le pare-feu de bordure.

Corollaire, qui n'est pas anodin : le trafic d'un tenant vers l'underlay est routé localement par le switch et ne passe jamais par la frontière. Il n'est donc jamais filtré par elle. Sans précaution, le permit ip <tenant> any final de l'ACL laisserait une VM atteindre le management des switches, celui de Proxmox et l'OOB/IPMI. make devis-reseau émet pour cette raison un deny par sous-réseau underlay avant le permit final, dérivé de underlay.yml.

2. Hébergeur et tenants — qui possède quoi

Un hébergeur possède le matériel et sert plusieurs tenants ; il a en général son propre tenant par défaut. Chezlepro est les deux à la fois, ce qui masque la distinction — mais elle décide de l'emplacement de chaque chose :

Objet Appartient à Vit dans
plan des services, inventaire le tenant OPS-<tenant>
underlay.yml (fabric physique) l'hébergeur OPS-<hébergeur>, monté par symlink
group_vars/opnsense.yml (frontière) l'hébergeur idem — une frontière pour tous ses tenants

Conséquence pratique : make instance-utiliser bascule le tenant actif, jamais l'hébergeur. Le devis lit donc la fabric et les intrants de frontière chez l'hébergeur, quel que soit le tenant actif — sans quoi basculer sur un invité ferait disparaître l'URL de gestion, l'adresse publique et les interfaces, et le devis repartirait en marqueurs.

L'hébergeur n'est pas déclaré : le symlink underlay.yml le désigne déjà, et une seconde déclaration ouvrirait la porte à deux valeurs contradictoires.

3. Ce que la frontière décide (et pourquoi rien n'est saisi à la main)

La frontière est un équipement partagé, comme les switches. Elle route vers tous les tenants fédérés, elle porte donc aussi leurs règles — pas seulement celles de l'instance active. Un tenant routé sans règles serait joignable jusqu'à la bordure puis bloqué en silence : chemin présent, politique absente, exactement le mode de panne du 2026-07-29.

Les alias d'hôtes sont préfixés du tenant (SETOPS_CHEZ17_SERVEUR_NGINX), et surtout chaque tenant a son propre alias d'administration : SETOPS_ADMIN_CHEZ17 n'ouvre que 10.27.0.0/16. Une union aurait laissé le plan de gestion d'un tenant entrer chez le voisin — ce que les ACL de switch interdisent par ailleurs. La bordure ne doit pas rouvrir ce que l'isolation inter-tenant ferme.

Deux omissions sont annoncées plutôt que tues : un tenant sans inventaire généré (aucune règle) et un tenant dont nftables_admin_ssh est vide (règle SSH omise — l'ouvrir à any exposerait le SSH à Internet).

Le registre des flux distingue les pairs par mot-clé. Or resoudre_flux.py saute volontairement le pair externe (scripts/resoudre_flux.py:184) : ces flux-là ne concernent pas le pare-feu d'hôte, ils relèvent de la bordure. Plusieurs raison le disent déjà noir sur blanc — « Frontière publique gérée à l'OPNsense ».

Autrement dit : la politique de la frontière était déjà écrite dans le registre, et il ne restait qu'à la dériver. C'est ce que fait scripts/devis_opnsense.py, à partir de :

  • roles/*/meta/flux.yml — les flux pair: externe, qui donnent les règles ;
  • l'inventaire de l'instance active — quels hôtes portent quel rôle, donc les destinations ;
  • ../*/plan/nomenclature.yml — les supernets des tenants fédérés, donc les routes ;
  • l'intrant nftables_admin_ssh — les réseaux d'administration.

Aucun port, aucune adresse et aucun nom d'hôte n'est écrit dans le générateur.

4. L'interface de chaque règle, et l'invariant du dernier octet

Dans OPNsense, une règle est toujours in sur l'interface d'arrivée — celle par laquelle le paquet pénètre le pare-feu. Posée ailleurs, elle ne s'applique jamais, et le trafic est bloqué sans que rien ne paraisse anormal dans la configuration. L'attribution se dérive du sens du flux :

Flux déclaré Arrive par Règle
sens: ingress, pair: externe le WAN (69.70.26.62) pass in on wan …
sens: egress, pair: externe le lien de transit (opt1) pass in on opt1 …

Le SSH d'administration suit la première ligne : le VPN est hébergé sur le pfSense voisin, et son trafic revient par l'adresse publique de la frontière.

Le piège de la case à cocher. Ce trafic d'administration a une source RFC1918 (192.168.255.0/24) alors qu'il arrive sur une interface WAN. Or OPNsense active par défaut Block private networks sur le WAN, et ce filtre s'applique avant les règles : coché, il jette le paquet sans qu'aucune règle ne soit consultée. La configuration paraît juste, le SSH ne passe pas, et rien ne l'explique. Le devis le signale dès qu'une source RFC1918 entre par le WAN — c'est un réglage d'interface, donc invisible dans les règles elles-mêmes.

Invariant du dernier octet. 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 donc .1 partout : 10.0.0.1, 10.27.16.1, 10.27.21.1… Le chiffre n'est pas codé en dur, il vient de reservations.passerelle dans la nomenclature, et make underlay (preuve P23) refuse une passerelle qui s'en écarte.

Seule exception, assumée : les liens plus étroits qu'un /24. Sur le /29 de transit, l'adressage est dicté par les participants du lien — les deux frontières occupent .1 et .2, le switch prend .6.

5. La garde anti-lockout

Une seule règle entrante ne vient pas d'Internet : le SSH de gestion. Sa source est l'alias SETOPS_ADMIN, alimenté par l'intrant nftables_admin_sshle même qui nourrit la garde des nftables d'hôte. Source unique, donc pas de divergence possible entre « ce que le pare-feu d'hôte laisse passer » et « ce que la bordure laisse entrer ».

make devis-opnsense-verifier (preuve P24) refuse un devis dont cet intrant est vide : sans lui, la règle SSH n'aurait aucune source et le block in final fermerait l'accès d'administration. Le trou ne peut plus passer inaperçu.

6. Le lien de transit, et les deux routes

C'est le piège qui nous a coûté une passe de déploiement le 2026-07-29.

Le lien vit dans l'underlay, pas dans un tenant

La frontière route vers tous les supernets tenants (10.21.0.0/16, 10.27.0.0/16…) par le même prochain saut. Le lien qui la relie au routeur est-ouest est donc partagé : il n'appartient à aucun tenant et ne peut dériver d'aucun index. Sa place est l'underlay, cluster-global, au même titre que le management, l'iSCSI et Ceph.

Il se déclare dans underlay.yml par la clé passerelle_sortie — l'adresse du pare-feu sur ce lien :

- nom: transit-frontiere
  vlan: 40
  sous_reseau: 10.0.4.0/29
  passerelle: 10.0.4.6        # SVI du switch L3 (sleipnir-01)
  passerelle_sortie: 10.0.4.1 # bifrost-1 = sortie par défaut de la flotte

Plan du /29. Les frontières occupent le bas de la plage, le SVI du switch le haut :

Adresse Qui
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 à une IP virtuelle CARP si les deux passent en HA
10.0.4.4-.5 libres
10.0.4.6 SVI du switch routeur (sleipnir-01)

Le jour où les deux OPNsense passent en haute disponibilité, passerelle_sortie devra pointer sur l'IP virtuelle CARP et non sur un boîtier nommé — c'est le seul changement que la bascule exigera, et il se fait à un endroit.

Nommage. bifrost-1 et bifrost-2 désignent les deux frontières — Bifröst est le pont vers l'extérieur. La fabric interne porte un autre nom, sleipnir-01…03 : le cheval qui traverse les mondes, pas le pont qui en sort. Les deux boîtiers sont déclarés comme hôtes du lien de transit ; ils sont hors flotte Ansible, la déclaration ne sert qu'à documenter le lien et à réserver les noms. Le devis switch ne leur adresse aucune ligne — sa partie B ne retient que les hôtes du réseau de management.

Un /29 plutôt qu'un /30 : pendant une transition, deux pare-feux cohabitent sur le lien, et un /30 n'offre que deux adresses. make underlay (preuve P23) refuse une sortie hors du lien, confondue avec le SVI, sans SVI, ou déclarée deux fois.

Ce seul bloc alimente les deux devis : le passerelle (10.0.4.6) devient le SVI côté switch et le prochain saut des routes tenants côté frontière ; le passerelle_sortie (10.0.4.1) devient la route par défaut du switch. Le prochain saut n'est donc pas un intrant du panneau — le saisir en doublon rouvrirait la possibilité de deux valeurs contradictoires pour un seul et même lien.

Les deux routes, et pourquoi il en faut deux

make devis-reseau émet alors, en section 5 :

ip route 0.0.0.0 0.0.0.0             10.0.4.1   # aller
ip route 192.168.255.0 255.255.255.0 10.0.4.1   # retour (nftables_admin_ssh)

L'aller est la sortie générale : sans elle, aucun hôte de la flotte n'atteint quoi que ce soit hors de sa zone.

L'ordre d'application, qui n'est pas indifférent

Les sections 0 à 4b du devis switch sont sûres et posables à tout moment. La section 5 ne l'est pas. Ces routes déplacent la sortie du switch — y compris celle de ses propres réponses — vers l'adresse de la frontière. Tant que cette adresse ne répond pas, elles coupent l'accès d'administration au switch lui-même.

C'est le même mécanisme que le piège ci-dessous, mais appliqué à l'équipement depuis lequel on travaille. Préalables, rappelés dans le devis : boîtier câblé, adressé sur le lien de transit, joignable depuis le switch, et une session console ouverte pendant l'opération.

Le retour est le piège proprement dit. Un paquet d'administration entre par la frontière et atteint la VM ; la réponse part de la VM vers sa passerelle — un SVI de switch. Si le switch n'a pas de route vers le réseau d'administration, ou s'il l'atteint par un autre chemin (typiquement sa passerelle de management), la réponse revient au pare-feu par une autre interface que celle où l'état a été créé. Elle est alors jetée en silence : ni réponse, ni ICMP unreachable.

Le symptôme est déroutant : la passerelle de zone répond au ping (elle, sa pile de management sait revenir), mais aucun hôte derrière elle n'est joignable. On croit à une règle de pare-feu ; c'est une route manquante à l'autre bout.

Les réseaux d'administration ne sont pas saisis ici : ils viennent de l'intrant nftables_admin_ssh, la même source unique qui alimente la garde des nftables d'hôte et l'alias SETOPS_ADMIN de la frontière. Les trois pare-feux et les routes de retour ne peuvent donc pas diverger.

7. Appliquer le devis

OPNsense expose une API REST de première classe, authentifiée par clé + secret (System → Access → Users → l'utilisateur → API keys). C'est ce qui permettra d'appliquer le devis sans clics : devis_opnsense.py --json produit déjà la structure destinée à cet usage (alias, routes, règles), et les endpoints visés sont /api/firewall/alias/*, /api/firewall/filter/* et /api/routes/*, avec le motif habituel « on prépare puis on applique ».

Où vivent les identifiants

Même partage que Proxmox — l'anodin en clair, le secret dans la voûte :

Quoi Réglable
URL de gestion, interfaces, prochain saut group_vars/opnsense.yml (en clair) panneau « Intrants de base » du GUI, section Frontière
Clé et secret d'API voûte unique de l'instance, sous vault_opnsense_api_key / vault_opnsense_api_secret ansible-vault edit

Les valeurs non sensibles sont de vrais intrants : opnsense_api_url, opnsense_api_verifier_certs, opnsense_if_wan, opnsense_if_transit et opnsense_prochain_saut figurent au schéma du panneau (INTRANTS_SCHEMA) et s'éditent sans toucher au YAML — un opérateur règle la frontière depuis la console, sans IA.

Tant qu'elles sont vides, le devis affiche des marqueurs (<IF-TRANSIT>, <PROCHAIN-SAUT-SWITCH>) ; il se complète tout seul dès qu'elles sont renseignées.

Garde-fou : opnsense_api_key et opnsense_api_secret sont dans INTRANTS_CLES_INTERDITES — le GUI refuse de les écrire. Impossible de coller un secret d'API dans le panneau par mégarde. Ils n'apparaissent qu'en lecture seule, par leur nom, dans le rappel SECRETS_ATTENDUS.

Le fichier en clair ne porte que des références par nom ({{ vault_opnsense_api_key }}). scripts/voute.py les recense automatiquement et la preuve P18 vérifie qu'elles figurent au gabarit vault.yml.example — le gabarit est passé de 23 à 25 secrets sans intervention.

Pour renseigner les vraies valeurs, une seule commande, jamais de secret sur disque en clair :

ansible-vault edit instance/inventories/principal/group_vars/all/vault.yml

Le secret d'API n'est affiché qu'à sa création dans OPNsense. S'il est perdu, il faut révoquer la clé et en générer une nouvelle.

pfSense CE, lui, n'a pas d'API officielle (celle de Netgate n'existe que sur pfSense Plus) : seul un paquet tiers en fournit une. C'est l'une des raisons du choix d'OPNsense.

8. Ce qui reste ouvert

  • Le câblage — le lien de transit est décidé (VLAN 40, 10.0.4.0/29) et le devis switch émet déjà son SVI et ses routes. Reste à figer, une fois le boîtier raccordé, un seul intrant : opnsense_if_transit, l'interface qui porte le VLAN 40 sur le boîtier — la seule valeur que rien ne peut deviner. Le prochain saut, lui, dérive du transit (§6).

    Trois noms désignent le même port dans OPNsense, et l'intrant en veut un seul : igb1 est le périphérique FreeBSD, TENANTS (ou tout autre libellé) est la description affichée, et opt1 est l'identifiant interne — le seul que parle l'API REST, et le plus stable des trois : il survit à un changement de carte réseau comme à un renommage.

  • Le second VPN — pendant la transition, le VPN d'administration actuel (pfSense) reste en service et OPNsense en montera un second. Son sous-réseau devra être ajouté à nftables_admin_ssh, sans quoi il sera muet de la même façon.

  • L'application par l'API--json produit la structure ; le client d'application reste à écrire.

  • Pas de mot-clé underlay dans le registre des flux — l'underlay est aujourd'hui interdit en bloc aux tenants. Si un besoin légitime apparaît (superviser l'hyperviseur depuis obs-01, par exemple), il ne peut pas être déclaré : il faudrait ajouter un pair underlay au registre plutôt qu'une exception manuelle dans l'ACL.

  • La syntaxe des routesréglé le 2026-08-02, confronté à un show running-config Binardat : la plateforme écrit ip route 0.0.0.0/0 <saut> en notation CIDR. Le générateur suit le dialecte.

  • Restent non vérifiés sur Binardat : la syntaxe des ACL (ip access-list extended), 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.

  • Les ports physiques restent à nommer<PORT-VERS-PROXMOX>, <PORT-VERS-FRONTIERE> et <PORT-TRUNK> ; rien dans le modèle ne peut les deviner.

  • La sortie générale n'est pas déclaréeréglé le 2026-08-02. Elle est déclarée dans le registre, donc dérivée comme le reste : serveur_debian (le socle, porté par tous les hôtes) déclare 443, 80 et 123/udp — dépôts apt et horloge ; client_unbound déclare 53 en UDP et TCP, la récursion depuis la racine que le choix souverain implique. Le block out de la section 5 est donc un vrai default-deny assumé, et le devis l'énonce désormais au lieu de le poser en silence.