diff --git a/CHANGELOG.md b/CHANGELOG.md index 07b8467..c00e766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,10 +46,17 @@ Et le devis listait **28 à 30 IPSets par tenant dont la moitié n'était réfé 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. +### Puis : une règle par rôle source +Les quatre règles restées en liste explicite sont éclatées — un flux dont le pair nomme quatre +rôles donne quatre règles, chacune renvoyant à l'IPSet de son rôle. **Plus une seule adresse +en dur : 52 règles, toutes par IPSet.** + +Le gain n'est pas cosmétique : une règle porte désormais **qui** elle autorise. `-source ++t17-srv-keycloak` se lit ; `-source 10.27.16.21,10.27.17.11,10.27.19.31,10.27.20.21` demande +de retrouver à qui appartient chaque adresse. + +La raison, elle, appartient au **flux** et non à chacune de ses règles : elle est écrite une +fois au-dessus du paquet qu'elle explique, au lieu d'être répétée quatre fois. ### 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 diff --git a/docs/audit/preuve-2026-08-03.md b/docs/audit/preuve-2026-08-03.md index 47b22ff..3036912 100644 --- a/docs/audit/preuve-2026-08-03.md +++ b/docs/audit/preuve-2026-08-03.md @@ -37,7 +37,7 @@ | P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (19 sections). | | P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 5 reseau(x), aucune collision avec la plage tenant. | | P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 26 regles, 2 routes, admin=192.168.254.2/32,192.168.255.0/24,192.168.255.2/32. | -| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 34 groupe(s), 40 regle(s). | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 34 groupe(s), 52 regle(s). | ## Couverture des affirmations ✅ du registre diff --git a/scripts/devis_proxmox_fw.py b/scripts/devis_proxmox_fw.py index 1f621d1..4d060d0 100644 --- a/scripts/devis_proxmox_fw.py +++ b/scripts/devis_proxmox_fw.py @@ -145,29 +145,35 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: continue # la frontiere s'en charge if pairs == ["localhost"]: continue # boucle locale : aucune regle inter-noeud - srcs = _resoudre_sources(data, fl.get("pair"), tous, ips) - if not srcs: + if not _resoudre_sources(data, fl.get("pair"), tous, ips): sans_source.append(f"{groupe}:{','.join(_ports(fl))}") continue - # 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: + # 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 cle = {"flotte": f"t{n['index']}-flotte", "edge": nom_ipset(GROUPE_EDGE, n["index"])}.get( - pairs[0], nom_ipset(pairs[0], n["index"])) + 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 if cle in ipsets: - source = f"+{cle}" + source, via = f"+{cle}", pair utilises.add(cle) - for port in _ports(fl): - regles.append({ - "action": "ACCEPT", "sens": "IN", - "source": source, - "proto": fl.get("protocole", "tcp"), - "dport": port, - "raison": fl.get("raison", ""), - }) + 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", ""), + }) if regles: groupes.append({ "nom": nom_groupe(groupe, n["index"]), "role": groupe, "regles": regles, @@ -230,11 +236,17 @@ def rendre(devis: dict) -> str: 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']}") + # 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 for r in g["regles"]: + cle = (r["dport"], r["proto"], r["raison"]) + if cle != derniere and r["raison"]: + out.append(f" # {r['raison']}") + derniere = cle out.append(f" {r['sens']} {r['action']} -source {r['source']} " f"-p {r['proto']} -dport {r['dport']}") - if r["raison"]: - out.append(f" # {r['raison']}") 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} "