pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Devis de pare-feu Proxmox — le filtrage EST-OUEST intra-tenant.
|
|
|
|
|
|
|
|
|
|
DERIVE du meme registre des flux que les nftables d'hote et que la frontiere. Les trois
|
|
|
|
|
lisent `roles/*/meta/flux.yml` par les MEMES fonctions : ils ne peuvent pas diverger.
|
|
|
|
|
|
|
|
|
|
PARTAGE DES ROLES (cf. docs/sdn-evpn.md) :
|
|
|
|
|
- ce devis : est-ouest INTRA-tenant, applique par l'hyperviseur ;
|
|
|
|
|
- `make flux` : la meme politique, rejouee sur l'hote lui-meme (defense en
|
|
|
|
|
profondeur — une VM compromise franchit deux barrieres) ;
|
|
|
|
|
- `make devis-opnsense` : tout ce qui ENTRE ou SORT d'un tenant.
|
|
|
|
|
|
|
|
|
|
Les flux `pair: externe` sont donc SAUTES ici : ils relevent de la frontiere.
|
|
|
|
|
|
|
|
|
|
NON destructif : ce script n'ecrit rien sur le cluster. Il produit un devis a relire,
|
|
|
|
|
puis (a venir) un JSON pour l'API Proxmox.
|
|
|
|
|
|
|
|
|
|
Usage :
|
|
|
|
|
python3 scripts/devis_proxmox_fw.py # devis lisible
|
|
|
|
|
python3 scripts/devis_proxmox_fw.py --json # meme contenu, pour l'API
|
|
|
|
|
python3 scripts/devis_proxmox_fw.py --verifier # garde : aucun role sans source
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
RACINE = Path(__file__).resolve().parents[1]
|
|
|
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
|
|
|
|
|
|
|
|
|
from resoudre_flux import ( # noqa: E402
|
2026-08-03 11:53:38 -04:00
|
|
|
GROUPE_EDGE,
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
charger_flux,
|
|
|
|
|
est_groupe_operationnel,
|
|
|
|
|
_enfants,
|
|
|
|
|
_hotes_du_groupe,
|
|
|
|
|
_ip_par_hote,
|
|
|
|
|
_pairs,
|
|
|
|
|
_resoudre_sources,
|
|
|
|
|
)
|
|
|
|
|
from devis_reseau import admin_de, decouvrir, inventaire_de, prefixe # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 11:50:50 -04:00
|
|
|
LONGUEUR_MAX_GROUPE = 18 # limite de Proxmox pour un nom de groupe de securite
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
|
|
|
|
|
|
2026-08-03 11:50:50 -04:00
|
|
|
def _role_court(role: str) -> str:
|
|
|
|
|
"""Forme abregee d'un role, commune aux IPSets et aux groupes."""
|
|
|
|
|
return role.replace("serveur_", "srv-").replace("client_", "cli-").replace("_", "-")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def nom_ipset(role: str, index: int) -> str:
|
|
|
|
|
"""Nom d'IPSet. MEME prefixe que les groupes — `t17-` — pour qu'un seul schema de
|
|
|
|
|
nommage se lise dans tout le devis.
|
|
|
|
|
|
|
|
|
|
Pas de troncature ici : Proxmox est large sur les IPSets, etroit sur les groupes.
|
|
|
|
|
Un nom peut donc etre entier cote IPSet et abrege cote groupe ; chacun respecte sa
|
|
|
|
|
contrainte, et le prefixe reste commun.
|
|
|
|
|
"""
|
|
|
|
|
return f"t{index}-{_role_court(role)}".lower()
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def nom_groupe(role: str, index: int) -> str:
|
|
|
|
|
"""Nom de groupe de securite, sous la limite de 18 caracteres de Proxmox.
|
|
|
|
|
|
|
|
|
|
Le prefixe porte l'INDEX du tenant plutot que son etiquette : `t17-` au lieu de
|
|
|
|
|
`chez17-`, ce qui laisse trois caracteres de plus au role et rend la troncature
|
|
|
|
|
beaucoup plus rare. Les groupes sont a l'echelle du datacenter, d'ou le prefixe.
|
|
|
|
|
"""
|
2026-08-03 11:50:50 -04:00
|
|
|
return f"t{index}-{_role_court(role)}".lower()[:LONGUEUR_MAX_GROUPE]
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _verifier_unicite(noms: list[str], tenant: str) -> None:
|
|
|
|
|
"""Une troncature qui fait collisionner deux roles FUSIONNERAIT leurs regles.
|
|
|
|
|
|
|
|
|
|
Silencieusement, et dans le mauvais sens : une VM recevrait les autorisations d'un
|
|
|
|
|
role qu'elle ne porte pas. On echoue plutot que d'emettre un devis pareil.
|
|
|
|
|
"""
|
|
|
|
|
vus: dict[str, str] = {}
|
|
|
|
|
for role, nom in noms:
|
|
|
|
|
if nom in vus:
|
|
|
|
|
raise SystemExit(
|
|
|
|
|
f"erreur: {tenant} — les roles '{vus[nom]}' et '{role}' donnent le meme "
|
|
|
|
|
f"groupe '{nom}' apres troncature a {LONGUEUR_MAX_GROUPE} caracteres. "
|
|
|
|
|
f"Leurs regles fusionneraient. Renommer un role ou raccourcir le prefixe.")
|
|
|
|
|
vus[nom] = role
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ports(fl: dict) -> list[str]:
|
|
|
|
|
p = fl["port"]
|
|
|
|
|
return [str(x) for x in (p if isinstance(p, list) else [p])]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|
|
|
|
"""Le devis, en structure. Un bloc par tenant federe."""
|
|
|
|
|
flux_par_role = charger_flux()
|
|
|
|
|
blocs: list[dict] = []
|
|
|
|
|
|
|
|
|
|
for nom, pfx, n in tenants:
|
|
|
|
|
inv = inventaire_de(nom)
|
|
|
|
|
if inv is None:
|
|
|
|
|
continue
|
|
|
|
|
etiquette = f"{pfx}{n['index']}"
|
|
|
|
|
data = yaml.safe_load(inv.read_text(encoding="utf-8")) or {}
|
|
|
|
|
# Les PLANIFIES comptent : le pare-feu se prepare avant que la VM existe,
|
|
|
|
|
# sinon le devis changerait a chaque materialisation d'hote.
|
|
|
|
|
tous = set(_ip_par_hote(data))
|
|
|
|
|
ips = _ip_par_hote(data)
|
|
|
|
|
|
|
|
|
|
# Un IPSet par role porte dans ce tenant : les membres sont ses hotes.
|
2026-08-03 11:53:38 -04:00
|
|
|
# `flotte` en recoit un aussi : sans lui, six regles par tenant portaient
|
|
|
|
|
# quatorze adresses en dur, illisibles et impossibles a mettre a jour d'un geste.
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
ipsets: dict[str, dict] = {}
|
2026-08-03 11:53:38 -04:00
|
|
|
if ips:
|
|
|
|
|
ipsets[f"t{n['index']}-flotte"] = {
|
|
|
|
|
"role": "flotte (tous les hotes du tenant)", "membres": sorted(set(ips.values())),
|
|
|
|
|
}
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
for groupe in sorted(_enfants(data)):
|
|
|
|
|
if not est_groupe_operationnel(groupe):
|
|
|
|
|
continue
|
|
|
|
|
membres = sorted({ips[h] for h in _hotes_du_groupe(data, groupe) if h in ips})
|
|
|
|
|
if membres:
|
2026-08-03 11:50:50 -04:00
|
|
|
ipsets[nom_ipset(groupe, n["index"])] = {
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
"role": groupe, "membres": membres,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
groupes: list[dict] = []
|
|
|
|
|
sans_source: list[str] = []
|
2026-08-03 11:53:38 -04:00
|
|
|
utilises: set[str] = set()
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
for groupe in sorted(_enfants(data)):
|
|
|
|
|
if not est_groupe_operationnel(groupe):
|
|
|
|
|
continue
|
|
|
|
|
if not _hotes_du_groupe(data, groupe):
|
|
|
|
|
continue
|
|
|
|
|
regles: list[dict] = []
|
|
|
|
|
for fl in flux_par_role.get(groupe, []):
|
|
|
|
|
if fl.get("sens") != "ingress":
|
|
|
|
|
continue
|
|
|
|
|
pairs = _pairs(fl)
|
|
|
|
|
if "externe" in pairs:
|
|
|
|
|
continue # la frontiere s'en charge
|
|
|
|
|
if pairs == ["localhost"]:
|
|
|
|
|
continue # boucle locale : aucune regle inter-noeud
|
2026-08-03 11:59:58 -04:00
|
|
|
if not _resoudre_sources(data, fl.get("pair"), tous, ips):
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
sans_source.append(f"{groupe}:{','.join(_ports(fl))}")
|
|
|
|
|
continue
|
2026-08-03 11:59:58 -04:00
|
|
|
# UNE REGLE PAR ROLE SOURCE. Un flux dont le pair liste quatre roles
|
|
|
|
|
# donne quatre regles, chacune renvoyant a l'IPSet de son role : la
|
|
|
|
|
# regle porte alors QUI elle autorise, au lieu d'aligner des adresses.
|
|
|
|
|
# `flotte` et `edge` sont des mots-cles, mais ont leur IPSet eux aussi.
|
|
|
|
|
for pair in pairs:
|
|
|
|
|
if pair in ("localhost", "expositions", "derive"):
|
|
|
|
|
continue
|
2026-08-03 11:53:38 -04:00
|
|
|
cle = {"flotte": f"t{n['index']}-flotte",
|
|
|
|
|
"edge": nom_ipset(GROUPE_EDGE, n["index"])}.get(
|
2026-08-03 11:59:58 -04:00
|
|
|
pair, nom_ipset(pair, n["index"]))
|
|
|
|
|
srcs_pair = _resoudre_sources(data, pair, tous, ips)
|
|
|
|
|
if not srcs_pair:
|
|
|
|
|
continue # ce role n'est pas porte dans ce tenant
|
2026-08-03 11:53:38 -04:00
|
|
|
if cle in ipsets:
|
2026-08-03 11:59:58 -04:00
|
|
|
source, via = f"+{cle}", pair
|
2026-08-03 11:53:38 -04:00
|
|
|
utilises.add(cle)
|
2026-08-03 11:59:58 -04:00
|
|
|
else:
|
|
|
|
|
source, via = ",".join(srcs_pair), pair
|
|
|
|
|
for port in _ports(fl):
|
|
|
|
|
regles.append({
|
|
|
|
|
"action": "ACCEPT", "sens": "IN",
|
|
|
|
|
"source": source, "via": via,
|
|
|
|
|
"proto": fl.get("protocole", "tcp"),
|
|
|
|
|
"dport": port,
|
|
|
|
|
"raison": fl.get("raison", ""),
|
|
|
|
|
})
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
if regles:
|
|
|
|
|
groupes.append({
|
|
|
|
|
"nom": nom_groupe(groupe, n["index"]), "role": groupe, "regles": regles,
|
|
|
|
|
})
|
|
|
|
|
_verifier_unicite([(g["role"], g["nom"]) for g in groupes], nom)
|
|
|
|
|
|
|
|
|
|
# Affectation : quelle VM recoit quels groupes.
|
|
|
|
|
affect: list[dict] = []
|
|
|
|
|
par_nom_groupe = {g["role"]: g["nom"] for g in groupes}
|
2026-08-03 11:47:13 -04:00
|
|
|
# TOUS les hotes du plan, actifs comme planifies — meme regle que les IPSets.
|
|
|
|
|
# Se limiter aux actifs donnait un devis different selon l'etat du tenant : un
|
|
|
|
|
# operateur aurait lu qu'une seule VM avait besoin de regles.
|
|
|
|
|
for hote in sorted(ips):
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
vmid = None
|
|
|
|
|
for g, membres in _enfants(data).items():
|
|
|
|
|
h = (membres.get("hosts") or {}).get(hote) or {}
|
|
|
|
|
if h.get("proxmox_vmid"):
|
|
|
|
|
vmid = h["proxmox_vmid"]
|
|
|
|
|
break
|
|
|
|
|
porte = [par_nom_groupe[g] for g in sorted(_enfants(data))
|
|
|
|
|
if hote in (_enfants(data)[g].get("hosts") or {}) and g in par_nom_groupe]
|
|
|
|
|
if porte:
|
|
|
|
|
affect.append({"hote": hote, "vmid": vmid, "groupes": porte})
|
|
|
|
|
|
|
|
|
|
blocs.append({
|
|
|
|
|
"tenant": nom, "etiquette": etiquette,
|
2026-08-03 11:53:38 -04:00
|
|
|
"ipsets": {k: v for k, v in ipsets.items() if k in utilises},
|
|
|
|
|
"groupes": groupes,
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
"affectations": affect, "sans_source": sorted(set(sans_source)),
|
|
|
|
|
"admin": admin_de(nom),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {"blocs": blocs}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rendre(devis: dict) -> str:
|
|
|
|
|
out = [
|
|
|
|
|
"# ============================================================",
|
|
|
|
|
"# DEVIS PARE-FEU PROXMOX — est-ouest INTRA-tenant",
|
|
|
|
|
"# Genere par scripts/devis_proxmox_fw.py. NE PAS EDITER A LA MAIN.",
|
|
|
|
|
"# Derive du MEME registre des flux que les nftables d'hote : les deux",
|
|
|
|
|
"# barrieres disent la meme chose, et ne peuvent pas diverger.",
|
|
|
|
|
"#",
|
|
|
|
|
"# Ce qui ENTRE ou SORT d'un tenant n'est PAS ici : c'est la frontiere",
|
|
|
|
|
"# (`make devis-opnsense`). Les flux `pair: externe` sont sautes.",
|
|
|
|
|
"# ============================================================",
|
|
|
|
|
"",
|
|
|
|
|
"## 0. Prerequis, au niveau du datacenter",
|
|
|
|
|
"# Pare-feu active, politique d'entree DROP. Sans cela, ces regles",
|
|
|
|
|
"# s'ajoutent a un tout-permis et n'interdisent rien.",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
for b in devis["blocs"]:
|
|
|
|
|
out += [f"## Tenant {b['tenant']} ({b['etiquette']})", ""]
|
|
|
|
|
out.append(f"### IPSets — {len(b['ipsets'])} (les membres d'un role, en un seul endroit)")
|
|
|
|
|
for nom, s in sorted(b["ipsets"].items()):
|
|
|
|
|
out.append(f"ipset {nom} # {s['role']}")
|
|
|
|
|
for ip in s["membres"]:
|
|
|
|
|
out.append(f" {ip}")
|
|
|
|
|
out += ["", f"### Groupes de securite — {len(b['groupes'])} (les flux entrants d'un role)"]
|
|
|
|
|
for g in b["groupes"]:
|
|
|
|
|
out.append(f"securitygroup {g['nom']} # {g['role']}")
|
2026-08-03 11:59:58 -04:00
|
|
|
# La raison est portee par le FLUX, pas par chacune de ses regles : quatre
|
|
|
|
|
# roles autorises sur un meme port la repeteraient quatre fois. On la sort
|
|
|
|
|
# une seule fois, au-dessus du paquet qu'elle explique.
|
|
|
|
|
derniere = None
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
for r in g["regles"]:
|
2026-08-03 11:59:58 -04:00
|
|
|
cle = (r["dport"], r["proto"], r["raison"])
|
|
|
|
|
if cle != derniere and r["raison"]:
|
|
|
|
|
out.append(f" # {r['raison']}")
|
|
|
|
|
derniere = cle
|
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
|
|
|
out.append(f" {r['sens']} {r['action']} -source {r['source']} "
|
|
|
|
|
f"-p {r['proto']} -dport {r['dport']}")
|
|
|
|
|
out += ["", f"### Affectation aux VM — {len(b['affectations'])}"]
|
|
|
|
|
for a in b["affectations"]:
|
|
|
|
|
out.append(f" vmid {a['vmid'] or '(non materialise)'} {a['hote']:<18} "
|
|
|
|
|
f"-> {', '.join(a['groupes'])}")
|
|
|
|
|
if b["sans_source"]:
|
|
|
|
|
out += ["", "# /!\\ Flux ingress sans source resolue dans ce tenant (aucune regle emise) :"]
|
|
|
|
|
out += [f"# {x}" for x in b["sans_source"]]
|
|
|
|
|
out.append("")
|
|
|
|
|
out += [
|
|
|
|
|
"## Defaut",
|
|
|
|
|
"# Politique d'entree DROP au datacenter : ce qui n'est pas ci-dessus",
|
|
|
|
|
"# n'entre pas. Un flux oublie se declare dans `roles/<role>/meta/flux.yml`,",
|
|
|
|
|
"# jamais a la main dans Proxmox — la regle serait perdue a la generation.",
|
|
|
|
|
]
|
|
|
|
|
return "\n".join(out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
|
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
|
|
|
ap.add_argument("--json", action="store_true")
|
|
|
|
|
ap.add_argument("--verifier", action="store_true")
|
|
|
|
|
args = ap.parse_args(argv)
|
|
|
|
|
|
|
|
|
|
devis = construire(decouvrir())
|
|
|
|
|
if args.verifier:
|
|
|
|
|
total_g = sum(len(b["groupes"]) for b in devis["blocs"])
|
|
|
|
|
total_r = sum(len(g["regles"]) for b in devis["blocs"] for g in b["groupes"])
|
|
|
|
|
if not total_g:
|
|
|
|
|
print("erreur: aucun groupe de securite derive — le registre ou l'inventaire "
|
|
|
|
|
"est vide.", file=sys.stderr)
|
|
|
|
|
return 2
|
|
|
|
|
print(f"CONFORME : pare-feu Proxmox, {len(devis['blocs'])} tenant(s), "
|
|
|
|
|
f"{total_g} groupe(s), {total_r} regle(s).")
|
|
|
|
|
return 0
|
|
|
|
|
print(json.dumps(devis, indent=2, ensure_ascii=False) if args.json else rendre(devis))
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main(sys.argv[1:]))
|