flux : l interne refuse a voix haute, la bordure se tait (P53)
Some checks are pending
verifier / verifier (push) Waiting to run
Some checks are pending
verifier / verifier (push) Waiting to run
Decision de l exploitant : block vers l Internet, reject a l interieur, parce que
c est prudent. Ce n est pas le refus qui informe, c est CE QU IL FAIT AU SILENCE :
sous drop partout, un timeout voulait dire aucune machine, aucune route, ou une
politique. Quand la politique parle, il n en reste qu une.
nftables par hote policy drop + reject with icmpx type admin-prohibited
pare-feu Proxmox policy_in = REJECT (POLITIQUE_VM, source unique)
frontiere OPNsense block — INCHANGE, et c est la condition
admin-prohibited ET NON tcp reset : un RST est indiscernable d un port ferme sans
service. La chaine forward reste muette : elle porte le trafic qui TRAVERSE l hote,
et y repondre ferait parler cette machine au nom d une destination qui n est pas
elle.
POURQUOI C EST PRUDENT : l obscurite etait deja nulle a l interieur (chaque machine
porte un /etc/hosts qui liste ses voisines), et la bordure protege le reject —
rien d indeclare ne franchit le perimetre, donc il ne repond jamais a l Internet.
982 000 entrees par jour a la frontiere, dont 82 % un balayage VNC.
LA MESURE A CORRIGE LA MESURE, DEUX FOIS.
Ma preuve interdisait le litteral DROP et a fait echouer un code JUSTE : la
detection d une politique posee AU DATACENTER, qui est un garde-fou. Une preuve qui
interdit un mot au lieu de mesurer une propriete finit par accuser ce qu elle
devrait proteger.
Et l absence parlait deja : EHOSTUNREACH en 3,05 s pour une machine inexistante,
timeout a 6 s pour un refus de la frontiere. Mes deux erreurs de diagnostic ne
venaient pas du drop mais de ma SONDE — curl et bash /dev/tcp ecrasent les deux
dans un meme echec.
Applique : 6 VM en REJECT, 0 creee, 0 retiree. Rien ne se ferme.
Trois controles negatifs verifies.
make verifier : vert. make prouver : CONFORME, 53 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
This commit is contained in:
parent
89c911ef05
commit
4beb7e0724
6 changed files with 229 additions and 16 deletions
70
CHANGELOG.md
70
CHANGELOG.md
|
|
@ -1,5 +1,75 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-28 — P53 : l'interne refuse à voix haute, la bordure se tait
|
||||
|
||||
**53 preuves.** Décision de l'exploitant : `block` vers l'Internet, `reject` à l'intérieur.
|
||||
*« Parce que c'est prudent. »*
|
||||
|
||||
Ce n'est pas le refus qui informe, c'est **ce qu'il fait au silence**. Sous `drop` partout,
|
||||
un timeout voulait dire trois choses incompatibles — aucune machine, aucune route, ou une
|
||||
politique. Quand la politique parle, il n'en reste qu'une.
|
||||
|
||||
```
|
||||
nftables par hôte policy drop + reject with icmpx type admin-prohibited
|
||||
pare-feu Proxmox policy_in = REJECT (POLITIQUE_VM, source unique)
|
||||
frontière OPNsense block — INCHANGÉ, et c'est la condition
|
||||
```
|
||||
|
||||
**`admin-prohibited` et non `tcp reset`** : un RST est indiscernable d'un port fermé sans
|
||||
service. Ce message-ci dit qu'une *politique* a refusé — la seule forme qui distingue « on
|
||||
ne veut pas de toi » de « il n'y a rien ».
|
||||
|
||||
**La chaîne `forward` reste muette**, et ce n'est pas un oubli : elle porte le trafic qui
|
||||
*traverse* l'hôte. Y répondre ferait parler cette machine **au nom d'une destination qui
|
||||
n'est pas elle** — le défaut qu'on corrige dans `input`, déplacé d'un cran.
|
||||
|
||||
### Pourquoi c'est prudent, et pas seulement commode
|
||||
|
||||
**L'obscurité était déjà nulle à l'intérieur.** Chaque machine porte un `/etc/hosts`
|
||||
généré qui liste toutes ses voisines avec leurs adresses. Se cacher de pairs qui ont déjà
|
||||
notre adresse ne protège de rien — le `drop` coûtait du diagnostic sans rien acheter.
|
||||
|
||||
**Et la bordure protège le reject.** Rien d'indéclaré ne franchit le périmètre : ce reject
|
||||
ne répond donc **jamais** à l'Internet. Mesure du 2026-08-27 à la frontière, qui explique
|
||||
l'autre moitié du choix : 982 000 entrées par jour, dont 82 % un balayage contre le port
|
||||
VNC. Y répondre serait un vecteur d'amplification, source usurpée comprise.
|
||||
|
||||
P53 garde les deux moitiés **ensemble**, parce que l'une sans l'autre est fausse : *une
|
||||
bordure bavarde s'expose, un interne muet ment.* Trois contrôles négatifs vérifiés.
|
||||
|
||||
### La mesure a corrigé la mesure — deux fois
|
||||
|
||||
**Ma preuve était trop grossière.** Elle interdisait le littéral `DROP` dans le pare-feu
|
||||
est-ouest, et a fait échouer un code **juste** :
|
||||
`dc["politique"].upper() in ("DROP", "REJECT")` détecte une politique posée *au
|
||||
datacenter*, où elle vaudrait pour tout le parc — un garde-fou, pas un réglage. *Une preuve
|
||||
qui interdit un mot au lieu de mesurer une propriété finit par accuser ce qu'elle devrait
|
||||
protéger.* Elle cherche désormais une **assignation en dur**, pas une occurrence.
|
||||
|
||||
**Et l'absence parlait déjà.** Mesuré depuis `ops-01` après application :
|
||||
|
||||
```
|
||||
cache du site, déclaré OUVERT 0,00 s
|
||||
machine INEXISTANTE OSError errno 113 EHOSTUNREACH 3,05 s
|
||||
autre tenant, frontière TimeoutError 6,01 s
|
||||
```
|
||||
|
||||
La passerelle anycast dit `EHOSTUNREACH` ; seule la frontière se tait. **Mes deux erreurs
|
||||
de diagnostic de la journée ne venaient donc pas du `drop`, mais de ma sonde** — `curl` et
|
||||
`bash /dev/tcp` écrasent `EHOSTUNREACH` et `timeout` dans un même « échec ». L'information
|
||||
était là ; l'instrument la jetait. *Encore une fois, vérifier d'où l'on mesure avant
|
||||
d'accuser ce qu'on mesure.*
|
||||
|
||||
Le `reject` garde toute sa valeur pour ce qu'aucun errno ne dit : la politique **d'hôte**
|
||||
et **est-ouest**. Sa démonstration attend deux machines vivantes dans un même tenant —
|
||||
Chezlepro n'en a qu'une.
|
||||
|
||||
**Appliqué** : 6 VM passées en `REJECT` (Chezlepro `ops-01`, les cinq de patient 0), 0
|
||||
créée, 0 retirée. Le changement ne ferme rien — `REJECT` et `DROP` refusent tous deux,
|
||||
l'un répond.
|
||||
|
||||
make verifier : vert. make prouver : CONFORME, 53 OK, 0 echec, 0 saute.
|
||||
|
||||
## 2026-08-28 — L'insémination aboutit : un runner de tenant, né d'un runner de site
|
||||
|
||||
**52 preuves.** `ops-01` existe, configurée de bout en bout **par le runner du SITE**, qui
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (52 OK · 0 echec · 0 saute)
|
||||
- **Verdict** : ✅ CONFORME (53 OK · 0 echec · 0 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
|
|
@ -65,6 +65,7 @@
|
|||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 125 regles `pass`), tous non consignes et tous motives. |
|
||||
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
||||
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 15 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ 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.
|
||||
|
||||
LE DROP EST PORTE PAR LA VM, jamais par le datacenter. Au datacenter, `policy_in`
|
||||
LE REFUS 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.
|
||||
|
|
@ -29,6 +29,16 @@ Usage :
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# LA POLITIQUE DE REFUS DE L'EST-OUEST — UNE SEULE SOURCE (2026-08-28).
|
||||
#
|
||||
# Elle etait ecrite DEUX FOIS : une a l'ecriture, une a la detection d'ecart. Deux
|
||||
# litteraux pour la meme decision, et le jour ou l'un change sans l'autre, le devis
|
||||
# propose une mise a jour eternelle qu'il vient pourtant d'appliquer.
|
||||
#
|
||||
# REJECT ET NON DROP : cette couche filtre l'interne. Voir la preuve P53 et
|
||||
# `docs/flux-conception.md` — l'interne parle, la bordure se tait.
|
||||
POLITIQUE_VM = "REJECT"
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
|
@ -117,7 +127,7 @@ def plan(api: Cluster, devis: dict) -> dict:
|
|||
"groupes": sorted(str(r.get("action")) for r in regles
|
||||
if isinstance(r, dict) and r.get("type") == "group"),
|
||||
"actif": str(o.get("enable", "")),
|
||||
# Le DROP vit ICI, sur la VM du tenant — pas au datacenter, ou il vaudrait
|
||||
# Le refus 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(),
|
||||
}
|
||||
|
|
@ -138,7 +148,7 @@ def plan(api: Cluster, devis: dict) -> dict:
|
|||
if v in affect_posees
|
||||
and (affect_posees[v]["groupes"] != a["groupes"]
|
||||
or affect_posees[v]["actif"] != "1"
|
||||
or affect_posees[v]["politique"] != "DROP")},
|
||||
or affect_posees[v]["politique"] != POLITIQUE_VM)},
|
||||
"affect_posees": affect_posees,
|
||||
"vm_absentes": sorted(vm_absentes),
|
||||
"datacenter": {"actif": str(opts.get("enable", "")), "politique": str(opts.get("policy_in", ""))},
|
||||
|
|
@ -238,10 +248,26 @@ 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}")
|
||||
# `policy_in=DROP` sur LA VM : c'est ce qui rend les groupes ci-dessus exclusifs.
|
||||
# `policy_in` 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.
|
||||
#
|
||||
# REJECT ET NON DROP, PARCE QUE C'EST L'INTERNE (2026-08-28). Cette couche filtre
|
||||
# l'est-ouest : des machines d'un meme ecosysteme, ou d'un tenant a son site. Le
|
||||
# silence y coutait cher — un timeout voulait dire aussi bien « aucune machine »
|
||||
# que « une politique », et le 2026-08-28 cette ambiguite a fait appliquer un
|
||||
# pare-feu pour reparer une ABSENCE. Ce qui informe n'est pas le refus, c'est ce
|
||||
# qu'il fait au TIMEOUT : quand la politique parle, le silence ne veut plus dire
|
||||
# qu'une chose.
|
||||
#
|
||||
# L'OBSCURITE ETAIT DEJA NULLE ICI : chaque machine porte un `/etc/hosts` genere
|
||||
# qui liste toutes ses voisines avec leurs adresses. Se cacher de pairs qui ont
|
||||
# deja notre adresse n'achete rien.
|
||||
#
|
||||
# LA FRONTIERE, ELLE, RESTE EN `block` — et c'est ce qui rend ce reject prudent :
|
||||
# rien d'indeclare ne franchit le perimetre, donc il ne repond jamais a
|
||||
# l'Internet. Voir la preuve P53.
|
||||
_fait(api(f"/nodes/{noeud}/qemu/{v}/firewall/options", "PUT",
|
||||
{"enable": 1, "policy_in": "DROP"}), f"pare-feu de la VM {v}")
|
||||
{"enable": 1, "policy_in": POLITIQUE_VM}), 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"]:
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
# GARDE ANTI-LOCKOUT. Le plan de gestion du tenant est resolu en
|
||||
# `+<t>-flotte` : les hotes du tenant, et EUX SEULS. Or Ansible se
|
||||
# connecte depuis le reseau d'administration, qui n'en fait pas partie.
|
||||
# Sans cette regle, la premiere VM passee en `policy_in=DROP` se ferme
|
||||
# Sans cette regle, la premiere VM passee en `policy_in=REJECT` se ferme
|
||||
# derriere l'outil qui vient de la configurer. Meme source unique que
|
||||
# les nftables et la frontiere : l'intrant `nftables_admin_ssh`.
|
||||
if "22" in _ports(fl) and admin_de(nom):
|
||||
|
|
@ -265,7 +265,7 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
if porte:
|
||||
affect.append({"hote": hote, "vmid": vmid, "groupes": porte})
|
||||
|
||||
# Roles portes mais SANS regle entrante : sous une politique DROP ils sont
|
||||
# Roles portes mais SANS regle entrante : sous une politique REJECT ils sont
|
||||
# injoignables. C'est souvent voulu (ils n'ecoutent que sur la boucle locale, ou
|
||||
# seule la frontiere les atteint) — mais l'absence doit se lire, pas se deviner.
|
||||
avec = {g["role"] for g in groupes}
|
||||
|
|
@ -310,19 +310,25 @@ def rendre(devis: dict) -> str:
|
|||
"# (`make devis-opnsense`). Les flux `pair: externe` sont sautes.",
|
||||
"# ============================================================",
|
||||
"",
|
||||
"## 0. Prerequis — TROIS verrous, et le DROP se pose PAR VM",
|
||||
"## 0. Prerequis — TROIS verrous, et le REJECT 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 :",
|
||||
"# La politique REJECT se pose SUR CHAQUE VM TENANT, jamais au datacenter :",
|
||||
"#",
|
||||
"# REJECT ET NON DROP : cette couche filtre l'EST-OUEST, entre machines qui se",
|
||||
"# connaissent deja (leur `/etc/hosts` genere liste toutes leurs voisines). Un",
|
||||
"# refus muet y coutait du diagnostic sans rien acheter — et rendait un timeout",
|
||||
"# indiscernable d'une machine absente. La FRONTIERE, elle, reste en `block` :",
|
||||
"# c'est ce qui rend ce reject prudent. Voir la preuve P53.",
|
||||
"# 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",
|
||||
"# pvesh set /nodes/<n>/qemu/<vmid>/firewall/options --enable 1 --policy_in REJECT",
|
||||
"#",
|
||||
"# `policy_in DROP` AU DATACENTER serait la politique par defaut de TOUTE VM dont",
|
||||
"# `policy_in` 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.",
|
||||
"",
|
||||
|
|
@ -353,7 +359,7 @@ def rendre(devis: dict) -> str:
|
|||
out.append(f" vmid {a['vmid'] or '(non materialise)'} {a['hote']:<18} "
|
||||
f"-> {', '.join(a['groupes'])}")
|
||||
if b["muets"]:
|
||||
out += ["", "### Sans regle entrante — injoignables sous la politique DROP"]
|
||||
out += ["", "### Sans regle entrante — injoignables sous la politique REJECT"]
|
||||
for m in b["muets"]:
|
||||
marque = " /!\\" if m["motif"].startswith("A VERIFIER") else ""
|
||||
out.append(f" {m['role']:<22} {m['motif']}{marque}")
|
||||
|
|
@ -363,11 +369,11 @@ def rendre(devis: dict) -> str:
|
|||
out.append("")
|
||||
out += [
|
||||
"## Defaut",
|
||||
"# `policy_in DROP` SUR CHAQUE VM TENANT : ce qui n'est pas ci-dessus n'entre pas.",
|
||||
"# `policy_in REJECT` 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,",
|
||||
"# Le refus 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)
|
||||
|
|
|
|||
|
|
@ -1396,6 +1396,85 @@ def preuve_carte_dit_vrai() -> tuple[bool, str]:
|
|||
f"{len(_carte_chiffres_mesures())} chiffres correspondent a la mesure.")
|
||||
|
||||
|
||||
def preuve_interne_parle_bordure_se_tait() -> tuple[bool, str]:
|
||||
"""L'interne REFUSE A VOIX HAUTE, la bordure reste MUETTE.
|
||||
|
||||
POURQUOI (2026-08-28). Sous `drop` partout, un timeout voulait dire trois choses
|
||||
incompatibles : aucune machine, aucune route, ou une politique. Deux erreurs de
|
||||
diagnostic en une journee, dont une qui a fait appliquer un pare-feu pour reparer une
|
||||
ABSENCE. Ce qui informe n'est pas le refus — c'est ce qu'il fait au SILENCE : quand la
|
||||
politique parle, un timeout ne veut plus dire qu'une chose.
|
||||
|
||||
L'OBSCURITE ETAIT DEJA NULLE A L'INTERIEUR : chaque machine porte un `/etc/hosts`
|
||||
genere qui liste toutes ses voisines avec leurs adresses. Se cacher de pairs qui ont
|
||||
deja notre adresse ne protege de rien, et coute le diagnostic.
|
||||
|
||||
LA BORDURE, ELLE, DOIT SE TAIRE, et c'est ce qui rend le reject interne prudent :
|
||||
rien d'indeclare ne franchit le perimetre, donc ce reject ne repond jamais a
|
||||
l'Internet. Mesure du 2026-08-27 a la frontiere : 982 000 entrees par jour, dont 82 %
|
||||
un balayage contre le port VNC. Y repondre serait un vecteur d'amplification, source
|
||||
usurpee comprise.
|
||||
|
||||
Cette preuve garde les deux moities ENSEMBLE, parce que l'une sans l'autre est fausse :
|
||||
une bordure bavarde s'expose, un interne muet ment.
|
||||
"""
|
||||
fautes: list[str] = []
|
||||
mesures: list[str] = []
|
||||
|
||||
# 1. nftables d'hote : chaque ruleset genere doit REFUSER A VOIX HAUTE.
|
||||
src = (RACINE / "scripts" / "resoudre_flux.py").read_text(encoding="utf-8")
|
||||
if "reject with icmpx type admin-prohibited" not in src:
|
||||
fautes.append("le gabarit nftables n'emet pas de `reject with icmpx type "
|
||||
"admin-prohibited` : l'interne resterait muet")
|
||||
# `tcp reset` serait indiscernable d'un port ferme sans service.
|
||||
if "reject with tcp reset" in src:
|
||||
fautes.append("le gabarit nftables emet un `tcp reset`, indiscernable d'un port "
|
||||
"ferme sans service — `admin-prohibited` dit qu'une POLITIQUE refuse")
|
||||
|
||||
rulesets = sorted((INSTANCE / "flux-genere").glob("*.nft"))
|
||||
muets = [f.name for f in rulesets
|
||||
if "reject with icmpx type admin-prohibited" not in f.read_text(encoding="utf-8")]
|
||||
if muets:
|
||||
fautes.append(f"{len(muets)} ruleset(s) genere(s) sans refus audible : "
|
||||
+ ", ".join(muets[:4]))
|
||||
mesures.append(f"{len(rulesets)} ruleset(s) nftables refusent a voix haute")
|
||||
|
||||
# 2. Pare-feu est-ouest Proxmox : politique REJECT, et UNE SEULE source.
|
||||
ew = (RACINE / "scripts" / "appliquer_proxmox_fw.py").read_text(encoding="utf-8")
|
||||
if 'POLITIQUE_VM = "REJECT"' not in ew:
|
||||
fautes.append("le pare-feu est-ouest ne pose pas `POLITIQUE_VM = \"REJECT\"`")
|
||||
# DEUX LITTERAUX POUR LA MEME DECISION, c'est une divergence differee : l'ecriture de
|
||||
# la politique et la detection de son ecart doivent lire la MEME source.
|
||||
#
|
||||
# ON NE CHERCHE PAS LE MOT « DROP », ON CHERCHE UNE ASSIGNATION EN DUR. Ma premiere
|
||||
# version refusait toute occurrence du litteral, et elle a fait echouer un code
|
||||
# JUSTE : `dc["politique"].upper() in ("DROP", "REJECT")` detecte une politique posee
|
||||
# AU DATACENTER, ou elle vaudrait pour tout le parc — c'est un garde-fou, pas un
|
||||
# reglage. Une preuve qui interdit un mot au lieu de mesurer une propriete finit par
|
||||
# accuser ce qu'elle devrait proteger.
|
||||
if re.search(r'"policy_in":\s*"', ew):
|
||||
fautes.append("la politique de la VM est ecrite en dur : elle doit venir de "
|
||||
"`POLITIQUE_VM`, seule source")
|
||||
if re.search(r'\["politique"\]\s*!=\s*"', ew):
|
||||
fautes.append("la detection d'ecart compare a un litteral : elle doit lire "
|
||||
"`POLITIQUE_VM`, sinon les deux divergeront")
|
||||
mesures.append("pare-feu est-ouest en REJECT, source unique")
|
||||
|
||||
# 3. La bordure se tait — et ce n'est PAS une preference, c'est la condition de
|
||||
# prudence du reject interne. Les actions du devis OPNsense restent `pass`/`block`.
|
||||
front = (RACINE / "scripts" / "devis_opnsense.py").read_text(encoding="utf-8")
|
||||
if '"reject"' in front or "'reject'" in front:
|
||||
fautes.append("le devis de la frontiere emet un `reject` : la bordure doit rester "
|
||||
"muette, sans quoi elle repond aux balayages d'Internet")
|
||||
actions = sorted(set(re.findall(r'"action":\s*"(\w+)"', front))
|
||||
| set(re.findall(r'action.*?or\s+"(\w+)"', front)))
|
||||
mesures.append("frontiere muette (actions : " + ", ".join(actions or ["pass"]) + ")")
|
||||
|
||||
if fautes:
|
||||
return False, "La ligne n'est plus tenue :\n - " + "\n - ".join(fautes)
|
||||
return True, "L'interne parle, la bordure se tait — " + " ; ".join(mesures) + "."
|
||||
|
||||
|
||||
def preuve_materialiser_n_exige_pas_d_entrer() -> tuple[bool, str]:
|
||||
"""Creer une VM ne demande pas d'entrer dans le tenant qui l'heberge.
|
||||
|
||||
|
|
@ -2006,6 +2085,8 @@ PREUVES: list[dict] = [
|
|||
"refs": [], "func": preuve_collections_declarees_et_epinglees},
|
||||
{"id": "P52", "titre": "Materialiser n'exige pas d'entrer dans le tenant",
|
||||
"refs": [], "func": preuve_materialiser_n_exige_pas_d_entrer},
|
||||
{"id": "P53", "titre": "L'interne refuse a voix haute, la bordure se tait",
|
||||
"refs": [], "func": preuve_interne_parle_bordure_se_tait},
|
||||
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
|
||||
"func": preuve_devis_frontiere_du_site},
|
||||
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
||||
|
|
|
|||
|
|
@ -392,8 +392,37 @@ def _rendre_nft(hote: str, regles: list[str]) -> str:
|
|||
f" ct state invalid drop\n"
|
||||
f" ip protocol icmp accept\n"
|
||||
f" ip6 nexthdr icmpv6 accept\n\n"
|
||||
f"{corps}\n"
|
||||
f"{corps}\n\n"
|
||||
f" # L'INTERNE PARLE, LA BORDURE SE TAIT (2026-08-28).\n"
|
||||
f" #\n"
|
||||
f" # Ce qui n'est pas accepte plus haut est REFUSE A VOIX HAUTE, et ce n'est pas\n"
|
||||
f" # le refus qui informe : c'est ce qu'il fait au TIMEOUT. Sous `drop`, un\n"
|
||||
f" # silence voulait dire trois choses incompatibles — aucune machine, aucune\n"
|
||||
f" # route, ou une politique. Deux erreurs de diagnostic en une journee le\n"
|
||||
f" # 2026-08-28, dont une qui a fait appliquer un pare-feu pour reparer une\n"
|
||||
f" # ABSENCE. Quand la politique parle, le silence ne veut plus dire qu'une\n"
|
||||
f" # chose : il n'y a personne, ou il n'y a pas de chemin.\n"
|
||||
f" #\n"
|
||||
f" # `admin-prohibited` ET NON `tcp reset` : un RST est indiscernable d'un port\n"
|
||||
f" # ferme sans service. Ce message-ci dit qu'une POLITIQUE a refuse — c'est la\n"
|
||||
f" # seule forme qui distingue « on ne veut pas de toi » de « il n'y a rien ».\n"
|
||||
f" #\n"
|
||||
f" # POURQUOI C'EST PRUDENT ICI ET PAS A LA FRONTIERE : la bordure reste en\n"
|
||||
f" # `block`, donc rien d'indeclare ne franchit le perimetre. Ce reject ne\n"
|
||||
f" # repond jamais a l'Internet — il ne parle qu'a des pairs qui ont deja\n"
|
||||
f" # l'adresse de cette machine dans leur `/etc/hosts` genere. L'obscurite\n"
|
||||
f" # etait donc deja nulle entre nous : le `drop` coutait du diagnostic sans\n"
|
||||
f" # rien acheter. Mesure du 2026-08-27 a la frontiere, qui explique l'autre\n"
|
||||
f" # moitie du choix : 982 000 entrees par jour, dont 82 %% un balayage\n"
|
||||
f" # Internet contre le port VNC. Repondre a chacune serait un vecteur\n"
|
||||
f" # d'amplification, source usurpee comprise.\n"
|
||||
f" reject with icmpx type admin-prohibited\n"
|
||||
f" }}\n\n"
|
||||
f" # LA CHAINE `forward` RESTE MUETTE, et ce n'est pas un oubli : elle porte le\n"
|
||||
f" # trafic qui TRAVERSE cet hote, pas celui qui lui est destine. Y repondre\n"
|
||||
f" # ferait parler cette machine AU NOM d'une destination qui n'est pas elle —\n"
|
||||
f" # un refus qu'on attribuerait au mauvais endroit, c'est-a-dire exactement le\n"
|
||||
f" # defaut qu'on corrige dans `input`.\n"
|
||||
f" chain forward {{\n"
|
||||
f" type filter hook forward priority 0; policy drop;\n"
|
||||
f" ct state established,related accept\n"
|
||||
|
|
|
|||
Loading…
Reference in a new issue