#!/usr/bin/env python3 """Le pare-feu Proxmox laisse ARRIVER ce que la frontiere laisse entrer — et l'edge reste ferme a l'Internet. POURQUOI (2026-09-29). Le devis est-ouest sautait tout flux `externe`, « la frontiere s'en charge ». Mais depuis que les VM rejettent par defaut, un service publie (Postfix 25, Dovecot 993) aurait ete refuse par sa propre VM, et le poste ne joignait pas Dovecot. A l'inverse, l'edge declarait 80/443 `externe` : la frontiere posait « WAN, n'importe qui -> edge ». Or l'edge n'expose les interfaces qu'a la zone d'administration ; le public passera par le web frontal. Invariants gardes, sur le devis rendu depuis les plans montes : 1. `serveur_nginx` ne declare aucun flux `externe`, et aucune regle d'edge ne vient de `+t-internet` ; 2. tout flux entrant `externe` (hors SSH, reserve a l'administration) d'un role porte donne une regle depuis `+t-internet` ; 3. l'IPSet « internet » exclut les trois blocs RFC 1918, et aucune regle n'a de source vide ; 4. `appliquer_proxmox_fw` pose une exclusion en `nomatch`. """ from __future__ import annotations import json import subprocess import sys from pathlib import Path import yaml RACINE = Path(__file__).resolve().parents[2] sys.path.insert(0, str(RACINE / "scripts")) def flux(role: str) -> list[dict]: f = RACINE / "roles" / role / "meta" / "flux.yml" return (yaml.safe_load(f.read_text(encoding="utf-8")) or {}).get("flux") or [] if f.exists() else [] def pairs(fl: dict) -> list[str]: p = fl.get("pair") return [str(x) for x in (p if isinstance(p, list) else [p])] def main() -> int: echecs: list[str] = [] if any("externe" in pairs(fl) for fl in flux("serveur_nginx")): echecs.append("serveur_nginx declare un flux `externe` : l'edge serait ouvert a l'Internet") r = subprocess.run([sys.executable, str(RACINE / "scripts/devis_proxmox_fw.py"), "--json"], capture_output=True, text=True, cwd=RACINE) try: devis = json.loads(r.stdout) except ValueError: print(f"SAUTE devis indisponible ici (pas de plans montes ?) : {r.stderr.strip()[:120]}") devis = [] blocs = devis if isinstance(devis, list) else devis.get("blocs", []) for b in blocs: idx = next((k.split("-")[0] for k in b.get("ipsets", {}) if k.endswith("-internet")), None) roles = {g["role"]: g.get("regles", []) for g in b.get("groupes", [])} for role, regles in roles.items(): for rg in regles: if not str(rg.get("source", "")).strip(): echecs.append(f"{b['tenant']} {role} : regle sans source ({rg})") if role == "serveur_nginx" and "-internet" in str(rg.get("source")): echecs.append(f"{b['tenant']} : l'edge recoit l'Internet ({rg})") for fl in flux(role): if fl.get("sens") != "ingress" or "externe" not in pairs(fl): continue if str(fl.get("protocole", "tcp")).lower() != "icmp" and "22" in str(fl.get("port")): continue if not any("-internet" in str(rg.get("source")) for rg in regles): echecs.append(f"{b['tenant']} {role} : flux `externe` {fl.get('port')} sans regle " f"depuis l'Internet — la VM rejetterait ce que la frontiere laisse entrer") if idx: membres = b["ipsets"][f"{idx}-internet"]["membres"] for prive in ("!10.0.0.0/8", "!172.16.0.0/12", "!192.168.0.0/16"): if prive not in membres: echecs.append(f"{b['tenant']} : l'IPSet internet n'exclut pas {prive[1:]}") from appliquer_proxmox_fw import _membre if _membre("!10.0.0.0/8") != {"cidr": "10.0.0.0/8", "nomatch": 1} or _membre("1.2.3.0/24") != {"cidr": "1.2.3.0/24"}: echecs.append("appliquer_proxmox_fw._membre ne rend pas `!cidr` en exclusion `nomatch`") for e in echecs: print(f"ECHEC {e}") if echecs: return 1 print(f"OK — edge ferme a l'Internet ; flux `externe` recus depuis +t-internet " f"({len(blocs)} tenant(s)) ; exclusions RFC 1918 en `nomatch`.") return 0 if __name__ == "__main__": sys.exit(main())