Set-OPS-Public/docs/frontiere-opnsense.md
Daniel Allaire e1bbeb78f8 transit : bifrost-1/-2 en .1/.2, le SVI du switch remonte en .6
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>
2026-08-01 20:22:29 -04:00

230 lines
13 KiB
Markdown

# 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 <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. 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** (`<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 :
```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 <tenant> any`. À trancher : la déclarer
explicitement, ou l'assumer comme politique par défaut de la bordure.