proxmox-fw : le DROP se pose par VM, jamais au datacenter (D-64)
Le devis enseignait « politique d'entree DROP au datacenter ». Or policy_in y est la politique par defaut de TOUTE VM dont le pare-feu s'active — 37 machines heritees sans regles, sur ce cluster. `policy_in` existe aussi par VM : le devis et l'applicateur le posent la. Meme isolation, sans falaise, et l'applicateur n'a plus a refuser une partie de son devis. Trois verrous, pas un : datacenter enable=1, `enable` de la VM (defaut 0), et `firewall=1` sur la carte. C'est le verrou du milieu que j'avais manque en annoncant que huit VM en production tomberaient. `enable=1` au datacenter bascule et verifie : pve-firewall running, 12 chaines cadres, AUCUNE chaine par VM, 0 regle visant roxanne, 15 VM toujours en marche, hyperviseurs et frontiere joignables, sortie tenant 2/2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b3447d5a96
commit
d61c187dcf
4 changed files with 94 additions and 21 deletions
41
CHANGELOG.md
41
CHANGELOG.md
|
|
@ -2,6 +2,47 @@
|
|||
|
||||
## 2026-08-06 — le chemin nord-sud devient dérivable
|
||||
|
||||
### Le DROP se pose par VM, pas au datacenter (D-64)
|
||||
|
||||
Le devis enseignait un geste dangereux : « pare-feu activé, politique d'entrée DROP » **au
|
||||
niveau du datacenter**. Or `policy_in` y est la politique par défaut de **toute** VM dont le
|
||||
pare-feu s'active. Sur ce cluster, cela vise 37 machines héritées qui n'ont aucune règle.
|
||||
|
||||
`policy_in` existe aussi **par VM**. Le devis et l'applicateur le posent désormais là :
|
||||
|
||||
```
|
||||
datacenter enable=1, policy_in laissé au défaut ACCEPT
|
||||
VM tenant enable=1 + policy_in=DROP + carte firewall=1
|
||||
VM héritée rien — politique inchangée, pare-feu éteint
|
||||
```
|
||||
|
||||
Même isolation est-ouest, sans le moment où tout bascule. Et l'applicateur n'a plus besoin de
|
||||
refuser une partie de son devis : la partie dangereuse a disparu.
|
||||
|
||||
**Trois verrous, pas un.** Une VM n'est filtrée que si le datacenter est actif, que **son
|
||||
propre** `enable` vaut 1 — défaut **0**, c'est le verrou du milieu — et que sa carte porte
|
||||
`firewall=1`. C'est ce verrou du milieu que j'avais manqué en annonçant que huit VM en
|
||||
production tomberaient.
|
||||
|
||||
### `enable=1` au datacenter : mesuré, pas supposé
|
||||
|
||||
Basculé avec vérification immédiate. Après :
|
||||
|
||||
```
|
||||
pve-firewall enabled/running
|
||||
chaînes iptables 12, toutes des chaînes-cadres PVEFW-*
|
||||
chaînes par VM aucune
|
||||
règles visant roxanne 0
|
||||
15 VM en marche 15
|
||||
hyperviseurs, frontière joignables
|
||||
sortie tenant 2/2, 13 ms
|
||||
```
|
||||
|
||||
`enable=1` pose **le cadre** et rien d'autre. Ce que le schéma laissait prévoir est maintenant
|
||||
constaté sur la machine — la distinction compte, et c'est la seule raison d'avoir tenté le
|
||||
geste plutôt que de l'écrire.
|
||||
|
||||
|
||||
### Un applicateur pour le pare-feu est-ouest — et un refus assumé
|
||||
|
||||
`scripts/appliquer_proxmox_fw.py` réconcilie les trois couches du devis : 26 IPSets,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ sont les seules vérifiables.
|
|||
| **D-11** | La **sortie générale est déclarée** dans le registre des flux | `block out` est un vrai default-deny ; un besoin oublié ne se manifeste pas par un refus clair | `frontiere-opnsense.md` §8 | P09 |
|
||||
| **D-29** | L'**overlay EVPN plafonne à 1450** ; le transport doit donc dépasser 1500 | choix d'exploitation ; l'encapsulation VXLAN coûte 50 octets | `sdn-evpn.md` §5 | P23 (MTU du transport) |
|
||||
| **D-30** | L'**ICMP « fragmentation nécessaire » est déclaré**, dans les deux sens | à 1450, tout ce qui traverse la frontière dépend de la découverte de MTU de chemin ; une bordure en default-deny la casse en silence | `roles/serveur_debian/meta/flux.yml` | P09 |
|
||||
| **D-64** | La politique `DROP` du pare-feu Proxmox se pose **sur chaque VM tenant**, jamais au datacenter | au datacenter, `policy_in` est la politique par défaut de **toute** VM dont le pare-feu s'active — un parc hérité sans règles tomberait d'un bloc. Par VM : même isolation, sans falaise, et le reste du cluster garde sa politique | `devis_proxmox_fw.py` §0 | P25 |
|
||||
| **D-62** | La sortie d'un VRF tenant emprunte **un seul saut** (`nexthop-vrf default` vers la frontière), jamais la table principale | `import vrf default` ferait hériter au tenant le défaut de l'hyperviseur — sortie **contournant la frontière** — et lui donnerait au passage le transport VXLAN, la gestion et les VLAN hérités. Un saut emprunté n'est pas une table héritée | `devis_sdn.py` §4 | P30 |
|
||||
| **D-63** | Tout supernet **routé** doit être **traduit** : le NAT sortant est dérivé, pas laissé en « automatique » | le mode automatique ne couvre que les réseaux *directement attachés* ; un supernet joint par route statique en sort sans avertissement. Panne muette : le filtre passe, un état s'ouvre, rien ne revient | `devis_opnsense.py` §2bis | P24 |
|
||||
| **D-61** | L'**interface d'une règle se dérive de l'attachement réel** de sa source, jamais du sens du flux | une règle OPNsense est toujours `in` sur l'interface d'arrivée ; posée ailleurs elle ne peut **jamais** correspondre, et le devis affirme alors un accès qui n'existe pas | `frontiere-opnsense.md` §7 | P24 |
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ DEUX SENS : ce que le devis demande et qui manque est cree, ce qu'il ne demande
|
|||
plus est RETIRE. Un groupe orphelin ne bloque rien, mais il decrit une politique
|
||||
qui n'est plus la notre.
|
||||
|
||||
CE QU'IL NE FAIT JAMAIS : activer le pare-feu du datacenter, ni poser sa politique
|
||||
d'entree a DROP. Ce reglage vaut pour TOUTES les VM du cluster, y compris celles
|
||||
qui n'ont aucune regle — le basculer couperait le parc herite d'un coup. L'ecart
|
||||
est signale, la decision reste humaine.
|
||||
LE DROP EST PORTE PAR LA VM, jamais par le datacenter. Au datacenter, `policy_in`
|
||||
est la politique par defaut de TOUTE VM dont le pare-feu s'active — un parc herite
|
||||
sans regles tomberait d'un bloc. Pose sur chaque VM tenant, il donne la meme
|
||||
isolation sans cette falaise, et le script le fait lui-meme.
|
||||
|
||||
Il n'active pas le pare-feu du datacenter (`enable=1`) : c'est un reglage de
|
||||
cluster, decide une fois, et signale tant qu'il manque.
|
||||
|
||||
PERIMETRE STRICT : seuls les objets prefixes par l'etiquette d'un tenant du devis
|
||||
(`t11-`, `t17-`) sont touches. Le reste du cluster n'existe pas pour ce script.
|
||||
|
|
@ -93,11 +96,15 @@ def plan(api: Cluster, devis: dict) -> dict:
|
|||
vm_absentes.append(vmid)
|
||||
continue
|
||||
regles = api(f"/nodes/{noeud}/qemu/{vmid}/firewall/rules") or []
|
||||
o = api(f"/nodes/{noeud}/qemu/{vmid}/firewall/options") or {}
|
||||
affect_posees[vmid] = {
|
||||
"noeud": noeud,
|
||||
"groupes": sorted(str(r.get("action")) for r in regles
|
||||
if isinstance(r, dict) and r.get("type") == "group"),
|
||||
"actif": str((api(f"/nodes/{noeud}/qemu/{vmid}/firewall/options") or {}).get("enable", "")),
|
||||
"actif": str(o.get("enable", "")),
|
||||
# Le DROP vit ICI, sur la VM du tenant — pas au datacenter, ou il vaudrait
|
||||
# pour tout le parc, y compris ce qui n'appartient a aucun tenant.
|
||||
"politique": str(o.get("policy_in", "")).upper(),
|
||||
}
|
||||
|
||||
opts = api("/cluster/firewall/options") or {}
|
||||
|
|
@ -115,7 +122,8 @@ def plan(api: Cluster, devis: dict) -> dict:
|
|||
"affect_majer": {v: a for v, a in affect_voulues.items()
|
||||
if v in affect_posees
|
||||
and (affect_posees[v]["groupes"] != a["groupes"]
|
||||
or affect_posees[v]["actif"] != "1")},
|
||||
or affect_posees[v]["actif"] != "1"
|
||||
or affect_posees[v]["politique"] != "DROP")},
|
||||
"affect_posees": affect_posees,
|
||||
"vm_absentes": sorted(vm_absentes),
|
||||
"datacenter": {"actif": str(opts.get("enable", "")), "politique": str(opts.get("policy_in", ""))},
|
||||
|
|
@ -144,13 +152,19 @@ def afficher(p: dict) -> bool:
|
|||
print(f" {', '.join(str(v) for v in p['vm_absentes'][:8])}"
|
||||
+ (" …" if len(p["vm_absentes"]) > 8 else ""))
|
||||
dc = p["datacenter"]
|
||||
if dc["actif"] != "1" or dc["politique"].upper() != "DROP":
|
||||
print(f"\n /!\\ PREREQUIS NON REMPLI — pare-feu du datacenter : actif={dc['actif'] or '0'}, "
|
||||
f"politique d'entree={dc['politique'] or '(defaut: ACCEPT)'}.")
|
||||
print(" Sans `enable=1` ET `policy_in=DROP`, les regles ci-dessus s'ajoutent a un")
|
||||
print(" tout-permis et n'interdisent RIEN.")
|
||||
print(" Ce script ne le bascule PAS : le reglage vaut pour TOUTES les VM du")
|
||||
print(" cluster, y compris le parc herite qui n'a aucune regle. A decider a la main.")
|
||||
if dc["actif"] != "1":
|
||||
print(f"\n /!\\ PREREQUIS NON REMPLI — pare-feu du datacenter desactive "
|
||||
f"(enable={dc['actif'] or '0'}).")
|
||||
print(" Sans lui, RIEN ne filtre : les objets ci-dessus restent inertes.")
|
||||
print(" `enable=1` seul ne ferme rien — une VM n'est filtree que si SON propre")
|
||||
print(" `enable` vaut 1 (defaut 0) et que sa carte porte `firewall=1`.")
|
||||
print(" Ce script ne le bascule pas : c'est un reglage de cluster, decide une fois.")
|
||||
if dc["politique"].upper() in ("DROP", "REJECT"):
|
||||
print(f"\n /!\\ DANGER — `policy_in={dc['politique']}` AU DATACENTER.")
|
||||
print(" C'est la politique par defaut de TOUTE VM dont le pare-feu s'active,")
|
||||
print(" y compris ce qui n'appartient a aucun tenant. Set-OPS pose le DROP")
|
||||
print(" PAR VM tenant : cette valeur au datacenter n'est pas requise, et expose")
|
||||
print(" le reste du parc. La remettre a ACCEPT.")
|
||||
creer = len(p["ipsets_creer"]) + len(p["groupes_creer"])
|
||||
majer = len(p["ipsets_majer"]) + len(p["groupes_majer"]) + len(p["affect_majer"])
|
||||
retirer = len(p["ipsets_retirer"]) + len(p["groupes_retirer"])
|
||||
|
|
@ -209,8 +223,10 @@ def appliquer(api: Cluster, p: dict) -> int:
|
|||
for g in a["groupes"]:
|
||||
_fait(api(f"/nodes/{noeud}/qemu/{v}/firewall/rules", "POST",
|
||||
{"type": "group", "action": g, "enable": 1}), f"VM {v} += {g}")
|
||||
_fait(api(f"/nodes/{noeud}/qemu/{v}/firewall/options", "PUT", {"enable": 1}),
|
||||
f"pare-feu de la VM {v}")
|
||||
# `policy_in=DROP` sur LA VM : c'est ce qui rend les groupes ci-dessus exclusifs.
|
||||
# Au datacenter, la meme valeur s'appliquerait a tout ce qui n'est pas un tenant.
|
||||
_fait(api(f"/nodes/{noeud}/qemu/{v}/firewall/options", "PUT",
|
||||
{"enable": 1, "policy_in": "DROP"}), f"pare-feu de la VM {v}")
|
||||
|
||||
# 4. Retraits : groupes avant IPSets, un IPSet reference ne se supprime pas.
|
||||
for n in p["groupes_retirer"]:
|
||||
|
|
|
|||
|
|
@ -249,9 +249,21 @@ def rendre(devis: dict) -> str:
|
|||
"# (`make devis-opnsense`). Les flux `pair: externe` sont sautes.",
|
||||
"# ============================================================",
|
||||
"",
|
||||
"## 0. Prerequis, au niveau du datacenter",
|
||||
"# Pare-feu active, politique d'entree DROP. Sans cela, ces regles",
|
||||
"# s'ajoutent a un tout-permis et n'interdisent rien.",
|
||||
"## 0. Prerequis — TROIS verrous, et le DROP se pose PAR VM",
|
||||
"#",
|
||||
"# Une VM n'est filtree que si les trois sont fermes :",
|
||||
"# datacenter enable=1 (defaut 0 : rien ne filtre)",
|
||||
"# la VM enable=1 (defaut 0 — c'est le verrou du milieu)",
|
||||
"# sa carte firewall=1 (par interface)",
|
||||
"#",
|
||||
"# La politique DROP se pose SUR CHAQUE VM TENANT, jamais au datacenter :",
|
||||
"# pvesh set /cluster/sdn # sans toucher a policy_in",
|
||||
"# pvesh set /cluster/firewall/options --enable 1",
|
||||
"# pvesh set /nodes/<n>/qemu/<vmid>/firewall/options --enable 1 --policy_in DROP",
|
||||
"#",
|
||||
"# `policy_in DROP` AU DATACENTER serait la politique par defaut de TOUTE VM dont",
|
||||
"# le pare-feu deviendrait actif — y compris un parc herite sans aucune regle, qui",
|
||||
"# tomberait d'un bloc. Le poser par VM donne la meme isolation sans cette falaise.",
|
||||
"",
|
||||
]
|
||||
for b in devis["blocs"]:
|
||||
|
|
@ -290,9 +302,12 @@ def rendre(devis: dict) -> str:
|
|||
out.append("")
|
||||
out += [
|
||||
"## Defaut",
|
||||
"# Politique d'entree DROP au datacenter : ce qui n'est pas ci-dessus",
|
||||
"# n'entre pas. Un flux oublie se declare dans `roles/<role>/meta/flux.yml`,",
|
||||
"# jamais a la main dans Proxmox — la regle serait perdue a la generation.",
|
||||
"# `policy_in DROP` SUR CHAQUE VM TENANT : ce qui n'est pas ci-dessus n'entre pas.",
|
||||
"# Un flux oublie se declare dans `roles/<role>/meta/flux.yml`, jamais a la main",
|
||||
"# dans Proxmox — la regle serait perdue a la generation.",
|
||||
"#",
|
||||
"# Le DROP est porte par la VM et non par le datacenter : l'isolation est la meme,",
|
||||
"# et le parc qui n'appartient a aucun tenant garde sa politique inchangee.",
|
||||
]
|
||||
return "\n".join(out)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue