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

216 lines
12 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.1 # SVI du switch L3
passerelle_sortie: 10.0.4.2 # la frontière = sortie par défaut de la flotte
```
**Nommage.** `bifrost-0` et `bifrost-1` 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.