# 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 : | Où | 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 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. Ce que la frontière décide (et pourquoi rien n'est saisi à la main) 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. ## 3. 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_ssh` — **le 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. ## 4. 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 : ```yaml - 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.1`) devient le SVI côté switch *et* le prochain saut des routes tenants côté frontière ; le `passerelle_sortie` (`10.0.4.2`) 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.2 # aller ip route 192.168.255.0 255.255.255.0 10.0.4.2 # 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. ## 5. 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 | Où | 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** (``, ``) ; 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 : ```sh 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. ## 6. 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`, le nom de 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 (voir §4). - **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 routes n'est pas dialecte-consciente** — les ACL le sont (masque normal en `binardat`, wildcard en `cisco`), les `ip route` de la section 5 sortent en forme Cisco quel que soit le dialecte. À vérifier sur le matériel réel. - **La sortie générale** (mises à jour apt, ACME) n'est pas déclarée dans le registre : les hôtes sortent aujourd'hui parce que la politique `output` des nftables est `accept` et que l'ACL de switch se termine par `permit ip any`. À trancher : la déclarer explicitement, ou l'assumer comme politique par défaut de la bordure.