underlay : l'invariant du dernier octet retrouve sa portée (D-04, D-52 à D-54)

Il valait partout ; il ne vaut que dans l'adressage dérivé des tenants, où
passerelle_de(index, zone) produit le même .1 dans les treize sous-réseaux d'un
tenant. C'est une propriété de la dérivation, pas une loi universelle.

Dans l'underlay il produisait deux effets pervers. Un seuil arbitraire : les
sous-réseaux plus étroits qu'un /24 étaient exemptés, donc élargir un /29
changeait la validité du fichier sans que rien d'autre bouge. Et une couture
entre propriétaires : l'octet attendu venait de la nomenclature d'un tenant,
appliquée à la fabric de l'hébergeur — la validité de l'underlay aurait dépendu
du tenant actif.

Ce qui reste est plus fort et suffit (D-52) : une passerelle doit être l'adresse
d'un hôte déclaré sur ce réseau. Elle attrape les passerelles fantômes, ce que le
comptage d'octets ne faisait pas. Vérifié : la garde mord toujours.

À noter, parce que l'ordre était mauvais : le ré-adressage de l'OPNsense en .1 a
été demandé au nom de cette règle, deux messages avant qu'elle soit recadrée. Pas
perdu — .1 est la position conventionnelle d'une passerelle — mais la portée
aurait dû être questionnée avant de faire changer une adresse en service.

D-53 : le réseau et l'underlay de l'hébergeur méritent leur propre dépôt.
underlay.yml décrit une infrastructure, le dépôt de tenant une organisation ; un
tenant peut déménager, une fabric non. Consigné, non fait.

D-54 : 10.0.0.0/24 est réservé à l'IPAM, la gestion des équipements et l'OOB,
accès sysadmin. Aucun hyperviseur, aucune VM, aucun trafic tenant. C'est la
raison d'être des VLAN 11 et 40.

30 preuves OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-04 14:46:06 -04:00
parent 1bc516fd92
commit 63aa79e710
4 changed files with 88 additions and 22 deletions

View file

@ -1,5 +1,44 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 2026-08-04 (suite 2) — l'invariant du dernier octet retrouve sa portée
Il valait partout ; il ne vaut que là où il a un sens : l'**adressage dérivé des
tenants**, où `passerelle_de(index, zone)` produit le même `.1` dans les treize
sous-réseaux. C'est une propriété de la dérivation, pas une loi universelle.
Dans l'underlay, il produisait deux effets pervers :
- **un seuil arbitraire** — les sous-réseaux plus étroits qu'un `/24` étaient exemptés,
donc élargir un `/29` changeait la validité du fichier sans que rien d'autre bouge ;
- **une couture entre propriétaires** — l'octet attendu venait de la nomenclature d'un
*tenant*, appliquée à la fabric de l'*hébergeur*. La validité de l'underlay aurait
dépendu du tenant actif si l'un d'eux réservait autre chose.
Ce qui reste est plus fort, et suffit : **une passerelle doit être l'adresse d'un hôte
déclaré sur ce réseau** (D-52). Elle attrape les passerelles fantômes, ce que le comptage
d'octets ne faisait pas.
> **À noter, parce que l'ordre était mauvais.** Le ré-adressage de l'OPNsense en `.1` a
> été demandé au nom de cette règle, deux messages avant qu'elle soit recadrée. Il n'est
> pas perdu — `.1` est la position conventionnelle d'une passerelle, et la frontière la
> porte désormais partout — mais la portée de la règle aurait dû être questionnée avant
> de faire changer une adresse sur un boîtier en service.
### Deux décisions consignées, non construites
**D-53** — le **réseau et l'underlay** de l'hébergeur méritent leur propre dépôt.
`underlay.yml` décrit une **infrastructure** ; le dépôt de tenant décrit une
**organisation**. Un tenant peut déménager, une fabric non. Les mêler oblige, à chaque
commit, à trancher ce qu'on touche. Le symlink désignant l'hébergeur pointerait vers ce
dépôt-là — la distinction deviendrait visible dans les chemins.
**D-54** — `10.0.0.0/24` est réservé à l'**IPAM, la gestion des équipements et l'OOB** ;
accès sysadmin. Aucun hyperviseur, aucune VM, aucun trafic tenant — ni encapsulé, ni
décapsulé. C'est la raison d'être des VLAN 11 et 40. Écrit dans `underlay.yml` à côté du
réseau lui-même, pas seulement dans la documentation.
30 preuves OK.
## 2026-08-04 (suite) — plus aucun commutateur ne route ## 2026-08-04 (suite) — plus aucun commutateur ne route
Question posée à froid : « je ne vois plus de valeur ajoutée au point de routage Question posée à froid : « je ne vois plus de valeur ajoutée au point de routage

View file

@ -26,7 +26,8 @@ sont les seules vérifiables.
|---|---|---|---|---| |---|---|---|---|---|
| **D-01** | OPNsense est une frontière **nord/sud**, pas la passerelle des zones | le routage inter-zone reste au débit ligne ; la bordure ne voit pas l'est-ouest | `frontiere-opnsense.md` §1 | — | | **D-01** | OPNsense est une frontière **nord/sud**, pas la passerelle des zones | le routage inter-zone reste au débit ligne ; la bordure ne voit pas l'est-ouest | `frontiere-opnsense.md` §1 | — |
| **D-02** | Le **lien de transit** vit dans l'underlay, pas dans un tenant | la frontière route vers tous les tenants par le même saut : il ne peut dériver d'aucun `index` | `frontiere-opnsense.md` §6 | P23 | | **D-02** | Le **lien de transit** vit dans l'underlay, pas dans un tenant | la frontière route vers tous les tenants par le même saut : il ne peut dériver d'aucun `index` | `frontiere-opnsense.md` §6 | P23 |
| **D-04** | Un point de routage porte **le même dernier octet** sur tous ses sous-réseaux | on retient une adresse, pas treize | `frontiere-opnsense.md` §4 | P23 | | **D-04** | Un point de routage porte **le même dernier octet** sur tous ses sous-réseaux — **dans l'adressage dérivé des tenants seulement** | on retient une adresse, pas treize. Dans l'underlay, l'adressage est dicté par les participants d'un lien physique : la règle y imposait un seuil arbitraire et empruntait sa valeur à la nomenclature d'un tenant | `frontiere-opnsense.md` §4 | P20 |
| **D-52** | Dans l'underlay, une passerelle doit être **l'adresse d'un hôte déclaré** sur ce réseau | remplace le comptage d'octets par une règle plus forte : elle attrape les passerelles fantômes | `underlay.py` | P23 |
| **D-49** | **Aucun commutateur ne route.** La frontière est le seul équipement L3 | l'EVPN a retiré les VLAN tenants du fil, puis la fusion du lien de sortie a rendu les nœuds de sortie **adjacents** à la frontière : plus un seul flux à router | `underlay.yml` | P23 | | **D-49** | **Aucun commutateur ne route.** La frontière est le seul équipement L3 | l'EVPN a retiré les VLAN tenants du fil, puis la fusion du lien de sortie a rendu les nœuds de sortie **adjacents** à la frontière : plus un seul flux à router | `underlay.yml` | P23 |
| **D-50** | `passerelle` = la passerelle du sous-réseau, **où qu'elle vive** ; le SVI se **dérive** du rôle de son porteur | la clé signifiait « adresse du SVI du switch » — une hypothèse, pas une donnée. Un même moteur sert désormais les deux postures | `devis_reseau.py` | P23 | | **D-50** | `passerelle` = la passerelle du sous-réseau, **où qu'elle vive** ; le SVI se **dérive** du rôle de son porteur | la clé signifiait « adresse du SVI du switch » — une hypothèse, pas une donnée. Un même moteur sert désormais les deux postures | `devis_reseau.py` | P23 |
| **D-51** | **Deux** commutateurs en paire, pas trois en étoile | le centre d'une étoile est sur **tous** les chemins : point de panne unique du plan de données. Deux liens vers deux commutateurs donnent la redondance qu'une étoile ne peut pas donner | `underlay.yml` | — | | **D-51** | **Deux** commutateurs en paire, pas trois en étoile | le centre d'une étoile est sur **tous** les chemins : point de panne unique du plan de données. Deux liens vers deux commutateurs donnent la redondance qu'une étoile ne peut pas donner | `underlay.yml` | — |
@ -60,6 +61,8 @@ sont les seules vérifiables.
| **D-46** | Un hébergeur porte **trois** catégories, pas deux : son **tenant**, ses **opérations**, le **plan de contrôle** | Chezlepro est hébergeur ET tenant, ce qui masquait des besoins n'appartenant à aucun tenant | `hebergeur-exploitation.md` §2 | — | | **D-46** | Un hébergeur porte **trois** catégories, pas deux : son **tenant**, ses **opérations**, le **plan de contrôle** | Chezlepro est hébergeur ET tenant, ce qui masquait des besoins n'appartenant à aucun tenant | `hebergeur-exploitation.md` §2 | — |
| **D-47** | Les **services d'exploitation** de l'hébergeur vivent dans **son dépôt**, et leurs VM se rattachent à un **pont VLAN, jamais un VNet** | un service qui observe la fabric ne peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison de la panne | `hebergeur-exploitation.md` §1, §4 | — | | **D-47** | Les **services d'exploitation** de l'hébergeur vivent dans **son dépôt**, et leurs VM se rattachent à un **pont VLAN, jamais un VNet** | un service qui observe la fabric ne peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison de la panne | `hebergeur-exploitation.md` §1, §4 | — |
| **D-48** | Les **hyperviseurs** sont gérables par Ansible ; « hors flotte » ne vaut que pour les **commutateurs** et la **frontière** | ce sont des Debian joignables en SSH ; c'est la seule façon d'y poser un exportateur de métriques | `hebergeur-exploitation.md` §5 | — | | **D-48** | Les **hyperviseurs** sont gérables par Ansible ; « hors flotte » ne vaut que pour les **commutateurs** et la **frontière** | ce sont des Debian joignables en SSH ; c'est la seule façon d'y poser un exportateur de métriques | `hebergeur-exploitation.md` §5 | — |
| **D-53** | Le **réseau et l'underlay** de l'hébergeur méritent leur **propre dépôt**, séparé de son tenant | `underlay.yml` et le cluster décrivent une infrastructure ; le dépôt de tenant décrit une organisation. Les mêler oblige à trancher qui possède quoi à chaque commit | `hebergeur-exploitation.md` §7 | — |
| **D-54** | `10.0.0.0/24` est réservé à l'**IPAM, la gestion des équipements et l'OOB** — accès sysadmin | aucun hyperviseur, aucune VM, aucun trafic tenant ; c'est la raison d'être des VLAN 11 et 40 | `underlay.yml` | — |
| **D-18** | Chaque tenant a un **responsable désigné** | sans lui, « qui peut décider de déménager cette organisation ? » se pose au pire moment | `migration-tenant.md` §3 | — | | **D-18** | Chaque tenant a un **responsable désigné** | sans lui, « qui peut décider de déménager cette organisation ? » se pose au pire moment | `migration-tenant.md` §3 | — |
| **D-38** | Toute authentification **web** passe par Keycloak ; LDAP est la **source unique** des comptes | une identité, un mot de passe ; aucun service ne tient son propre répertoire d'humains | `authentification.md` §1-2 | — | | **D-38** | Toute authentification **web** passe par Keycloak ; LDAP est la **source unique** des comptes | une identité, un mot de passe ; aucun service ne tient son propre répertoire d'humains | `authentification.md` §1-2 | — |

View file

@ -84,3 +84,31 @@ Deux obstacles mesurés le 2026-08-04, à lever avant :
À noter au passage : la convention héritée était déjà `1000 + numéro de tenant`. La formule À noter au passage : la convention héritée était déjà `1000 + numéro de tenant`. La formule
de Set-OPS (`1000 + index×10 + zone`) en est un raffinement, pas une invention. de Set-OPS (`1000 + index×10 + zone`) en est un raffinement, pas une invention.
## 7. Un dépôt à part pour le réseau (décidé, non fait)
`underlay.yml` et `proxmox-hebergeur.yml` vivent aujourd'hui dans `OPS-Chezlepro`, qui est
aussi le dépôt du **tenant** Chezlepro. C'est ce qui a permis de démarrer, et c'est ce qui
oblige, à chaque commit, à trancher si l'on touche à l'infrastructure ou à l'organisation.
Ils décrivent des objets différents : l'un une **infrastructure** — des câbles, des VLAN,
un cluster —, l'autre une **organisation** — ses serveurs, ses applications, ses comptes.
Un tenant peut déménager ; une fabric ne déménage pas.
Le dépôt réseau de l'hébergeur porterait donc `underlay.yml`, `proxmox-hebergeur.yml`, et
plus tard l'inventaire des services d'exploitation (§4). Le symlink qui désigne
l'hébergeur pointerait vers lui plutôt que vers son tenant — ce qui rendrait enfin la
distinction visible dans les chemins eux-mêmes.
**Rien n'est fait.** La bascule demande de déplacer deux fichiers, de refaire le symlink,
et de vérifier que les trois générateurs qui les lisent suivent.
## 8. Le VLAN de gestion, et ce qu'il n'est pas
`10.0.0.0/24` est réservé à l'**IPAM, la gestion des équipements et l'OOB/IPMI**. Accès
sysadmin uniquement.
Aucun hyperviseur n'y a d'adresse, aucune VM n'y est branchée, aucun trafic tenant ne le
traverse — ni encapsulé, ni décapsulé. C'est précisément la raison d'être des VLAN 11
(transport VXLAN) et 40 (sortie tenant) : les avoir sortis de ce domaine de diffusion.

View file

@ -364,27 +364,23 @@ def valider(underlay: dict | None,
if p.get("montante") and hn == r_courant: if p.get("montante") and hn == r_courant:
erreurs.append(f"hote '{hn}': une `montante` alors qu'il EST le routeur") erreurs.append(f"hote '{hn}': une `montante` alors qu'il EST le routeur")
# Invariant du dernier octet : tous les points de routage doivent le partager. # L'INVARIANT DU DERNIER OCTET NE S'APPLIQUE PAS ICI (decide le 2026-08-04).
# Exemption des sous-reseaux plus etroits qu'un /24 : sur un /29 de transit, #
# l'adressage est dicte par les participants du lien, pas par la convention. # Il vaut pour l'adressage DERIVE des tenants, ou `passerelle_de(index, zone)`
attendu = octet_passerelle(plan_nomenclature) # produit le meme `.1` dans les treize sous-reseaux d'un tenant : on retient une
if attendu is not None: # adresse, pas treize. C'est une propriete de la derivation, pas une regle
for r in reseaux(underlay): # universelle.
gw = r.get("passerelle") #
if not gw: # Dans l'underlay, l'adressage est dicte par les PARTICIPANTS d'un lien physique.
continue # La regle y produisait deux effets pervers : un seuil arbitraire (les sous-reseaux
try: # plus etroits qu'un /24 etaient exemptes, donc elargir un /29 changeait la
net = ipaddress.ip_network(r["sous_reseau"], strict=False) # validite du fichier), et une couture entre proprietaires — l'octet attendu venait
except ValueError: # de la nomenclature d'un TENANT, appliquee a la fabric de l'HEBERGEUR. La validite
continue # de l'underlay aurait dependu du tenant actif si l'un d'eux reservait autre chose.
if net.prefixlen > 24: #
continue # lien etroit : exempte, cf. le /29 de transit # Ce qui reste, et qui est plus fort : une passerelle doit etre l'adresse d'un hote
octet = int(str(gw).rsplit(".", 1)[1]) # DECLARE sur ce reseau (verifie plus bas). Elle attrape les passerelles fantomes,
if octet != attendu: # ce que le comptage d'octets ne faisait pas.
erreurs.append(
f"reseau '{r.get('nom','?')}': passerelle {gw} finit par .{octet} alors que "
f"les points de routage portent .{attendu} partout "
f"(nomenclature: reservations.passerelle)")
v = (underlay or {}).get("acl_inter_tenant") v = (underlay or {}).get("acl_inter_tenant")
if v is not None and not isinstance(v, bool): if v is not None and not isinstance(v, bool):