nftables_baseline : la passerelle n'est pas une porte (garde 8006)
La sonde connectivite essaie l'API Proxmox (8006) sur la passerelle par defaut : chez un locataire en SDN, c'est l'hyperviseur dans son VRF, hors de la frontiere. Ouvert -> CRITIQUE. Temoin : CONNECTIVITE_PASSERELLE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ce84d0e1ea
commit
92a6fb0ead
3 changed files with 80 additions and 2 deletions
34
CHANGELOG.md
34
CHANGELOG.md
|
|
@ -1,5 +1,39 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-10-05 (88) — La passerelle n'est pas une porte : une garde sur l'API Proxmox
|
||||
|
||||
**Le reste ouvert de (87)** : chez un locataire en SDN, la passerelle de chaque zone est une
|
||||
interface de l'**hyperviseur**, dans le VRF du locataire. Une API Proxmox qui y répondrait
|
||||
serait jointe sans passer par la frontière. Fermée aujourd'hui (`ECONNREFUSED`).
|
||||
|
||||
**Option choisie par l'exploitant** : tester la **propriété**, depuis les machines des
|
||||
locataires, plutôt que lire `net.ipv4.tcp_l3mdev_accept` sur les hyperviseurs. Cela aurait
|
||||
demandé un nouveau rôle sur des machines que Set-OPS ne gère pas (Prometheus les tire ; leur
|
||||
node_exporter 1.5 n'a pas de collecteur `sysctl`).
|
||||
|
||||
**Fait** (`nftables_baseline`) :
|
||||
- la sonde `connectivite`, qui tourne chaque minute sur chaque machine, essaie aussi les ports
|
||||
de `nftables_baseline_connectivite_passerelle_interdits` (`[8006]`) sur la **passerelle par
|
||||
défaut**. `ouvert` → **CRITIQUE** (« PASSERELLE OUVERTE ») ; refusé, rejeté ou silencieux →
|
||||
attendu ; passerelle introuvable → avertissement, une garde qui ne s'exerce pas le dit. Ici,
|
||||
`ouvert` prouve bien quelque chose : entre une machine et sa passerelle, aucun relais ;
|
||||
- elle couvre toute cause : le réglage du noyau, un pare-feu d'hôte retiré, un service qui se
|
||||
mettrait à écouter partout ;
|
||||
- `CONNECTIVITE_PASSERELLE=<adresse>` vise une autre « passerelle » : le contrôle négatif de la
|
||||
garde ; nouvelle mesure de performance `passerelle_ouverte`.
|
||||
|
||||
**Éprouvé**, sonde rendue et lancée à la main :
|
||||
- cas normal sur `ops-01` et `web-dorsal-01` (Technolibre), `web-frontal-01` (Chezlepro) : la
|
||||
passerelle de zone refuse le 8006, sonde verte, ses mesures habituelles intactes ;
|
||||
- au site (`site-mon-01`, passerelle = la frontière) : verte, aucune fausse alarme ;
|
||||
- témoin négatif (443 interdit, « passerelle » = la forge du site, qui répond) : **CRITIQUE**,
|
||||
code 2.
|
||||
|
||||
**À déployer** : `nftables_baseline` (groupe `serveur_durci`), chez les deux locataires et au
|
||||
site. Le ruleset nftables ne change pas ; seule la sonde change.
|
||||
|
||||
**Validation** : `ansible-lint` (0 violation), `make verifier` conforme, 89/89.
|
||||
|
||||
## 2026-10-05 (87) — Une règle qui disait qu'un locataire pouvait créer des VM
|
||||
|
||||
**La question de l'exploitant** : « un flux est permis entre une machine d'un tenant et l'API
|
||||
|
|
|
|||
|
|
@ -17,3 +17,12 @@ nftables_baseline_ruleset_genere: "{{ inventory_dir }}/../../flux-genere/{{ inve
|
|||
# La liste des flux que la sonde `connectivite` teste chaque minute, generee par `make flux`
|
||||
# a cote du ruleset, depuis les MEMES regles (voir templates/sonde-connectivite.sh.j2).
|
||||
nftables_baseline_connectivite_genere: "{{ inventory_dir }}/../../flux-genere/{{ inventory_hostname }}.connectivite.json"
|
||||
|
||||
# LA PASSERELLE N'EST PAS UNE PORTE (2026-10-05). Ports qui ne doivent JAMAIS repondre sur la
|
||||
# passerelle par defaut de la machine — la sonde `connectivite` le verifie chaque minute.
|
||||
# Chez un locataire en SDN, la passerelle de zone est l'HYPERVISEUR lui-meme, dans le VRF du
|
||||
# locataire : une API Proxmox qui y repondrait contournerait la frontiere. Fermee aujourd'hui
|
||||
# (mesure du 2026-10-05 : ECONNREFUSED) ; la garde dit si elle s'ouvre, quelle qu'en soit la
|
||||
# cause (`net.ipv4.tcp_l3mdev_accept`, un pare-feu d'hote retire...). Sur une machine du site,
|
||||
# la passerelle est la frontiere, qui n'ecoute pas sur ces ports.
|
||||
nftables_baseline_connectivite_passerelle_interdits: [8006]
|
||||
|
|
|
|||
|
|
@ -33,10 +33,21 @@
|
|||
# couvrent pas (AVERTISSEMENT). Un flux reel non declare serait coupe a la prochaine regle
|
||||
# posee : mieux vaut l'apprendre avant.
|
||||
#
|
||||
# CE QU'ELLE GARDE AUSSI (2026-10-05) : la passerelle par defaut ne doit repondre sur aucun
|
||||
# port de `nftables_baseline_connectivite_passerelle_interdits` (l'API Proxmox, 8006). Chez un
|
||||
# locataire en SDN, cette passerelle est l'hyperviseur, dans le VRF du locataire ; une API qui y
|
||||
# repondrait contournerait la frontiere. Ici, `ouvert` prouve bien quelque chose : entre la
|
||||
# machine et sa passerelle, aucun relais. Ouvert -> CRITIQUE ; refuse, rejete, delai -> attendu.
|
||||
# Passerelle introuvable -> AVERTISSEMENT : une garde qui ne s'exerce pas doit le dire.
|
||||
#
|
||||
# Contrat : une ligne, code 0/1/2 (docs/supervision-conception.md).
|
||||
# `CONNECTIVITE_JSON=<fichier>` impose une autre liste : c'est le controle negatif.
|
||||
# `CONNECTIVITE_PASSERELLE=<adresse>` vise une autre « passerelle » : le controle negatif de la
|
||||
# garde (une adresse dont un port interdit repond doit la faire crier).
|
||||
exec python3 - "${CONNECTIVITE_JSON:-/usr/local/lib/setops/connectivite.json}" <<'PY'
|
||||
import concurrent.futures, errno, ipaddress, json, socket, subprocess, sys
|
||||
import concurrent.futures, errno, ipaddress, json, os, socket, subprocess, sys
|
||||
|
||||
INTERDITS = {{ nftables_baseline_connectivite_passerelle_interdits | to_json }}
|
||||
|
||||
try:
|
||||
d = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
|
|
@ -78,6 +89,23 @@ with concurrent.futures.ThreadPoolExecutor(max_workers=16) as ex:
|
|||
resultats = list(ex.map(essai, sorties))
|
||||
|
||||
coupes = [(f, r) for f, r in resultats if r not in ("ouvert", "refuse")]
|
||||
|
||||
# La passerelle n'est pas une porte.
|
||||
def passerelle_par_defaut():
|
||||
if os.environ.get("CONNECTIVITE_PASSERELLE"):
|
||||
return os.environ["CONNECTIVITE_PASSERELLE"]
|
||||
try:
|
||||
for l in subprocess.run(["ip", "-4", "route", "show", "default"], capture_output=True,
|
||||
text=True, timeout=5).stdout.splitlines():
|
||||
c = l.split()
|
||||
if "via" in c:
|
||||
return c[c.index("via") + 1]
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
return None
|
||||
|
||||
gw = passerelle_par_defaut()
|
||||
porte = [p for p in INTERDITS if gw and tcp(gw, p) == "ouvert"]
|
||||
personne = [(f, r) for f, r in resultats if r == "refuse"]
|
||||
ouverts = len(resultats) - len(coupes) - len(personne)
|
||||
|
||||
|
|
@ -132,12 +160,19 @@ def nomme(xs):
|
|||
return ", ".join(f"{f['vers']}:{f['port'] if f['proto'] != 'icmp' else 'ping'} ({r})" for f, r in xs[:5]) \
|
||||
+ (f" et {len(xs) - 5} autre(s)" if len(xs) > 5 else "")
|
||||
|
||||
perf = f"| ouverts={ouverts};;;0 coupes={len(coupes)};;;0 sans_service={len(personne)};;;0 hors_regles={len(hors)};;;0"
|
||||
perf = (f"| ouverts={ouverts};;;0 coupes={len(coupes)};;;0 sans_service={len(personne)};;;0 "
|
||||
f"hors_regles={len(hors)};;;0 passerelle_ouverte={len(porte)};;;0")
|
||||
if porte:
|
||||
print(f"PASSERELLE OUVERTE : {gw} repond sur {', '.join(map(str, porte))} — la passerelle de zone "
|
||||
f"(l'hyperviseur, chez un locataire) expose ce qui doit rester derriere la frontiere. {perf}")
|
||||
sys.exit(2)
|
||||
if coupes:
|
||||
print(f"COUPE : {len(coupes)}/{len(resultats)} flux sortant(s) ne passent pas — {nomme(coupes)}. {perf}")
|
||||
sys.exit(2)
|
||||
avert = []
|
||||
info = ""
|
||||
if INTERDITS and not gw:
|
||||
avert.append("passerelle par defaut introuvable — la garde de la passerelle ne s'exerce pas")
|
||||
if personne:
|
||||
avert.append(f"{len(personne)} flux refuse(s) — service absent, ou refus de la frontiere — "
|
||||
f"{nomme(personne)}")
|
||||
|
|
|
|||
Loading…
Reference in a new issue