pare-feu Proxmox : des IPSets partout où c'est possible, et rien d'inutile

Six règles par tenant portaient quatorze adresses en dur : les mots-clés
`flotte` et `edge` n'avaient pas droit à un IPSet, seuls les rôles en
avaient. `flotte` en reçoit un, `edge` renvoie à celui de nginx. 36 des 40
règles se lisent maintenant `-source +t17-…`.

Et le devis listait 28 à 30 IPSets par tenant dont la moitié n'était
référencée nulle part : un opérateur en aurait créé 58 pour n'en utiliser
qu'une douzaine. Seuls les IPSets référencés sont émis — 6 par tenant. Un
devis crée ce qu'il liste.

Restent quatre règles en liste explicite, celles dont la source est plusieurs
rôles à la fois. Aucun IPSet unique ne les couvre et Proxmox n'accepte qu'une
référence par règle ; les éclater gonflerait le devis pour un gain
discutable.

Preuves : 25 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-03 11:53:38 -04:00
parent ece250ae49
commit dbb4465602
2 changed files with 36 additions and 5 deletions

View file

@ -36,6 +36,21 @@ chacun respecte sa contrainte, et le préfixe reste commun.
Vérifié : aucune collision d'IPSet, et **tout renvoi `+X` d'une règle pointe vers un IPSet qui
existe** — 58 IPSets, 34 groupes, aucun orphelin.
### Corrigé — des listes d'adresses en dur, et 48 IPSets inutilisés
Six règles par tenant portaient **quatorze adresses en dur** : les mots-clés `flotte` et
`edge` n'avaient pas droit à un IPSet, seuls les rôles en avaient. `flotte` en reçoit un
désormais, et `edge` renvoie à celui de nginx. **36 des 40 règles** se lisent maintenant
`-source +t17-…`.
Et le devis listait **28 à 30 IPSets par tenant dont la moitié n'était référencée nulle
part** : un opérateur en aurait créé 58 pour n'en utiliser qu'une douzaine. Seuls les IPSets
réellement référencés sont émis — **6 par tenant**. Un devis crée ce qu'il liste.
Restent quatre règles en liste explicite : celles dont la source est **plusieurs rôles à la
fois** (LDAPS ouvert à Keycloak, Dovecot, Icinga Web et Postfix). Aucun IPSet unique ne les
couvre et Proxmox n'accepte qu'une référence par règle ; les éclater gonflerait le devis pour
un gain discutable.
### Corrigé — l'affectation variait selon l'état du tenant
Elle partait de `hotes_actifs`, avec un repli sur « tous » quand il n'y en avait aucun. Deux
tenants donnaient donc deux comportements : Technolibre listait ses 14 VM (zéro actif → repli),

View file

@ -33,6 +33,7 @@ RACINE = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(RACINE / "scripts"))
from resoudre_flux import ( # noqa: E402
GROUPE_EDGE,
charger_flux,
est_groupe_operationnel,
_enfants,
@ -111,7 +112,13 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
ips = _ip_par_hote(data)
# Un IPSet par role porte dans ce tenant : les membres sont ses hotes.
# `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.
ipsets: dict[str, dict] = {}
if ips:
ipsets[f"t{n['index']}-flotte"] = {
"role": "flotte (tous les hotes du tenant)", "membres": sorted(set(ips.values())),
}
for groupe in sorted(_enfants(data)):
if not est_groupe_operationnel(groupe):
continue
@ -123,6 +130,7 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
groupes: list[dict] = []
sans_source: list[str] = []
utilises: set[str] = set()
for groupe in sorted(_enfants(data)):
if not est_groupe_operationnel(groupe):
continue
@ -141,10 +149,17 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
if not srcs:
sans_source.append(f"{groupe}:{','.join(_ports(fl))}")
continue
# Source par IPSet quand le pair est UN role : une seule liste a tenir.
source = (f"+{nom_ipset(pairs[0], n['index'])}"
if len(pairs) == 1 and nom_ipset(pairs[0], n["index"]) in ipsets
else ",".join(srcs))
# Source par IPSet des que possible : une seule liste a tenir, et une
# regle qui se lit. Les mots-cles `flotte` et `edge` y ont droit comme
# les roles — `edge` designe le role nginx.
source = ",".join(srcs)
if len(pairs) == 1:
cle = {"flotte": f"t{n['index']}-flotte",
"edge": nom_ipset(GROUPE_EDGE, n["index"])}.get(
pairs[0], nom_ipset(pairs[0], n["index"]))
if cle in ipsets:
source = f"+{cle}"
utilises.add(cle)
for port in _ports(fl):
regles.append({
"action": "ACCEPT", "sens": "IN",
@ -179,7 +194,8 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
blocs.append({
"tenant": nom, "etiquette": etiquette,
"ipsets": ipsets, "groupes": groupes,
"ipsets": {k: v for k, v in ipsets.items() if k in utilises},
"groupes": groupes,
"affectations": affect, "sans_source": sorted(set(sans_source)),
"admin": admin_de(nom),
})