diff --git a/CHANGELOG.md b/CHANGELOG.md index 78d8252..ec29f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/frontiere-opnsense.md b/docs/frontiere-opnsense.md index 891aafd..421c584 100644 --- a/docs/frontiere-opnsense.md +++ b/docs/frontiere-opnsense.md @@ -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-` | +| `underlay.yml` (fabric physique) | l'**hébergeur** | `OPS-`, 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 diff --git a/scripts/devis_opnsense.py b/scripts/devis_opnsense.py index 404183d..53a9afa 100644 --- a/scripts/devis_opnsense.py +++ b/scripts/devis_opnsense.py @@ -63,12 +63,32 @@ IF_TRANSIT = "" 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 {} diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 6973ad2..04457d4 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -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}