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>
This commit is contained in:
parent
1bd211d62b
commit
1493059271
9 changed files with 184 additions and 24 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -1,5 +1,35 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-09-29 (33) — L'edge fermé à l'Internet ; le pare-feu Proxmox reçoit ce que la frontière laisse entrer
|
||||||
|
|
||||||
|
**La règle, mise au point par l'exploitant** : les URL `*.internal` des edges ne s'ouvrent
|
||||||
|
qu'à la zone d'administration de leur écosystème ; ce qui sera publié sur l'Internet le sera
|
||||||
|
par le **web frontal**, pas par l'edge.
|
||||||
|
|
||||||
|
**L'edge déclarait pourtant 80/443 `externe`.** Conséquences, dérivées du même registre :
|
||||||
|
la frontière portait « WAN, n'importe qui → edge 80/443 » chez les deux locataires, et le
|
||||||
|
pare-feu de la machine (`nftables`) de chaque edge — site compris — acceptait 80 et 443 de
|
||||||
|
toute source. Seul le pare-feu Proxmox, qui sautait tout `externe`, fermait la porte.
|
||||||
|
Désormais `serveur_nginx` ne déclare plus d'`externe` : 443 depuis `flotte` et `admin`, 80
|
||||||
|
(redirection) depuis `admin`. Vérifié avant : le poste joint les edges par le réseau de
|
||||||
|
gestion (10.37.0.17, règle `admin`), et les journaux d'accès des trois edges ne montrent
|
||||||
|
aucun client hors flotte et administration.
|
||||||
|
|
||||||
|
**Le pare-feu Proxmox et les flux `externe`.** Il les sautait tous (« la frontière s'en
|
||||||
|
charge ») ; depuis que les VM rejettent par défaut, un service publié aurait été refusé par
|
||||||
|
sa propre VM — et le poste ne joignait pas Dovecot 993, que la frontière lui ouvre. Même
|
||||||
|
partage que la frontière : un flux entrant `externe` est reçu depuis `+t<i>-internet`
|
||||||
|
(`0.0.0.0/1`, `128.0.0.0/1`, les trois blocs RFC 1918 exclus en `nomatch` — jamais une source
|
||||||
|
vide, et un IPSet refuse `/0`), et depuis `+t<i>-admin` quand le poste en est client
|
||||||
|
(`poste`, vrai par défaut ; faux pour le 25 de Postfix). Le SSH `externe` reste réservé à
|
||||||
|
l'administration. Rendu : Postfix 25 (Internet), Dovecot 993 (Internet, admin), ICMP
|
||||||
|
« fragmentation nécessaire » (découverte de MTU). `appliquer_proxmox_fw` pose et relit les
|
||||||
|
exclusions. `test_proxmox_fw_externe.py` garde les invariants : edge jamais ouvert à
|
||||||
|
l'Internet, tout `externe` reçu, aucune source vide, exclusions en `nomatch`.
|
||||||
|
|
||||||
|
**Le web frontal** ne déclare encore que le 80 depuis l'edge : le rendre public (son propre
|
||||||
|
TLS, sa redirection à la frontière) est l'étape suivante, pas celle-ci.
|
||||||
|
|
||||||
## 2026-09-29 (32) — L'edge expose aux personnes ; entre machines du site, on va au service
|
## 2026-09-29 (32) — L'edge expose aux personnes ; entre machines du site, on va au service
|
||||||
|
|
||||||
**La règle, énoncée par l'exploitant** : l'edge sert à exposer des interfaces web ; les
|
**La règle, énoncée par l'exploitant** : l'edge sert à exposer des interfaces web ; les
|
||||||
|
|
|
||||||
1
Makefile
1
Makefile
|
|
@ -319,6 +319,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
||||||
python3 scripts/tests/test_sondes_syntaxe.py
|
python3 scripts/tests/test_sondes_syntaxe.py
|
||||||
python3 scripts/tests/test_empreinte_plancher.py
|
python3 scripts/tests/test_empreinte_plancher.py
|
||||||
python3 scripts/tests/test_expositions_sans_port.py
|
python3 scripts/tests/test_expositions_sans_port.py
|
||||||
|
python3 scripts/tests/test_proxmox_fw_externe.py
|
||||||
|
|
||||||
# LA SONDE QUI RAPPORTE CE QUI DISTINGUE (2026-08-28). Trois faux diagnostics en une
|
# LA SONDE QUI RAPPORTE CE QUI DISTINGUE (2026-08-28). Trois faux diagnostics en une
|
||||||
# journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine
|
# journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,7 @@
|
||||||
| `serveur_nextcloud` | egress | 443 | tcp | edge | tls-requis | Découverte OIDC auprès de Keycloak (via son FQDN publié à l'edge). |
|
| `serveur_nextcloud` | egress | 443 | tcp | edge | tls-requis | Découverte OIDC auprès de Keycloak (via son FQDN publié à l'edge). |
|
||||||
| `serveur_nextcloud` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base de données Nextcloud (verify-full). |
|
| `serveur_nextcloud` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base de données Nextcloud (verify-full). |
|
||||||
| `serveur_nextcloud` | egress | 9980 | tcp | serveur_collabora | tls-cible | Vérifications WOPI serveur->Collabora (édition en ligne). TLS interne = feuille de route edge->backends. |
|
| `serveur_nextcloud` | egress | 9980 | tcp | serveur_collabora | tls-cible | Vérifications WOPI serveur->Collabora (édition en ligne). TLS interne = feuille de route edge->backends. |
|
||||||
| `serveur_nginx` | ingress | 80 | tcp | externe | clair | HTTP entrant — redirection permanente vers HTTPS. |
|
| `serveur_nginx` | ingress | 80 | tcp | admin | clair | HTTP depuis le reseau d'administration — redirection permanente vers HTTPS. |
|
||||||
| `serveur_nginx` | ingress | 443 | tcp | externe | tls-requis | HTTPS entrant — services exposés (terminaison TLS à l'edge). |
|
|
||||||
| `serveur_nginx` | ingress | 443 | tcp | flotte | tls-requis | HTTPS depuis le tenant : les FQDN publiés vivent à l'edge (découverte OIDC, appels inter-services par nom). |
|
| `serveur_nginx` | ingress | 443 | tcp | flotte | tls-requis | HTTPS depuis le tenant : les FQDN publiés vivent à l'edge (découverte OIDC, appels inter-services par nom). |
|
||||||
| `serveur_nginx` | ingress | 443 | tcp | admin | tls-requis | HTTPS depuis le reseau d'administration : l'exploitant administre les services par leur interface web, servie par l'edge. |
|
| `serveur_nginx` | ingress | 443 | tcp | admin | tls-requis | HTTPS depuis le reseau d'administration : l'exploitant administre les services par leur interface web, servie par l'edge. |
|
||||||
| `serveur_nginx` | egress | derive | tcp | expositions | tls-cible | Proxy vers les backends exposés (host:port dérivés des expose ; TLS interne = roadmap edge→backends). |
|
| `serveur_nginx` | egress | derive | tcp | expositions | tls-cible | Proxy vers les backends exposés (host:port dérivés des expose ; TLS interne = roadmap edge→backends). |
|
||||||
|
|
@ -135,4 +134,4 @@
|
||||||
- **starttls** : 6 flux
|
- **starttls** : 6 flux
|
||||||
- **tls** : 11 flux
|
- **tls** : 11 flux
|
||||||
- **tls-cible** : 2 flux
|
- **tls-cible** : 2 flux
|
||||||
- **tls-requis** : 35 flux
|
- **tls-requis** : 34 flux
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,17 @@
|
||||||
# Cas « edge » : ingress depuis l'extérieur (frontière publique) + egress vers les backends,
|
# Cas « edge » : ingress depuis l'extérieur (frontière publique) + egress vers les backends,
|
||||||
# ces derniers dérivés des `expose:` des applications (pair: expositions).
|
# ces derniers dérivés des `expose:` des applications (pair: expositions).
|
||||||
flux:
|
flux:
|
||||||
|
# PAS D'`externe` (2026-09-29). L'edge expose les interfaces web de l'ecosysteme a SA
|
||||||
|
# zone d'administration, et a personne d'autre ; ce qui sera publie sur l'Internet le sera
|
||||||
|
# par le WEB FRONTAL. Les deux entrees `externe` qui figuraient ici posaient a la frontiere
|
||||||
|
# « WAN, n'importe qui -> edge 80/443 » chez chaque locataire — une ouverture que rien ne
|
||||||
|
# devait porter (le pare-feu Proxmox de l'edge la rejetait deja).
|
||||||
- sens: ingress
|
- sens: ingress
|
||||||
port: 80
|
port: 80
|
||||||
protocole: tcp
|
protocole: tcp
|
||||||
pair: externe
|
pair: admin
|
||||||
chiffrement: clair
|
chiffrement: clair
|
||||||
raison: "HTTP entrant — redirection permanente vers HTTPS."
|
raison: "HTTP depuis le reseau d'administration — redirection permanente vers HTTPS."
|
||||||
- sens: ingress
|
|
||||||
port: 443
|
|
||||||
protocole: tcp
|
|
||||||
pair: externe
|
|
||||||
chiffrement: tls-requis
|
|
||||||
raison: "HTTPS entrant — services exposés (terminaison TLS à l'edge)."
|
|
||||||
# L'edge sert AUSSI les hôtes du tenant : les FQDN publiés (`expose`) n'existent qu'ici,
|
# L'edge sert AUSSI les hôtes du tenant : les FQDN publiés (`expose`) n'existent qu'ici,
|
||||||
# et un service interne qui doit joindre un autre service passe par son nom publié — pas
|
# et un service interne qui doit joindre un autre service passe par son nom publié — pas
|
||||||
# par une adresse. C'est le cas de toute découverte OIDC : oauth2-proxy interroge
|
# par une adresse. C'est le cas de toute découverte OIDC : oauth2-proxy interroge
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,11 @@ def _norm(cidr: str) -> str:
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def _membre(m: str) -> dict:
|
||||||
|
"""Corps de l'API pour un membre d'IPSet : `!cidr` devient une exclusion (`nomatch`)."""
|
||||||
|
return {"cidr": m[1:], "nomatch": 1} if m.startswith("!") else {"cidr": m}
|
||||||
|
|
||||||
|
|
||||||
def _regle_cle(r: dict) -> tuple:
|
def _regle_cle(r: dict) -> tuple:
|
||||||
"""Identite d'une regle de groupe, insensible a la position et au commentaire."""
|
"""Identite d'une regle de groupe, insensible a la position et au commentaire."""
|
||||||
return (str(r.get("type") or r.get("sens") or "").lower(),
|
return (str(r.get("type") or r.get("sens") or "").lower(),
|
||||||
|
|
@ -130,7 +135,10 @@ def plan(api: Cluster, devis: dict) -> dict:
|
||||||
ipsets_poses = {}
|
ipsets_poses = {}
|
||||||
for s in (_lire(api, "/cluster/firewall/ipset") or []):
|
for s in (_lire(api, "/cluster/firewall/ipset") or []):
|
||||||
if isinstance(s, dict) and str(s.get("name", "")).startswith(prefixes):
|
if isinstance(s, dict) and str(s.get("name", "")).startswith(prefixes):
|
||||||
membres = [str(m.get("cidr")) for m in (api(f"/cluster/firewall/ipset/{s['name']}") or [])
|
# `nomatch` : une EXCLUSION (l'IPSet « internet » exclut les blocs prives). On la
|
||||||
|
# lit comme on l'ecrit, prefixee de `!` — sinon elle se lirait comme une inclusion.
|
||||||
|
membres = [("!" if m.get("nomatch") else "") + str(m.get("cidr"))
|
||||||
|
for m in (api(f"/cluster/firewall/ipset/{s['name']}") or [])
|
||||||
if isinstance(m, dict)]
|
if isinstance(m, dict)]
|
||||||
ipsets_poses[s["name"]] = sorted({_norm(m) for m in membres})
|
ipsets_poses[s["name"]] = sorted({_norm(m) for m in membres})
|
||||||
|
|
||||||
|
|
@ -243,12 +251,12 @@ def appliquer(api: Cluster, p: dict) -> int:
|
||||||
if _fait(api("/cluster/firewall/ipset", "POST",
|
if _fait(api("/cluster/firewall/ipset", "POST",
|
||||||
{"name": n, "comment": v["role"][:100]}), f"ipset {n}"):
|
{"name": n, "comment": v["role"][:100]}), f"ipset {n}"):
|
||||||
for m in v["membres"]:
|
for m in v["membres"]:
|
||||||
_fait(api(f"/cluster/firewall/ipset/{n}", "POST", {"cidr": m}), f"{n} += {m}")
|
_fait(api(f"/cluster/firewall/ipset/{n}", "POST", _membre(m)), f"{n} += {m}")
|
||||||
for n, (avant, apres) in sorted(p["ipsets_majer"].items()):
|
for n, (avant, apres) in sorted(p["ipsets_majer"].items()):
|
||||||
for m in sorted(set(apres) - set(avant)):
|
for m in sorted(set(apres) - set(avant)):
|
||||||
_fait(api(f"/cluster/firewall/ipset/{n}", "POST", {"cidr": m}), f"{n} += {m}")
|
_fait(api(f"/cluster/firewall/ipset/{n}", "POST", _membre(m)), f"{n} += {m}")
|
||||||
for m in sorted(set(avant) - set(apres)):
|
for m in sorted(set(avant) - set(apres)):
|
||||||
_fait(api(f"/cluster/firewall/ipset/{n}/{m}", "DELETE"), f"{n} -= {m}")
|
_fait(api(f"/cluster/firewall/ipset/{n}/{m.lstrip('!')}", "DELETE"), f"{n} -= {m}")
|
||||||
|
|
||||||
# 2. Groupes. Une mise a jour REMPLACE les regles : elles sont a nous, et les
|
# 2. Groupes. Une mise a jour REMPLACE les regles : elles sont a nous, et les
|
||||||
# comparer une a une pour un edit partiel couterait plus que de les reposer.
|
# comparer une a une pour un edit partiel couterait plus que de les reposer.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ PARTAGE DES ROLES (cf. docs/sdn-evpn.md) :
|
||||||
profondeur — une VM compromise franchit deux barrieres) ;
|
profondeur — une VM compromise franchit deux barrieres) ;
|
||||||
- `make devis-opnsense` : tout ce qui ENTRE ou SORT d'un tenant.
|
- `make devis-opnsense` : tout ce qui ENTRE ou SORT d'un tenant.
|
||||||
|
|
||||||
Les flux `pair: externe` sont donc SAUTES ici : ils relevent de la frontiere.
|
Les flux `pair: externe` : la frontiere decide QUI entre (redirection, filtre WAN) ; ce
|
||||||
|
devis laisse ARRIVER ce qu'elle a laisse passer. Sans lui, un service publie serait rejete
|
||||||
|
par sa propre VM (`policy_in: REJECT`). Voir `_EXTERNE` plus bas.
|
||||||
|
|
||||||
NON destructif : ce script n'ecrit rien sur le cluster. Il produit un devis a relire,
|
NON destructif : ce script n'ecrit rien sur le cluster. Il produit un devis a relire,
|
||||||
puis (a venir) un JSON pour l'API Proxmox.
|
puis (a venir) un JSON pour l'API Proxmox.
|
||||||
|
|
@ -30,6 +32,9 @@ from pathlib import Path
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
RACINE = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
# L'Internet vu par un IPSet Proxmox : `!` = entree `nomatch` (exclusion).
|
||||||
|
INTERNET = ("0.0.0.0/1", "128.0.0.0/1", "!10.0.0.0/8", "!172.16.0.0/12", "!192.168.0.0/16")
|
||||||
sys.path.insert(0, str(RACINE / "scripts"))
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||||||
|
|
||||||
from resoudre_flux import ( # noqa: E402
|
from resoudre_flux import ( # noqa: E402
|
||||||
|
|
@ -158,6 +163,14 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
||||||
# Le reseau d'administration a son PROPRE IPSet. L'ajouter a `flotte` ouvrirait
|
# Le reseau d'administration a son PROPRE IPSet. L'ajouter a `flotte` ouvrirait
|
||||||
# aussi tous les autres flux qui s'y referent — LDAP, SQL, metriques — a un
|
# aussi tous les autres flux qui s'y referent — LDAP, SQL, metriques — a un
|
||||||
# reseau qui n'a rien a y faire. Un ensemble, un sens.
|
# reseau qui n'a rien a y faire. Un ensemble, un sens.
|
||||||
|
# L'INTERNET, SANS LES ESPACES PRIVES (2026-09-29). Source des flux `externe` : la
|
||||||
|
# meme definition que la frontiere (`!SETOPS_INTERNES`). Jamais une source vide —
|
||||||
|
# une regle sans source ouvre le port a tout le monde, voisins compris. Un IPSet
|
||||||
|
# Proxmox refuse `0.0.0.0/0` : deux moities, et les trois blocs RFC 1918 exclus.
|
||||||
|
ipsets[f"t{n['index']}-internet"] = {
|
||||||
|
"role": "Internet (tout sauf RFC 1918) — flux `externe`",
|
||||||
|
"membres": list(INTERNET),
|
||||||
|
}
|
||||||
if admin_avec_tunnel(nom):
|
if admin_avec_tunnel(nom):
|
||||||
ipsets[f"t{n['index']}-admin"] = {
|
ipsets[f"t{n['index']}-admin"] = {
|
||||||
"role": "reseaux d'administration (intrant nftables_admin_ssh)",
|
"role": "reseaux d'administration (intrant nftables_admin_ssh)",
|
||||||
|
|
@ -190,9 +203,28 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
||||||
# la moitie `flotte` — plus aucun hote joignable en SSH depuis
|
# la moitie `flotte` — plus aucun hote joignable en SSH depuis
|
||||||
# l'interieur, Ansible compris. Meme piege pour le SMTP interne de
|
# l'interieur, Ansible compris. Meme piege pour le SMTP interne de
|
||||||
# Postfix, declare `[externe, client_smtp]`.
|
# Postfix, declare `[externe, client_smtp]`.
|
||||||
|
# CE QUE LA FRONTIERE LAISSE ENTRER DOIT POUVOIR ARRIVER (2026-09-29). Meme
|
||||||
|
# partage que `devis_opnsense` : le SSH `externe` est celui de la gestion,
|
||||||
|
# reserve a l'administration (garde anti-lockout, plus bas) ; tout autre
|
||||||
|
# flux `externe` vient de l'Internet, et du poste s'il en est client
|
||||||
|
# (`poste`, vrai par defaut — le 25 de Postfix ne l'est pas).
|
||||||
|
if "externe" in pairs and not (
|
||||||
|
str(fl.get("protocole", "tcp")).lower() != "icmp" and "22" in _ports(fl)):
|
||||||
|
sources = [(f"t{n['index']}-internet", "internet")]
|
||||||
|
if fl.get("poste", True) and admin_avec_tunnel(nom):
|
||||||
|
sources.append((f"t{n['index']}-admin", "administration (poste)"))
|
||||||
|
for cle_src, via in sources:
|
||||||
|
utilises.add(cle_src)
|
||||||
|
for cible in _cibles(fl):
|
||||||
|
regles.append({
|
||||||
|
"action": "ACCEPT", "sens": "IN",
|
||||||
|
"source": f"+{cle_src}", "via": via,
|
||||||
|
**cible,
|
||||||
|
"raison": fl.get("raison", ""),
|
||||||
|
})
|
||||||
pairs = [p for p in pairs if p != "externe"]
|
pairs = [p for p in pairs if p != "externe"]
|
||||||
if not pairs:
|
if not pairs:
|
||||||
continue # purement externe : la frontiere s'en charge
|
continue
|
||||||
if pairs == ["localhost"]:
|
if pairs == ["localhost"]:
|
||||||
continue # boucle locale : aucune regle inter-noeud
|
continue # boucle locale : aucune regle inter-noeud
|
||||||
if not _resoudre_sources(data, fl.get("pair"), tous, ips):
|
if not _resoudre_sources(data, fl.get("pair"), tous, ips):
|
||||||
|
|
@ -324,7 +356,7 @@ def rendre(devis: dict) -> str:
|
||||||
"# barrieres disent la meme chose, et ne peuvent pas diverger.",
|
"# barrieres disent la meme chose, et ne peuvent pas diverger.",
|
||||||
"#",
|
"#",
|
||||||
"# Ce qui ENTRE ou SORT d'un tenant n'est PAS ici : c'est la frontiere",
|
"# Ce qui ENTRE ou SORT d'un tenant n'est PAS ici : c'est la frontiere",
|
||||||
"# (`make devis-opnsense`). Les flux `pair: externe` sont sautes.",
|
"# (`make devis-opnsense`). Les flux `pair: externe` arrivent depuis `+t<index>-internet`.",
|
||||||
"# ============================================================",
|
"# ============================================================",
|
||||||
"",
|
"",
|
||||||
"## 0. Prerequis — TROIS verrous, et le REJECT se pose PAR VM",
|
"## 0. Prerequis — TROIS verrous, et le REJECT se pose PAR VM",
|
||||||
|
|
|
||||||
93
scripts/tests/test_proxmox_fw_externe.py
Normal file
93
scripts/tests/test_proxmox_fw_externe.py
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
#!/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())
|
||||||
|
|
@ -9,9 +9,8 @@
|
||||||
graph LR
|
graph LR
|
||||||
R["<b>nginx</b>"]
|
R["<b>nginx</b>"]
|
||||||
E0["admin"] -->|"443 · tls-requis"| R
|
E0["admin"] -->|"443 · tls-requis"| R
|
||||||
E1["externe"] -->|"443 · tls-requis"| R
|
E1["admin"] -->|"80 · clair"| R
|
||||||
E2["externe"] -->|"80 · clair"| R
|
E2["flotte"] -->|"443 · tls-requis"| R
|
||||||
E3["flotte"] -->|"443 · tls-requis"| R
|
|
||||||
R -.->|"sonde « edge »"| ICINGA[["Icinga"]]
|
R -.->|"sonde « edge »"| ICINGA[["Icinga"]]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -19,8 +18,7 @@ graph LR
|
||||||
|
|
||||||
| port | depuis | chiffrement | pourquoi |
|
| port | depuis | chiffrement | pourquoi |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `80` | `externe` | clair | HTTP entrant — redirection permanente vers HTTPS. |
|
| `80` | `admin` | clair | HTTP depuis le reseau d'administration — redirection permanente vers HTTPS. |
|
||||||
| `443` | `externe` | tls-requis | HTTPS entrant — services exposés (terminaison TLS à l'edge). |
|
|
||||||
| `443` | `flotte` | tls-requis | HTTPS depuis le tenant : les FQDN publiés vivent à l'edge (découverte OIDC, appels inter-services par nom). |
|
| `443` | `flotte` | tls-requis | HTTPS depuis le tenant : les FQDN publiés vivent à l'edge (découverte OIDC, appels inter-services par nom). |
|
||||||
| `443` | `admin` | tls-requis | HTTPS depuis le reseau d'administration : l'exploitant administre les services par leur interface web, servie par l'edge. |
|
| `443` | `admin` | tls-requis | HTTPS depuis le reseau d'administration : l'exploitant administre les services par leur interface web, servie par l'edge. |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ Chaque fiche est construite depuis ce que le rôle **déclare lui-même** — `m
|
||||||
| [`serveur_keycloak`](Rôle-serveur_keycloak) | 1 | 1 | — |
|
| [`serveur_keycloak`](Rôle-serveur_keycloak) | 1 | 1 | — |
|
||||||
| [`serveur_loki`](Rôle-serveur_loki) | 4 | 2 | — |
|
| [`serveur_loki`](Rôle-serveur_loki) | 4 | 2 | — |
|
||||||
| [`serveur_nextcloud`](Rôle-serveur_nextcloud) | 1 | 1 | — |
|
| [`serveur_nextcloud`](Rôle-serveur_nextcloud) | 1 | 1 | — |
|
||||||
| [`serveur_nginx`](Rôle-serveur_nginx) | 4 | 1 | — |
|
| [`serveur_nginx`](Rôle-serveur_nginx) | 3 | 1 | — |
|
||||||
| [`serveur_oauth2_proxy`](Rôle-serveur_oauth2_proxy) | 1 | 1 | — |
|
| [`serveur_oauth2_proxy`](Rôle-serveur_oauth2_proxy) | 1 | 1 | — |
|
||||||
| [`serveur_openldap`](Rôle-serveur_openldap) | 2 | 1 | — |
|
| [`serveur_openldap`](Rôle-serveur_openldap) | 2 | 1 | — |
|
||||||
| [`serveur_ops`](Rôle-serveur_ops) | 1 | 2 | — |
|
| [`serveur_ops`](Rôle-serveur_ops) | 1 | 2 | — |
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue