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>
93 lines
4.2 KiB
Python
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())
|