Set-OPS-Public/scripts/tests/test_proxmox_fw_externe.py
Daniel Allaire 1493059271 edge ferme a l Internet ; le pare-feu Proxmox recoit les flux externe
L edge n expose qu a sa zone d administration (plus de pair externe) ; le public passera
par le web frontal. Le devis Proxmox recoit les flux externe depuis +t<i>-internet (RFC 1918
exclus en nomatch) et depuis l admin quand le poste en est client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:58:01 -04:00

93 lines
4.2 KiB
Python

#!/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<i>-internet` ;
2. tout flux entrant `externe` (hors SSH, reserve a l'administration) d'un role porte
donne une regle depuis `+t<i>-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<i>-internet "
f"({len(blocs)} tenant(s)) ; exclusions RFC 1918 en `nomatch`.")
return 0
if __name__ == "__main__":
sys.exit(main())