Set-OPS-Public/docs/frontiere-opnsense.md
Daniel Allaire 3698152b6a frontière nord/sud : devis dérivé, lien de transit et les deux routes
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>
2026-08-01 19:32:04 -04:00

169 lines
8.9 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 les switches L3** | 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.
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.
## 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
```
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.
### 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.
**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. Restent à figer, une fois le boîtier raccordé, les
deux intrants côté frontière : `opnsense_if_transit` (l'interface qui porte le VLAN 40) et
`opnsense_prochain_saut` (`10.0.4.1`).
- **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.
- **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.