frontiere : l'interface d'une regle se derive de l'attachement (D-61)

Le devis rangeait tout flux entrant sur le WAN, en supposant que
l'administration revenait par l'adresse publique. Vrai pour 192.168.255.0/24,
faux depuis que c'est 10.0.0.0/24 — directement attache sur `lan`. Les deux
regles SSH etaient mortes deux fois : mauvaise interface, et « Block private
networks » les aurait filtrees. Le devis conseillait meme de decocher ce filtre
sur le WAN, ce qui aurait affaibli l'interface publique pour rien.

`reseaux_locaux_frontiere()` derive de l'underlay les sous-reseaux ou la
frontiere porte une adresse, hors transit. Un alias par interface : Technolibre
a les deux cotes, Chezlepro seulement la gestion. Sans underlay, tout retombe
sur le WAN — comportement inchange.

P24 confronte desormais chaque regle d'administration a l'attachement de sa
source, et refuse l'ancien comportement.

Nouvel intrant `opnsense_if_gestion`, au catalogue du GUI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-06 14:39:51 -04:00
parent b526160cbc
commit 510568e3b4
5 changed files with 203 additions and 35 deletions

View file

@ -2,6 +2,57 @@
## 2026-08-06 — le chemin nord-sud devient dérivable ## 2026-08-06 — le chemin nord-sud devient dérivable
### La frontière filtre pour de vrai
La règle `any → any` d'`opt1` est retirée. Les 26 règles posées la veille n'étaient jusque-là
qu'une intention derrière un laissez-passer ; elles sont maintenant la politique.
Prouvé dans les deux sens, pas seulement constaté :
```
frontière → asgard sonde de passerelle Online, 0 %, 0.2 ms
asgard → 10.0.4.1 ping depuis vlan40 2 transmis, 0 reçu, 100 % perte
```
Le lien L2 est sain — c'est le pare-feu qui jette. Et la sonde tient : le trafic **émis par le
pare-feu lui-même** passe par `let out anything from firewall host itself`, sa réponse revient
par l'état, et les règles `pass in on opt1` ne le concernent pas. J'avais annoncé un risque
là-dessus ; il n'existait pas.
**Conséquence ferme, désormais mesurée.** Les 14 règles d'`opt1` n'autorisent que les supernets
tenants en source. Rien n'autorise `10.0.4.41/.43/.47`. Poser la route par défaut des
hyperviseurs sur `vlan40` les couperait donc immédiatement : cette tâche dépend maintenant de
l'inventaire d'exploitation de l'hébergeur (D-46/48), qui n'avait pas d'échéance.
### L'interface d'une règle se dérive de l'attachement, pas du sens du flux (D-61)
Le passage de l'administration au VLAN 10 a révélé une hypothèse devenue fausse. Le devis
rangeait tout flux entrant sur le WAN, en supposant que l'administration revenait par
l'adresse publique. Vrai pour `192.168.255.0/24` ; faux pour `10.0.0.0/24`, qui est
directement attaché sur `lan` (igb0, « GESTION »).
Les deux règles SSH étaient donc **mortes deux fois** : mauvaise interface, et
*Block private networks* les aurait filtrées de toute façon. Elles ne fonctionnaient que grâce
au `Default allow LAN to any` hérité. Pire, le devis en tirait un conseil nuisible —
« décocher *Block private networks* sur WAN » — qui aurait affaibli l'interface publique pour
admettre un réseau qui n'y arrive jamais.
`reseaux_locaux_frontiere()` dérive de l'underlay les sous-réseaux où la frontière porte
elle-même une adresse, hors lien de transit. Chaque CIDR d'administration est rangé de ce
côté-là ou du WAN, avec **un alias par interface** : Technolibre a les deux, Chezlepro n'a que
la gestion. L'avertissement RFC1918 ne compte plus que les sources réellement côté WAN.
Sans underlay, tout retombe sur le WAN — le comportement d'avant, inchangé.
**La garde tient la décision.** P24 confronte chaque règle d'administration à l'attachement de
sa source : une règle sur la gestion dont la source n'est attachée nulle part, ou une règle sur
le WAN dont la source est locale, font échouer le devis. Vérifié en rejouant l'ancien
comportement : la garde le refuse.
Nouvel intrant `opnsense_if_gestion` (`lan` ici), au catalogue du GUI. 27 règles au lieu de 26
— Technolibre en gagne une, ayant des sources des deux côtés.
### Le réseau d'administration est le VLAN 10, partout ### Le réseau d'administration est le VLAN 10, partout
`nftables_admin_ssh` déclarait encore `192.168.255.0/24` chez Chezlepro — l'ancien monde. `nftables_admin_ssh` déclarait encore `192.168.255.0/24` chez Chezlepro — l'ancien monde.

View file

@ -43,6 +43,7 @@ sont les seules vérifiables.
| **D-11** | La **sortie générale est déclarée** dans le registre des flux | `block out` est un vrai default-deny ; un besoin oublié ne se manifeste pas par un refus clair | `frontiere-opnsense.md` §8 | P09 | | **D-11** | La **sortie générale est déclarée** dans le registre des flux | `block out` est un vrai default-deny ; un besoin oublié ne se manifeste pas par un refus clair | `frontiere-opnsense.md` §8 | P09 |
| **D-29** | L'**overlay EVPN plafonne à 1450** ; le transport doit donc dépasser 1500 | choix d'exploitation ; l'encapsulation VXLAN coûte 50 octets | `sdn-evpn.md` §5 | P23 (MTU du transport) | | **D-29** | L'**overlay EVPN plafonne à 1450** ; le transport doit donc dépasser 1500 | choix d'exploitation ; l'encapsulation VXLAN coûte 50 octets | `sdn-evpn.md` §5 | P23 (MTU du transport) |
| **D-30** | L'**ICMP « fragmentation nécessaire » est déclaré**, dans les deux sens | à 1450, tout ce qui traverse la frontière dépend de la découverte de MTU de chemin ; une bordure en default-deny la casse en silence | `roles/serveur_debian/meta/flux.yml` | P09 | | **D-30** | L'**ICMP « fragmentation nécessaire » est déclaré**, dans les deux sens | à 1450, tout ce qui traverse la frontière dépend de la découverte de MTU de chemin ; une bordure en default-deny la casse en silence | `roles/serveur_debian/meta/flux.yml` | P09 |
| **D-61** | L'**interface d'une règle se dérive de l'attachement réel** de sa source, jamais du sens du flux | une règle OPNsense est toujours `in` sur l'interface d'arrivée ; posée ailleurs elle ne peut **jamais** correspondre, et le devis affirme alors un accès qui n'existe pas | `frontiere-opnsense.md` §7 | P24 |
| **D-60** | Nommage : tout l'ensemble de bordure est **`bifrost-N`**, et **N est son dernier octet** | on retient un chiffre au lieu d'une table ; le nom ne dit plus le type de la machine, c'est le champ `role` qui le fait — et lui seul pilote le devis | `underlay.yml` | — | | **D-60** | Nommage : tout l'ensemble de bordure est **`bifrost-N`**, et **N est son dernier octet** | on retient un chiffre au lieu d'une table ; le nom ne dit plus le type de la machine, c'est le champ `role` qui le fait — et lui seul pilote le devis | `underlay.yml` | — |
## 2. Qui possède quoi ## 2. Qui possède quoi

View file

@ -109,23 +109,38 @@ Aucun port, aucune adresse et aucun nom d'hôte n'est écrit dans le générateu
**Dans OPNsense, une règle est toujours `in` sur l'interface d'arrivée** — celle par laquelle **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 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. L'attribution se dérive du bloqué sans que rien ne paraisse anormal dans la configuration.
sens du flux :
Pour le trafic ordinaire, l'attribution se dérive du **sens du flux** :
| Flux déclaré | Arrive par | Règle | | Flux déclaré | Arrive par | Règle |
|---|---|---| |---|---|---|
| `sens: ingress`, `pair: externe` | le **WAN** (`69.70.26.62`) | `pass in on wan …` | | `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 …` | | `sens: egress`, `pair: externe` | le **lien de transit** (`opt1`) | `pass in on opt1 …` |
Le SSH d'administration suit la première ligne : le VPN est hébergé sur le pfSense voisin, et **Le SSH d'administration ne suit aucune de ces deux lignes** (D-61). Un plan de gestion peut
son trafic revient par l'adresse publique de la frontière. ê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.
**Le piège de la case à cocher.** Ce trafic d'administration a une source **RFC1918** L'interface se dérive donc de l'underlay : la frontière y déclare une adresse par réseau, et
(`192.168.255.0/24`) alors qu'il arrive sur une interface WAN. Or OPNsense active par défaut tout ce qui est contenu dans l'un de ces sous-réseaux — hors lien de transit — arrive par
*Block private networks* sur le WAN, et ce filtre s'applique **avant** les règles : coché, il `opnsense_if_gestion`. Le reste arrive par le WAN. Un tenant peut avoir les deux, et reçoit
jette le paquet sans qu'aucune règle ne soit consultée. La configuration paraît juste, le SSH alors **deux règles et deux alias**, chacun ne portant que les sources qui peuvent réellement
ne passe pas, et rien ne l'explique. Le devis le signale dès qu'une source RFC1918 entre par emprunter ce chemin.
le WAN — c'est un réglage d'interface, donc invisible dans les règles elles-mêmes.
```
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 **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 les sous-réseaux où il participe** — on retient une adresse, pas treize. `le commutateur` est

View file

@ -63,6 +63,7 @@ PROCHAIN_SAUT = "<PROCHAIN-SAUT-SWITCH>"
NOEUD_SORTIE = "<NOEUD-DE-SORTIE-EVPN>" NOEUD_SORTIE = "<NOEUD-DE-SORTIE-EVPN>"
IF_TRANSIT = "<IF-TRANSIT>" IF_TRANSIT = "<IF-TRANSIT>"
IF_WAN = "<IF-WAN>" IF_WAN = "<IF-WAN>"
IF_GESTION = "<IF-GESTION>"
def depot_hebergeur() -> Path | None: def depot_hebergeur() -> Path | None:
@ -138,6 +139,44 @@ def prochain_saut(transit: dict | None) -> str:
return str((transit or {}).get("passerelle") or "").strip() or PROCHAIN_SAUT return str((transit or {}).get("passerelle") or "").strip() or PROCHAIN_SAUT
def reseaux_locaux_frontiere(underlay: dict | None, transit: dict | None) -> list[str]:
"""Sous-reseaux DIRECTEMENT ATTACHES a la frontiere, hors lien de transit.
C'est le plan de gestion vu du pare-feu : les reseaux ou la frontiere porte elle-meme
une adresse. Un paquet venu de la n'arrive donc NI par le WAN NI par le transit, mais
par l'interface de gestion — et c'est a cette interface que sa regle doit etre posee.
Derive de l'underlay, jamais saisi : la frontiere y declare deja ses adresses, une par
reseau. Un intrant supplementaire ouvrirait la porte a deux valeurs contradictoires.
"""
nom = nom_frontiere(transit)
if not nom:
return []
sur = {str(h.get("reseau")) for h in underlay_mod.hotes(underlay)
if str(h.get("nom") or "") == nom}
sur.discard(str((transit or {}).get("nom")))
cidrs = []
for r in underlay_mod.reseaux(underlay):
if str(r.get("nom")) in sur and r.get("sous_reseau"):
cidrs.append(str(r["sous_reseau"]))
return sorted(cidrs)
def _porte_par(cidr: str, reseaux: list[str]) -> bool:
"""Vrai si `cidr` est contenu dans l'un des `reseaux` (ou lui est egal)."""
try:
r = ipaddress.ip_network(cidr, strict=False)
except ValueError:
return False
for n in reseaux:
try:
if r.subnet_of(ipaddress.ip_network(n, strict=False)):
return True
except (ValueError, TypeError):
continue
return False
def adresse_noeud_de_sortie(underlay: dict | None, transit: dict | None) -> str: def adresse_noeud_de_sortie(underlay: dict | None, transit: dict | None) -> str:
"""Adresse du noeud de sortie PRIMAIRE sur le lien de frontiere. '' si indeterminee. """Adresse du noeud de sortie PRIMAIRE sur le lien de frontiere. '' si indeterminee.
@ -219,6 +258,7 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
_intr = intrants_frontiere() _intr = intrants_frontiere()
if_wan = _ou_marqueur(_intr, "opnsense_if_wan", IF_WAN) if_wan = _ou_marqueur(_intr, "opnsense_if_wan", IF_WAN)
if_transit = _ou_marqueur(_intr, "opnsense_if_transit", IF_TRANSIT) if_transit = _ou_marqueur(_intr, "opnsense_if_transit", IF_TRANSIT)
if_gestion = _ou_marqueur(_intr, "opnsense_if_gestion", IF_GESTION)
nom_actif, nomenclature_active = _instance_active() nom_actif, nomenclature_active = _instance_active()
alias: dict[str, dict] = {} alias: dict[str, dict] = {}
@ -234,18 +274,35 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
# une union laisserait le plan de gestion d'un tenant entrer chez le voisin, ce que # une union laisserait le plan de gestion d'un tenant entrer chez le voisin, ce que
# les ACL de switch interdisent par ailleurs. La frontiere ne doit pas rouvrir ce que # les ACL de switch interdisent par ailleurs. La frontiere ne doit pas rouvrir ce que
# l'isolation inter-tenant ferme. # l'isolation inter-tenant ferme.
#
# Chaque CIDR est en outre RANGE PAR INTERFACE D'ARRIVEE. Une regle OPNsense est
# toujours `in` sur l'interface par ou le paquet entre : posee ailleurs, elle ne peut
# tout simplement jamais correspondre. Un plan de gestion directement attache a la
# frontiere (`10.0.0.0/24` ici) arrive par l'interface de GESTION ; un plan distant,
# qui revient par l'adresse publique ou un VPN, arrive par le WAN.
reseaux_gestion = reseaux_locaux_frontiere(underlay_mod.charger(), transit_underlay())
admin_par_tenant: dict[str, list[str]] = {} admin_par_tenant: dict[str, list[str]] = {}
admin_par_if: dict[str, dict[str, list[str]]] = {}
for nom, pfx, n in tenants: for nom, pfx, n in tenants:
etiquette = f"{pfx}{n['index']}" etiquette = f"{pfx}{n['index']}"
cidrs = admin_de(nom) cidrs = admin_de(nom)
admin_par_tenant[etiquette] = cidrs admin_par_tenant[etiquette] = cidrs
if cidrs: locaux = [c for c in cidrs if _porte_par(c, reseaux_gestion)]
alias[f"SETOPS_ADMIN_{etiquette}"] = { distants = [c for c in cidrs if c not in locaux]
"type": "network", admin_par_if[etiquette] = {"gestion": locaux, "wan": distants}
"contenu": list(cidrs), # UN ALIAS PAR INTERFACE, et le nom le dit. Un alias unique couvrant les deux
"description": f"Reseaux d'administration de {nom} (intrant " # cotes forcerait a poser la meme regle deux fois avec des sources qui ne peuvent
f"nftables_admin_ssh) — source unique, partagee avec ses nftables", # pas arriver la : la moitie serait morte, sans que rien ne le signale.
} for cle, portee in (("gestion", "GESTION"), ("wan", "WAN")):
part = admin_par_if[etiquette][cle]
if part:
alias[f"SETOPS_ADMIN_{etiquette}_{portee}"] = {
"type": "network",
"contenu": list(part),
"description": f"Reseaux d'administration de {nom} arrivant par "
f"{portee.lower()} (intrant nftables_admin_ssh) — "
f"source unique, partagee avec ses nftables",
}
# Union, pour les routes de RETOUR cote switch et la garde P24 : router n'est pas # Union, pour les routes de RETOUR cote switch et la garde P24 : router n'est pas
# autoriser. Le switch doit savoir revenir vers tous les plans de gestion. # autoriser. Le switch doit savoir revenir vers tous les plans de gestion.
admin = sorted({c for cidrs in admin_par_tenant.values() for c in cidrs}) admin = sorted({c for cidrs in admin_par_tenant.values() for c in cidrs})
@ -256,9 +313,12 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
# RFC1918 exactement — c'est le perimetre de la case « Block private networks ». # RFC1918 exactement — c'est le perimetre de la case « Block private networks ».
# `ipaddress.is_private` serait trop large : il inclut les plages de documentation # `ipaddress.is_private` serait trop large : il inclut les plages de documentation
# et le CGNAT, que la case voisine « Block bogon networks » traite separement. # et le CGNAT, que la case voisine « Block bogon networks » traite separement.
#
# SEULS les CIDR ranges cote WAN comptent : conseiller de decocher le filtre pour un
# reseau qui arrive par la gestion affaiblirait l'interface publique sans rien ouvrir.
rfc1918 = [ipaddress.ip_network(b) for b in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")] rfc1918 = [ipaddress.ip_network(b) for b in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")]
admin_prives = sorted({ admin_prives = sorted({
c for cidrs in admin_par_tenant.values() for c in cidrs c for parts in admin_par_if.values() for c in parts["wan"]
if any(ipaddress.ip_network(c, strict=False).subnet_of(b) for b in rfc1918) if any(ipaddress.ip_network(c, strict=False).subnet_of(b) for b in rfc1918)
}) })
@ -302,23 +362,32 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
# l'omettre ferme l'acces, l'ouvrir a `any` exposerait le SSH a Internet. # l'omettre ferme l'acces, l'ouvrir a `any` exposerait le SSH a Internet.
tenants_sans_admin.append(nom) tenants_sans_admin.append(nom)
continue continue
source = f"SETOPS_ADMIN_{etiquette}" if ssh_gestion else "any" # Interface d'ARRIVEE du paquet, celle a laquelle OPNsense attache la regle.
regles.append({ # Sortant (la flotte vers Internet) : le lien de transit, par ou le paquet
"sens": "in" if entrant else "out", # penetre le pare-feu. Entrant depuis Internet : le WAN. Entrant depuis un
# Interface d'ARRIVEE du paquet, celle a laquelle OPNsense attache la # plan d'administration : l'interface ou ce plan est REELLEMENT attache —
# regle. Entrant (depuis Internet ou le VPN d'admin, qui revient par # une regle posee sur la mauvaise interface ne correspond jamais.
# l'adresse publique) : le WAN. Sortant (la flotte vers Internet) : le if ssh_gestion:
# lien de transit, par ou le paquet penetre le pare-feu. portees = []
"interface": if_wan if entrant else if_transit, if admin_par_if[etiquette]["gestion"]:
"protocole": fl.get("protocole", "tcp"), portees.append((f"SETOPS_ADMIN_{etiquette}_GESTION", if_gestion))
"source": source if entrant else destination, if admin_par_if[etiquette]["wan"]:
"destination": destination if entrant else "any", portees.append((f"SETOPS_ADMIN_{etiquette}_WAN", if_wan))
"ports": _ports(fl), else:
"chiffrement": fl.get("chiffrement"), portees = [("any", if_wan if entrant else if_transit)]
"role": role, for source, interface in portees:
"tenant": nom, regles.append({
"raison": fl.get("raison", ""), "sens": "in" if entrant else "out",
}) "interface": interface,
"protocole": fl.get("protocole", "tcp"),
"source": source if entrant else destination,
"destination": destination if entrant else "any",
"ports": _ports(fl),
"chiffrement": fl.get("chiffrement"),
"role": role,
"tenant": nom,
"raison": fl.get("raison", ""),
})
intrants = intrants_frontiere() intrants = intrants_frontiere()
transit = transit_underlay() transit = transit_underlay()
@ -347,6 +416,8 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
"wan_ip": str(intrants.get("opnsense_wan_ip") or "").strip(), "wan_ip": str(intrants.get("opnsense_wan_ip") or "").strip(),
"if_wan": _ou_marqueur(intrants, "opnsense_if_wan", IF_WAN), "if_wan": _ou_marqueur(intrants, "opnsense_if_wan", IF_WAN),
"if_transit": _ou_marqueur(intrants, "opnsense_if_transit", IF_TRANSIT), "if_transit": _ou_marqueur(intrants, "opnsense_if_transit", IF_TRANSIT),
"if_gestion": _ou_marqueur(intrants, "opnsense_if_gestion", IF_GESTION),
"reseaux_gestion": reseaux_gestion,
"alias": alias, "alias": alias,
"routes": routes, "routes": routes,
"regles": regles, "regles": regles,
@ -422,6 +493,9 @@ def rendre(devis: dict) -> str:
f"# WAN : {devis['if_wan']}" f"# WAN : {devis['if_wan']}"
+ (f"{devis['wan_ip']} (face publique de l'ecosysteme)" if devis.get("wan_ip") else ""), + (f"{devis['wan_ip']} (face publique de l'ecosysteme)" if devis.get("wan_ip") else ""),
f"# TRANSIT : {devis['if_transit']} (trunk 802.1Q vers les switches)", f"# TRANSIT : {devis['if_transit']} (trunk 802.1Q vers les switches)",
f"# GESTION : {devis['if_gestion']}"
+ (f" — plan(s) attache(s) : {', '.join(devis['reseaux_gestion'])}"
if devis.get("reseaux_gestion") else " — aucun plan attache a la frontiere"),
] ]
if tr: if tr:
out += [ out += [
@ -530,6 +604,32 @@ def verifier(devis: dict) -> tuple[bool, list[str]]:
for nom, a in devis["alias"].items(): for nom, a in devis["alias"].items():
if not a["contenu"]: if not a["contenu"]:
erreurs.append(f"Alias {nom} vide : une regle le referencerait dans le vide.") erreurs.append(f"Alias {nom} vide : une regle le referencerait dans le vide.")
# Une regle est TOUJOURS `in` sur l'interface d'arrivee du paquet. Posee ailleurs elle
# ne correspond jamais : le devis affirme un acces qui n'existe pas, et la panne ne se
# voit qu'a l'usage. On confronte donc chaque source d'administration a l'endroit ou
# son reseau est reellement attache.
gestion = devis.get("reseaux_gestion") or []
for r in devis["regles"]:
src = str(r.get("source") or "")
if not src.startswith("SETOPS_ADMIN_"):
continue
cidrs = (devis["alias"].get(src) or {}).get("contenu") or []
locaux = [c for c in cidrs if _porte_par(c, gestion)]
if r["interface"] == devis.get("if_gestion") and len(locaux) != len(cidrs):
erreurs.append(
f"Regle sur {r['interface']} (gestion) avec la source {src} : "
+ ", ".join(c for c in cidrs if c not in locaux)
+ " n'est attache a aucun plan local de la frontiere — le paquet "
"arriverait par une autre interface et la regle ne matcherait jamais."
)
if r["interface"] == devis.get("if_wan") and locaux:
erreurs.append(
f"Regle sur {r['interface']} (WAN) avec la source {src} : "
+ ", ".join(locaux)
+ " est directement attache a la frontiere, donc arrive par "
f"{devis.get('if_gestion')} — cette regle est morte."
)
return (not erreurs), erreurs return (not erreurs), erreurs

View file

@ -179,6 +179,7 @@ INTRANTS_SCHEMA = [
("opnsense_wan_ip", "frontiere", "constante", "Frontière", "Adresse publique du WAN de la frontière (face Internet de l'écosystème)", "str", "hebergeur"), ("opnsense_wan_ip", "frontiere", "constante", "Frontière", "Adresse publique du WAN de la frontière (face Internet de l'écosystème)", "str", "hebergeur"),
("opnsense_if_wan", "frontiere", "defaut", "Frontière", "Interface WAN — IDENTIFIANT OPNsense (wan), pas le libellé ni le périphérique", "str", "hebergeur"), ("opnsense_if_wan", "frontiere", "defaut", "Frontière", "Interface WAN — IDENTIFIANT OPNsense (wan), pas le libellé ni le périphérique", "str", "hebergeur"),
("opnsense_if_transit", "frontiere", "defaut", "Frontière", "Interface de transit — IDENTIFIANT OPNsense (opt1, opt2…), pas « igb1 » ni le libellé affiché", "str", "hebergeur"), ("opnsense_if_transit", "frontiere", "defaut", "Frontière", "Interface de transit — IDENTIFIANT OPNsense (opt1, opt2…), pas « igb1 » ni le libellé affiché", "str", "hebergeur"),
("opnsense_if_gestion", "frontiere", "defaut", "Frontière", "Interface où arrivent les plans d'administration attachés à la frontière — IDENTIFIANT OPNsense (lan). Une règle SSH posée ailleurs ne peut jamais correspondre", "str", "hebergeur"),
("underlay_routeur", "fabric", "constante", "Fabric", "Switch qui porte le routage (SVI, ACL, routes) — les autres restent en L2 pur", "str", "hebergeur"), ("underlay_routeur", "fabric", "constante", "Fabric", "Switch qui porte le routage (SVI, ACL, routes) — les autres restent en L2 pur", "str", "hebergeur"),
("underlay_dialecte", "fabric", "constante", "Fabric", "Dialecte de CLI du commutateur (cisco | binardat)", "str", "hebergeur"), ("underlay_dialecte", "fabric", "constante", "Fabric", "Dialecte de CLI du commutateur (cisco | binardat)", "str", "hebergeur"),
("underlay_stp_mode", "fabric", "defaut", "Fabric", "Mode de spanning-tree (rstp | mstp | pvst)", "str", "hebergeur"), ("underlay_stp_mode", "fabric", "defaut", "Fabric", "Mode de spanning-tree (rstp | mstp | pvst)", "str", "hebergeur"),