devis switch : un seul routeur, les autres en L2 pur
Sans MLAG, le routage est porté par un unique switch (`underlay.routeur`). Le devis émettait un jeu unique de SVI sans dire à quel switch il s'adressait : appliqué sur les trois, il aurait créé autant de conflits d'adresses qu'il y a de zones. Il se scinde désormais en deux : - partie A (switch routeur) : VLANs, SVI, ACL, trunks, routes ; - partie B (switches d'accès, L2 pur) : mêmes VLANs pour commuter les trames étiquetées, une adresse de gestion par switch tirée de `underlay.hotes` avec `ip default-gateway` vers le routeur, et les trunks. Aucun SVI de zone, aucune ACL, aucune route. Gardes : `make underlay` refuse un `routeur` inconnu des hôtes déclarés ; la partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine. Sans routeur désigné, l'en-tête signale le risque de duplication au lieu de laisser croire que le devis est applicable partout. Preuves : 24 OK, 0 échec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7b2ba07ffc
commit
2d2c5c69fa
5 changed files with 130 additions and 5 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
|
@ -1,5 +1,28 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-01 (suite 4) — un seul switch route, les autres en L2 pur
|
||||
|
||||
### Décidé — `underlay.routeur`
|
||||
Sans MLAG, le routage est porté par un **unique** switch (`bifrost-01` chez Chezlepro) ; les
|
||||
autres restent en L2 pur. Le devis émettait jusqu'ici un jeu unique de SVI sans dire à quel
|
||||
switch il s'adressait : appliqué sur les trois, il aurait créé autant de conflits d'adresses
|
||||
qu'il y a de zones.
|
||||
|
||||
### Ajouté — le devis se scinde en deux parties
|
||||
- **Partie A — switch routeur** : VLANs, SVI, ACL d'isolation, trunks, routes. Va sur lui
|
||||
seul, et l'en-tête le nomme.
|
||||
- **Partie B — switches d'accès (L2 pur)** : mêmes VLANs pour commuter les trames étiquetées,
|
||||
une adresse de gestion par switch (tirée de `underlay.hotes`) avec `ip default-gateway`
|
||||
vers le routeur, et les trunks. Aucun SVI de zone, aucune ACL, aucune route.
|
||||
|
||||
Deux gardes : `make underlay` refuse un `routeur` qui ne nomme aucun hôte déclaré ; et la
|
||||
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine — les coller tous
|
||||
écraserait l'adresse. Sans `routeur` désigné, l'en-tête signale explicitement le risque de
|
||||
duplication au lieu de laisser croire que le devis est applicable partout.
|
||||
|
||||
Reste ouvert et consigné : la syntaxe des `ip route` n'est pas dialecte-consciente,
|
||||
contrairement aux ACL.
|
||||
|
||||
## 2026-08-01 (suite 3) — les tenants n'atteignent plus l'underlay
|
||||
|
||||
### Corrigé — ACL de switch : `deny` vers la fabric physique
|
||||
|
|
|
|||
|
|
@ -12,13 +12,21 @@ 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` |
|
||||
| **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.
|
||||
|
|
@ -192,10 +200,9 @@ ansible-vault edit instance/inventories/principal/group_vars/all/vault.yml
|
|||
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.
|
||||
- **Trois switches, un seul devis** — `devis_reseau.py` émet un jeu unique de SVI sans
|
||||
désigner le switch qui les porte. Sans MLAG, il reste à trancher entre « un seul routeur,
|
||||
les deux autres en L2 pur » et « SVI répliqués en VRRP ». Appliquer le devis tel quel sur
|
||||
les trois créerait des conflits d'adresses.
|
||||
- **La syntaxe des routes n'est pas dialecte-consciente** — les ACL le sont (masque normal
|
||||
en `binardat`, wildcard en `cisco`), les `ip route` de la section 5 sortent en forme Cisco
|
||||
quel que soit le dialecte. À vérifier sur le matériel réel.
|
||||
- **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
|
||||
|
|
|
|||
|
|
@ -82,6 +82,61 @@ def section_underlay(underlay: dict | None) -> list[str]:
|
|||
return out
|
||||
|
||||
|
||||
def partie_acces(underlay: dict | None, tenants: list, vlans: str) -> list[str]:
|
||||
"""Devis des switches d'ACCES : L2 pur, aucun SVI de zone, aucune ACL, aucune route.
|
||||
|
||||
Sans MLAG, un seul switch route (cf. `underlay.routeur`). Les autres commutent les
|
||||
trames etiquetees et n'ont d'adresse IP que pour leur propre gestion — ils joignent
|
||||
le reste par la passerelle de management, pas par un SVI a eux.
|
||||
"""
|
||||
r_nom = underlay_mod.routeur(underlay)
|
||||
if not r_nom:
|
||||
return []
|
||||
mgmt = next((r for r in underlay_mod.reseaux(underlay) if r.get("passerelle")), None)
|
||||
autres = [h for h in underlay_mod.hotes(underlay) if h.get("nom") != r_nom]
|
||||
if not autres:
|
||||
return []
|
||||
out = [
|
||||
"",
|
||||
"! ============================================================",
|
||||
f"! PARTIE B — SWITCHES D'ACCES (L2 pur) : {', '.join(h['nom'] for h in autres)}",
|
||||
f"! Le routage est porte par {r_nom} SEUL (underlay.routeur). Ces switches",
|
||||
"! commutent les trames etiquetees et ne portent AUCUN SVI de zone, AUCUNE",
|
||||
"! ACL, AUCUNE route : dupliquer les SVI creerait autant de conflits d'IP.",
|
||||
"! ============================================================",
|
||||
"configure terminal",
|
||||
"!",
|
||||
"! ----- B1. Memes VLANs (pour commuter les trames etiquetees) -----",
|
||||
]
|
||||
for r in underlay_mod.reseaux(underlay):
|
||||
out += [f"vlan {r['vlan']}", f" name {r['nom']}"]
|
||||
for nom, pfx, n in tenants:
|
||||
for zone in sorted(n["categories"]):
|
||||
out += [f"vlan {vlan_de(n['index'], zone)}",
|
||||
f" name {pfx}{n['index']}-{n['categories'][zone]['libelle']}"]
|
||||
out += ["!", "! ----- B2. Gestion (seule adresse IP du switch) -----",
|
||||
"! /!\\ UN SEUL de ces blocs par switch : celui qui porte son nom.",
|
||||
"! Les coller tous les deux sur la meme machine ecraserait son adresse."]
|
||||
if mgmt:
|
||||
net = ipaddress.ip_network(mgmt["sous_reseau"], strict=False)
|
||||
for h in autres:
|
||||
out += [f"! --- {h['nom']} ---",
|
||||
f"interface Vlan{mgmt['vlan']}",
|
||||
f" description gestion {h['nom']}",
|
||||
f" ip address {h['ip']} {net.netmask}",
|
||||
" no shutdown",
|
||||
f"ip default-gateway {mgmt['passerelle']} ! le routeur {r_nom}"]
|
||||
else:
|
||||
out.append("! Aucun reseau underlay avec passerelle : gestion a definir a la main.")
|
||||
out += ["!", "! ----- B3. Trunks -----",
|
||||
"! Vers les hyperviseurs ET vers le routeur : memes VLANs etiquetes.",
|
||||
"interface <PORT-TRUNK>",
|
||||
" switchport mode trunk",
|
||||
f" switchport trunk allowed vlan add {vlans}",
|
||||
"!", "end", "write memory"]
|
||||
return out
|
||||
|
||||
|
||||
def section_frontiere(transit: dict | None) -> list[str]:
|
||||
"""Port du switch vers le pare-feu de bordure. Porte le VLAN de transit, et lui seul.
|
||||
|
||||
|
|
@ -174,6 +229,7 @@ def decouvrir() -> list[tuple[str, str, dict]]:
|
|||
def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -> str:
|
||||
dialecte = dialecte or DIALECTE_DEFAUT
|
||||
underlay = underlay_mod.charger()
|
||||
r_nom = underlay_mod.routeur(underlay)
|
||||
out = ["configure terminal", f"! dialecte CLI : {dialecte}", "!"]
|
||||
out += [
|
||||
"! ============================================================",
|
||||
|
|
@ -181,6 +237,22 @@ def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -
|
|||
"! Genere par scripts/devis_reseau.py depuis les nomenclatures.",
|
||||
"! VLAN = 1000 + index*10 + zone (unique globalement sur le trunk).",
|
||||
"! Underlay (VLAN < 1000) = fabric physique, cluster-global.",
|
||||
]
|
||||
if r_nom:
|
||||
out += [
|
||||
"!",
|
||||
f"! PARTIE A — SWITCH ROUTEUR : {r_nom} (underlay.routeur)",
|
||||
"! Sans MLAG, UN SEUL switch route. Cette partie ne va QUE sur lui.",
|
||||
"! Les autres recoivent la partie B (L2 pur), plus bas.",
|
||||
]
|
||||
else:
|
||||
out += [
|
||||
"!",
|
||||
"! /!\\ AUCUN ROUTEUR DESIGNE (cle `routeur` dans underlay.yml).",
|
||||
"! Applique tel quel sur plusieurs switches, ce devis duplique les SVI",
|
||||
"! et cree autant de conflits d'adresses. Designer le switch routeur.",
|
||||
]
|
||||
out += [
|
||||
"! ============================================================",
|
||||
"!",
|
||||
]
|
||||
|
|
@ -243,6 +315,8 @@ def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -
|
|||
out += ["!"] + section_frontiere(transit)
|
||||
out += ["!"] + section_routes(underlay)
|
||||
out += ["end", "write memory"]
|
||||
# Les switches d'acces : memes VLANs, aucun SVI de zone, aucune ACL, aucune route.
|
||||
out += partie_acces(underlay, tenants, ",".join(vlans_underlay + vlans_tenants))
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,15 @@ def hotes(underlay: dict | None) -> list[dict]:
|
|||
return (underlay or {}).get("hotes", []) or []
|
||||
|
||||
|
||||
def routeur(underlay: dict | None) -> str | None:
|
||||
"""Nom du switch qui porte les SVI, ou None si aucun n'est designe.
|
||||
|
||||
Sans MLAG, UN SEUL switch route : les autres restent en L2 pur. Appliquer le
|
||||
meme devis sur les trois creerait autant de conflits d'adresses que de SVI.
|
||||
"""
|
||||
return (underlay or {}).get("routeur") or None
|
||||
|
||||
|
||||
def reseau_transit(underlay: dict | None) -> dict | None:
|
||||
"""Le reseau de TRANSIT vers la frontiere nord/sud, s'il est declare.
|
||||
|
||||
|
|
@ -151,6 +160,12 @@ def valider(underlay: dict | None,
|
|||
erreurs.append(f"un seul reseau de transit autorise (passerelle_sortie), "
|
||||
f"trouves : {', '.join(transits)}")
|
||||
|
||||
# Le routeur designe doit etre un hote declare : sinon le devis ne saurait pas
|
||||
# quelle adresse de gestion donner aux switches d'acces.
|
||||
r_nom = routeur(underlay)
|
||||
if r_nom and r_nom not in {h.get("nom") for h in hotes(underlay)}:
|
||||
erreurs.append(f"routeur '{r_nom}': aucun hote de ce nom n'est declare")
|
||||
|
||||
par_nom = {r.get("nom"): r for r in reseaux(underlay)}
|
||||
for h in hotes(underlay):
|
||||
hn = h.get("nom", "?")
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@
|
|||
# Storage/Ceph restent en general L2 pur (pas de passerelle).
|
||||
---
|
||||
underlay:
|
||||
# Switch qui porte le routage : SVI de zone, ACL d'isolation, routes. Sans MLAG,
|
||||
# UN SEUL switch route ; les autres restent en L2 pur et reçoivent la partie B du
|
||||
# devis. Dupliquer les SVI sur plusieurs switches creerait autant de conflits
|
||||
# d'adresses qu'il y a de zones. Doit nommer un hote declare dans `hotes:`.
|
||||
routeur: switch-01
|
||||
|
||||
reseaux:
|
||||
- nom: management
|
||||
description: Switches, mgmt Proxmox, OOB/IPMI
|
||||
|
|
|
|||
Loading…
Reference in a new issue