frontière : ses intrants appartiennent à l'hébergeur, pas au tenant actif

Un hébergeur sert plusieurs tenants et n'a qu'une frontière. Ses intrants
étaient lus chez le tenant actif : basculer sur un invité — Technolibre, qui
n'a pas de frontière à lui — faisait perdre au devis l'URL de gestion,
l'adresse publique et les deux interfaces. Il repartait en marqueurs, comme
si le boîtier n'existait pas. Vérifié en simulant la bascule.

Même famille que le défaut de l'underlay corrigé plus tôt ; c'est la
distinction hébergeur/tenant qui le fait apparaître.

Le devis et le panneau lisent maintenant la frontière chez l'hébergeur, qui
n'est pas déclaré pour autant : le symlink `underlay.yml` le désigne déjà.
Repli sur l'instance active sans underlay monté.

Vérifié : devis identique avec l'hébergeur actif, intrants conservés avec un
invité actif.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-02 17:17:10 -04:00
parent 7256486c9c
commit 517119c7ac
4 changed files with 97 additions and 13 deletions

View file

@ -1,5 +1,24 @@
# CHANGELOG — Set-OPS
## 2026-08-02 (suite 7) — la frontière appartient à l'hébergeur, pas au tenant actif
Un hébergeur sert **plusieurs tenants** et n'a qu'**une** frontière. Or ses intrants
(`group_vars/opnsense.yml`) étaient lus chez le **tenant actif** : basculer sur un invité —
Technolibre, qui n'a pas de frontière à lui — faisait perdre au devis l'URL de gestion,
l'adresse publique et les deux interfaces. Il repartait en marqueurs, comme si le boîtier
n'existait pas. Vérifié en simulant la bascule : `AUCUN` intrant lu.
Même famille que le défaut de l'underlay corrigé plus tôt, et c'est la distinction
hébergeur/tenant qui le fait apparaître.
Le devis **et** le panneau lisent désormais la frontière chez l'hébergeur. Celui-ci n'est pas
déclaré pour autant : le symlink `underlay.yml` le **désigne déjà**, et une seconde
déclaration ouvrirait la porte à deux valeurs contradictoires. Repli sur l'instance active
quand aucun underlay n'est monté — un site sans fabric déclarée continue de fonctionner.
Vérifié : devis identique avec l'hébergeur actif, et intrants **conservés** avec un invité
actif. `docs/frontiere-opnsense.md` gagne un §2 qui pose qui possède quoi.
## 2026-08-02 (suite 6) — l'underlay devient modélisable
Un modèle décrivait jusqu'ici un **tenant** : ses services, ses zones, ses bases. Or tous les

View file

@ -38,7 +38,27 @@ atteindre le management des switches, celui de Proxmox et l'OOB/IPMI. `make devi
é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)
## 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
@ -70,7 +90,7 @@ restait qu'à la dériver. C'est ce que fait `scripts/devis_opnsense.py`, à par
Aucun port, aucune adresse et aucun nom d'hôte n'est écrit dans le générateur.
## 3. L'interface de chaque règle, et l'invariant du dernier octet
## 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
@ -102,7 +122,7 @@ Seule exception, assumée : les liens plus étroits qu'un `/24`. Sur le `/29` de
l'adressage est dicté par les participants du lien — les deux frontières occupent `.1` et
`.2`, le switch prend `.6`.
## 4. La garde anti-lockout
## 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
@ -113,7 +133,7 @@ le pare-feu d'hôte laisse passer » et « ce que la bordure laisse entrer ».
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.
## 5. Le lien de transit, et les deux routes
## 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.**
@ -205,7 +225,7 @@ Les réseaux d'administration ne sont pas saisis ici : ils viennent de l'intrant
l'alias `SETOPS_ADMIN` de la frontière. Les trois pare-feux et les routes de retour ne peuvent
donc pas diverger.
## 6. Appliquer le devis
## 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
@ -252,12 +272,12 @@ ansible-vault edit instance/inventories/principal/group_vars/all/vault.yml
> 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.
## 7. Ce qui reste ouvert
## 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 (§5).
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

View file

@ -63,12 +63,32 @@ IF_TRANSIT = "<IF-TRANSIT>"
IF_WAN = "<IF-WAN>"
def depot_hebergeur() -> Path | None:
"""Depot de l'HEBERGEUR — celui qui possede le materiel, donc la frontiere.
Derive du symlink `underlay.yml` : il pointe vers le depot de l'hebergeur, ce qui
le DESIGNE deja. Le declarer une seconde fois ouvrirait la porte a deux valeurs
contradictoires. None si aucun underlay (site sans fabric declaree).
"""
c = underlay_mod.chemin()
return c.resolve().parent if c else None
def intrants_frontiere() -> dict:
"""Intrants NON sensibles de la frontiere (group_vars/opnsense.yml). {} si absent."""
for nom in ("principal", "production", "lab"):
p = _inventaire().parent.parent / nom / "group_vars" / "opnsense.yml"
if p.is_file():
return yaml.safe_load(p.read_text(encoding="utf-8")) or {}
"""Intrants NON sensibles de la frontiere (group_vars/opnsense.yml). {} si absent.
Lus chez l'HEBERGEUR, pas chez le tenant actif : un hebergeur sert plusieurs
tenants et n'a qu'une frontiere. Basculer l'instance active sur un invite ne doit
pas faire perdre au devis l'URL de gestion, l'adresse publique et les interfaces.
Repli sur l'instance active quand aucun underlay ne designe d'hebergeur.
"""
for base in (depot_hebergeur(), _inventaire().parent.parent.parent):
if base is None:
continue
for nom in ("principal", "production", "lab"):
p = base / "inventories" / nom / "group_vars" / "opnsense.yml"
if p.is_file():
return yaml.safe_load(p.read_text(encoding="utf-8")) or {}
return {}

View file

@ -87,12 +87,37 @@ INTRANTS_PROXMOX = INVENTAIRE_MODELE.parent / "group_vars/proxmox.yml"
# reformater le fichier (cf. _ecrire_index_nomenclature).
# Frontiere nord/sud (OPNsense) : parametres NON sensibles du pare-feu de bordure.
# La cle/secret d'API n'y entrent JAMAIS — voute uniquement (cf. INTRANTS_CLES_INTERDITES).
INTRANTS_FRONTIERE = INVENTAIRE_DEFAUT.parent / "group_vars/opnsense.yml"
# Fabric physique : `underlay.yml`, monte par symlink a la racine du moteur depuis le
# depot de l'HEBERGEUR (ses switches, ses cables). Distinct de l'instance : il ne suit
# pas `make instance-utiliser`. Imbrique sous `underlay:` et riche en commentaires :
# on l'ecrit chirurgicalement, jamais par un safe_dump qui les effacerait.
FICHIER_UNDERLAY = Path(os.environ.get("SETOPS_UNDERLAY") or (RACINE / "underlay.yml"))
def _depot_hebergeur() -> Path | None:
"""Depot de l'HEBERGEUR, derive du symlink `underlay.yml` qui le designe deja.
Un hebergeur sert PLUSIEURS tenants et n'a qu'une frontiere : ses intrants ne
peuvent pas vivre chez le tenant actif, sinon basculer sur un invite les ferait
disparaitre du panneau. None si aucun underlay n'est monte.
"""
if not FICHIER_UNDERLAY.exists():
return None
return FICHIER_UNDERLAY.resolve().parent
def _fichier_frontiere() -> Path:
"""group_vars/opnsense.yml de l'hebergeur ; repli sur l'instance active."""
base = _depot_hebergeur()
if base:
for nom in ("principal", "production", "lab"):
p = base / "inventories" / nom / "group_vars" / "opnsense.yml"
if p.is_file():
return p
return INVENTAIRE_DEFAUT.parent / "group_vars/opnsense.yml"
INTRANTS_FRONTIERE = _fichier_frontiere()
FICHIERS_INTRANTS = {"identite": INTRANTS_IDENTITE, "proxmox": INTRANTS_PROXMOX,
"reseau": FICHIER_NOMENCLATURE, "frontiere": INTRANTS_FRONTIERE,
"fabric": FICHIER_UNDERLAY}