La refonte de ce matin posait une convention. Une convention qu'on n'outille pas tient tant que quelqu'un y pense : c'est le raisonnement de D-70, applique au corpus documentaire. Etat de depart mesure : 2 documents sur 34 declaraient leur lecteur. Les 32 autres disaient leur SUJET — ce qui avait enfoui le runbook de reprise le plus utile du depot au §6 de autorisation.md. Les 38 le declarent desormais, lecteur determine document par document et non colle au gabarit : l'exploitant (devis, migration de tenant, cycle de vie, gabarit d'or), le mainteneur (conceptions, registres, carte), le lecteur externe (ecosysteme-chezlepro), l'agent IA (MISE-A-JOUR-CODEX-CLAUDE). Deux exemptions DERIVEES, pas listees — un chemin en dur aurait vieilli a la premiere page ajoutee : un document qui s'annonce genere, et un fragment sans titre. Les 13 exemptes verifies un par un ; aucun document ecrit a la main n'est exempte par accident. La preuve ne lit que l'EN-TETE, ce qui empeche frontiere-opnsense.md et plan-et-generation.md — qui parlent de generation dans leur corps — d'etre exemptes a tort. Eprouvee dans les deux sens. Elle a echoue seule des sa premiere execution en nommant deux documents que mon inventaire avait manques (docs/audit/). Puis test negatif delibere : declaration retiree de meta-classe.md -> ECHEC la nommant ; restauree -> OK. Ce qu'elle ne teste pas : que le lecteur declare soit le BON. Ca se juge en revue ; elle garantit qu'on a du y penser. P01–P34. Comptes perimes corriges au passage (AGENTS.md et devis-services.md annoncaient encore 30 preuves). Verifie : prouver.py 0 (34 OK), plan-recette inchange. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
426 lines
26 KiB
Markdown
426 lines
26 KiB
Markdown
# La frontière OPNsense (nord/sud)
|
|
|
|
> **Pour qui :** le **mainteneur** de la bordure 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. Quand la fabric sait porter des ACL, `make devis-reseau` émet un `deny` par
|
|
sous-réseau underlay **avant** le `permit` final, dérivé de `underlay.yml`.
|
|
|
|
**Décision (2026-08-02) : pas d'ACL sur cette fabric** (`underlay.acl_inter_tenant: false`).
|
|
Les interfaces VLAN du Binardat n'offrent aucun `access-group` — on ne peut pas lier une ACL
|
|
à un SVI. Émettre des ACL qu'on ne peut pas lier serait pire que rien : elles auraient l'air
|
|
d'isoler sans jamais filtrer.
|
|
|
|
L'isolation inter-tenant repose donc **entièrement sur les nftables d'hôte**, en `policy
|
|
drop`. C'est défendable — c'est déjà là que vit le zéro-confiance est-ouest — mais il faut
|
|
nommer ce qu'on perd : **le plan de gestion de la fabric n'a plus de protection réseau contre
|
|
les tenants.** Une VM émettant vers `10.0.0.x` est routée localement vers le management des
|
|
switches, celui de Proxmox et l'OOB/IPMI ; la politique `output` des VM est permissive, et
|
|
l'IPMI n'est pas un hôte géré.
|
|
|
|
Des **VRF** auraient donné cette isolation sans ACL, par séparation des tables de routage.
|
|
Ce matériel n'en a pas — c'est le critère à retenir au prochain renouvellement. La seule
|
|
parade structurelle disponible aujourd'hui : sortir le management de la fabric routée des
|
|
tenants, comme l'est déjà le stockage.
|
|
|
|
## 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.
|
|
|
|
### « Vers Internet » n'est pas « vers n'importe où »
|
|
|
|
Les flux **sortants** d'un tenant (DNS, SMTP, NTP, HTTP/HTTPS) visaient `any`. Leur
|
|
destination n'excluait donc ni le plan de gestion, ni la frontière elle-même, ni le supernet
|
|
du voisin : mesuré le 2026-08-09, `https://10.0.0.1/` — la console d'administration du
|
|
pare-feu — répondait depuis une VM du tenant. Ils visent désormais **`!SETOPS_INTERNES`**,
|
|
une destination *niée* valant les trois blocs privés RFC 1918.
|
|
|
|
Pourquoi RFC 1918 et non la liste de nos réseaux : cette liste laissait dehors
|
|
`192.168.11.0/24`, le plan de gestion hérité. Une exclusion incomplète ne protège rien, et
|
|
un réseau interne ajouté demain doit être couvert sans qu'on y pense.
|
|
|
|
### Le VLAN d'administration atteint les services publiés
|
|
|
|
Un service publié est joint depuis Internet par le WAN, mais aussi depuis le VLAN
|
|
d'administration, où se trouve le poste de l'exploitant. Ce second chemin ne reposait sur
|
|
aucune règle Set-OPS : il fonctionnait grâce à la règle **d'usine**
|
|
`Default allow LAN to any rule`, qui autorise tout depuis le LAN — et qui privait donc notre
|
|
défaut-deny de tout effet. Cette règle héritée n'a **aucune API** : sa désactivation est un
|
|
geste d'interface, qui appartient à l'exploitant.
|
|
|
|
Set-OPS déclare donc explicitement ces flux sur l'interface de gestion, afin que la
|
|
désactiver ne coupe personne de ses propres consoles web. *(Fait le 2026-08-09 : la règle
|
|
d'usine et une seconde règle héritée ont été retirées. `make frontiere-mesurer` rend
|
|
CONFORME sur 56 lignes.)*
|
|
|
|
**Sauf quand le flux déclare `poste: false`.** Tous les services publiés ne s'adressent pas
|
|
à un humain : le `25` entrant de Postfix est un flux *serveur à serveur* — les MX distants.
|
|
L'étendre au VLAN d'administration faisait dire à la frontière ce qu'aucune autre couche
|
|
n'honorait : la bordure autorisait, le `nftables` de l'hôte refusait. Le mot-clé vit dans
|
|
`meta/flux.yml`, avec le rôle qui sait ce que son port veut dire ; le générateur, lui, ne
|
|
connaît toujours aucun numéro de port.
|
|
|
|
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 tenants fédérés et leurs zones, 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.
|
|
|
|
Pour le trafic ordinaire, 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 ne suit aucune de ces deux lignes** (D-61). Un plan de gestion peut
|
|
être **distant** — il revient alors par l'adresse publique, donc par le WAN — ou
|
|
**directement attaché** à la frontière, et il arrive alors par l'interface qui porte ce
|
|
réseau. Le sens du flux ne le dit pas ; seul l'attachement réel le dit.
|
|
|
|
L'interface se dérive donc de l'underlay : la frontière y déclare une adresse par réseau, et
|
|
tout ce qui est contenu dans l'un de ces sous-réseaux — hors lien de transit — arrive par
|
|
`opnsense_if_gestion`. Le reste arrive par le WAN. Un tenant peut avoir les deux, et reçoit
|
|
alors **deux règles et deux alias**, chacun ne portant que les sources qui peuvent réellement
|
|
emprunter ce chemin.
|
|
|
|
```
|
|
SETOPS_ADMIN_CHEZ17_GESTION 10.0.0.0/24 → pass in on lan
|
|
SETOPS_ADMIN_TECH11_GESTION 10.0.0.0/24 → pass in on lan
|
|
SETOPS_ADMIN_TECH11_WAN 192.168.255.2/32, 192.168.254.2/32 → pass in on wan
|
|
```
|
|
|
|
**Le piège de la case à cocher.** Une source **RFC1918** qui arrive par le WAN se heurte à
|
|
*Block private networks*, activé par défaut, 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 — mais **seulement pour
|
|
les sources réellement rangées côté WAN**. Conseiller de décocher ce filtre pour un réseau qui
|
|
entre par la gestion affaiblirait l'interface publique sans rien ouvrir du tout.
|
|
|
|
**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. `le commutateur` 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_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.
|
|
|
|
## 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 tenants fédérés 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.
|
|
|
|
**Une route par sous-réseau attribué, jamais une par supernet** (2026-08-09). Router
|
|
`10.27.0.0/16` faisait porter à la frontière des destinations qui n'existent nulle part :
|
|
elles atteignaient le nœud de sortie, y arrivaient dans la table *principale* — le VRF n'est
|
|
atteint que par les `/24` annoncés en BGP — et repartaient vers la passerelle
|
|
d'administration. Les alias `SETOPS_TENANT_*` énumèrent exactement les mêmes `/24`, et
|
|
`verifier()` exige que les deux ensembles coïncident : un alias plus large laisse le filtre
|
|
approuver l'inexistant, un alias plus étroit fait acheminer vers ce que le filtre refuse.
|
|
|
|
Les routes sont **réconciliées comme les règles** par `appliquer_opnsense.py` : leur identité
|
|
est portée par leur description (`setopsroute:<tenant>:<réseau>-><saut>`), donc une route
|
|
retirée du devis est retirée de la frontière. Le nom de la passerelle OPNsense est résolu
|
|
depuis l'adresse du prochain saut — il n'y a pas d'intrant supplémentaire à tenir à jour.
|
|
|
|
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 (le commutateur)
|
|
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 (`le commutateur`) |
|
|
|
|
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, `le commutateur…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 | 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.
|
|
|
|
## 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 routes~~ — **ré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.
|
|
- ~~La syntaxe des trunks~~ — **réglé le 2026-08-02** par `switchport trunk allowed vlan ?`.
|
|
Le devis émet la forme **sans mot-clé**, qui *définit* la liste : `add` l'*ajoute* à
|
|
l'existante, et sur un port trunk neuf — qui autorise tous les VLAN — n'aurait rien
|
|
retranché. Le devis aurait donné l'illusion de restreindre.
|
|
- **Le spanning-tree, sur Binardat** (`show spanning-tree`, 2026-08-02) : la plateforme est en
|
|
**MSTP** (IEEE 802.1s, *Force Version 3*) par défaut, priorité de pont **32768**, et il est
|
|
**actif** — son absence du `show running-config` signifiait « valeurs par défaut », non
|
|
« désactivé ». Deux conséquences : `underlay.stp.mode` doit dire `mstp` si l'on garde le
|
|
comportement d'usine, et la ligne de priorité émise pour les switches d'accès (32768) est un
|
|
non-opérant qui écrit ce qui est déjà vrai.
|
|
- ~~La syntaxe des ACL~~ — **réglé le 2026-08-02** : une ACL générée s'applique telle quelle,
|
|
ses règles dans l'ordre (`ip access-list extended <NOM>`, masques normaux, `any`). Détail de
|
|
lecture : le boîtier **affiche** `any-destination` là où l'on saisit `any` — comparer un
|
|
`show access-lists` au devis fait apparaître une différence qui n'en est pas une.
|
|
- ~~La syntaxe du spanning-tree~~ — **réglé le 2026-08-02** par `spanning-tree ?` en mode
|
|
configuration : `spanning-tree` seul active le protocole, `mode` et `priority` s'acceptent au
|
|
niveau **global**. La priorité n'a donc pas besoin d'être portée par une instance, même en
|
|
MSTP — la réserve inverse, notée la veille, était infondée.
|
|
|
|
**Vérifié contre le matériel** : VLAN, SVI, trunks (`switchport …`), routes, définition des
|
|
ACL, spanning-tree **global** (`spanning-tree`, `mode`, `priority`). Deux de ces vérifications
|
|
ont révélé un défaut réel plutôt que de confirmer l'existant — les routes (notation CIDR) et
|
|
surtout les trunks, dont la forme `add` ne retranchait rien.
|
|
|
|
**Restent non vérifiées, toutes au niveau interface ou hors de l'aide consultée** :
|
|
|
|
| Ligne émise | Où | Risque |
|
|
|---|---|---|
|
|
| `spanning-tree portfast trunk` | interface | `trunk` est un mot-clé Cisco ; l'équivalent s'écrit souvent `spanning-tree portfast` seul, voire `edged-port` |
|
|
| `ip access-group <NOM> in` | interface VLAN | **probablement invalide** : `ip ?` sur une interface VLAN n'offre aucun `access-group` (vérifié le 2026-08-02). Le filtrage semble aussi conditionné globalement par `firewall enable` |
|
|
| `ip default-gateway <ip>` | global | forme des switches d'accès (partie B) |
|
|
|
|
Sur une **interface VLAN**, ni `spanning-tree` ni `ip access-group` n'existent (liste complète
|
|
des commandes vérifiée). Pour `portfast` c'est attendu — il vise un port physique. Pour la
|
|
liaison des ACL, non : c'est le mécanisme qui rend l'isolation effective.
|
|
|
|
**Conséquence, et elle est lourde** : appliqué tel quel, le devis produirait des ACL
|
|
parfaitement définies et **jamais liées**. `show access-lists` afficherait « used 0 time(s) »,
|
|
et rien d'autre ne signalerait que l'isolation inter-tenant ne filtre pas. Le devis porte
|
|
désormais un avertissement à cet endroit, en dialecte `binardat` uniquement.
|
|
|
|
Reste à trancher sur une interface **physique** : `ip ?`, `access-group ?`, `spanning-tree ?`
|
|
— et le rôle de `firewall enable`.
|
|
- **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ée~~ — **ré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.
|