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:
parent
b526160cbc
commit
510568e3b4
5 changed files with 203 additions and 35 deletions
51
CHANGELOG.md
51
CHANGELOG.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue