2026-07-07 03:08:09 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Resolveur des flux reseau Set-OPS : meta/flux.yml -> registre d'audit + apercus nftables.
|
|
|
|
|
|
|
|
|
|
Agrege les `roles/<role>/meta/flux.yml`, resout les `pair`, et produit :
|
|
|
|
|
- docs/registre-flux.md : la MATRICE D'AUDIT (niveau moteur, role->role : port,
|
|
|
|
|
sens, chiffrement, raison). Deterministe, sans donnee d'instance.
|
|
|
|
|
- des APERCUS nftables par hote (IP resolues, `ip saddr` = moindre privilege,
|
|
|
|
|
policy drop) sous <instance>/flux-genere/<hote>.nft. Fichiers GENERES et
|
|
|
|
|
inspectables — NON actives (l'activation reste un geste dedie, teste par noeud).
|
|
|
|
|
|
|
|
|
|
NON destructif : n'active aucun pare-feu. Voir docs/flux-conception.md.
|
|
|
|
|
|
|
|
|
|
Usage :
|
|
|
|
|
python3 scripts/resoudre_flux.py registre # (re)genere docs/registre-flux.md
|
|
|
|
|
python3 scripts/resoudre_flux.py nftables # apercus par hote (requiert l'inventaire)
|
|
|
|
|
python3 scripts/resoudre_flux.py verifier # valide le schema de tous les flux.yml
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import glob
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
resolution d'instance : une seule, partagee — au lieu de neuf copies
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.
- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
_frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.
Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.
LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.
CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.
P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.
make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:19:55 -04:00
|
|
|
from inventory_rules import instance_courante, est_groupe_operationnel
|
2026-07-07 03:08:09 -04:00
|
|
|
|
|
|
|
|
RACINE = Path(__file__).resolve().parents[1]
|
|
|
|
|
ROLES = RACINE / "roles"
|
|
|
|
|
REGISTRE = RACINE / "docs" / "registre-flux.md"
|
resolution d'instance : une seule, partagee — au lieu de neuf copies
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.
- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
_frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.
Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.
LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.
CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.
P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.
make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:19:55 -04:00
|
|
|
INSTANCE = instance_courante()
|
2026-07-07 03:08:09 -04:00
|
|
|
|
|
|
|
|
SENS = {"ingress", "egress"}
|
flux : l'ICMP entre au registre — l'overlay à 1450 l'exige
Décision : l'overlay EVPN plafonne à 1450. Conséquence invisible : sous 1500,
tout ce qui traverse la frontière dépend de la découverte de MTU de chemin,
donc de l'ICMP « fragmentation nécessaire ».
Or le registre ne connaissait que TCP et UDP. Ce message ne pouvait pas être
déclaré et la bordure en `block in log all` l'aurait jeté : la connexion
s'établit, les petites requêtes passent, les grosses réponses restent
suspendues — la panne la plus coûteuse à diagnostiquer, et celle qu'on impute
d'abord à l'application.
`protocole: icmp` est admis ; le champ `port` y porte le type
(`frag-needed`). Le socle déclare les deux sens. Vérifié : nftables d'hôte
inchangés, le pair `externe` reste sauté.
Reconnaissance (lecture seule) : l'EVPN est à moitié construit — contrôleur
EVPN0017 (ASN 65000), zones VRF0011 et VRF0017, un VRF par tenant avec le VNI
égal à l'index. Aucun VNet, aucun nœud de sortie.
Signalé et non corrigé : les pairs BGP sont dans 10.27.19.0/24, le
sous-réseau Services-infra de Chezlepro. Le transport du cluster dérive de
l'index d'un tenant, et une VM de cette zone partage son sous-réseau avec les
VTEP — l'isolation est percée à l'endroit que l'EVPN devait fermer.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:07:30 -04:00
|
|
|
# `icmp` n'a pas de port : le champ `port` porte alors le TYPE (ex. `frag-needed`).
|
|
|
|
|
# Sans lui, la decouverte de MTU de chemin ne peut pas etre DECLAREE — et une bordure
|
|
|
|
|
# en default-deny la casse en silence des que l'overlay descend sous 1500.
|
|
|
|
|
PROTO = {"tcp", "udp", "icmp"}
|
2026-07-07 03:08:09 -04:00
|
|
|
CHIFFREMENT = {"tls-requis", "tls", "starttls", "ssh", "tls-cible", "clair", "n-a"}
|
2026-08-07 17:26:35 -04:00
|
|
|
# `admin` : les RESEAUX d'administration (intrant `nftables_admin_ssh`), pas des
|
|
|
|
|
# hotes. C'est le troisieme chemin d'arrivee, distinct de `flotte` (le tenant) et
|
|
|
|
|
# d'`externe` (Internet) — l'exploitant n'est ni l'un ni l'autre.
|
2026-08-24 17:15:19 -04:00
|
|
|
# `voisins_site` : les autres tenants FEDERES de la meme fabric. Ni `flotte` (mon
|
|
|
|
|
# ecosysteme), ni `externe` (l'Internet) — un quatrieme chemin d'arrivee, celui du
|
|
|
|
|
# VOISINAGE.
|
|
|
|
|
#
|
|
|
|
|
# POURQUOI CE MOT EXISTE (2026-08-24). Le cache d'artefacts se CHAINE : chaque ecosysteme
|
|
|
|
|
# garde le sien, et celui-ci prend le cache de l'hebergeur comme amont. Debian n'est alors
|
|
|
|
|
# telecharge qu'une fois pour tout le site. Sans ce mot, la seule facon de declarer ce flux
|
|
|
|
|
# aurait ete `ingress` + `externe` — qui signifie « depuis l'Internet » et aurait PUBLIE le
|
|
|
|
|
# cache au monde entier.
|
|
|
|
|
#
|
|
|
|
|
# Il rend des CIDR (les supernets des voisins), pas des adresses d'hotes : il est donc
|
|
|
|
|
# traite a part, comme `admin`.
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
# `fabric` — LE MATERIEL DE L'HEBERGEUR : hyperviseurs, frontiere, commutateurs.
|
|
|
|
|
#
|
|
|
|
|
# Il a fallu ce mot le 2026-08-25. Le runner de SITE declarait son API Proxmox en
|
|
|
|
|
# `externe`, et « externe » se rend par « tout sauf les espaces prives ». Or les
|
|
|
|
|
# hyperviseurs SONT en RFC 1918 : la regle sortante les aurait exclus, tout en ayant
|
|
|
|
|
# l'air d'ouvrir le flux. Un flux qui a l'air ouvert et qui ne l'est pas est pire qu'un
|
|
|
|
|
# flux ferme — il ne se cherche pas.
|
|
|
|
|
#
|
|
|
|
|
# Ce n'est pas non plus `flotte` (les machines d'un ecosysteme) ni `voisins_site` (les
|
|
|
|
|
# tenants d'a cote) : c'est le SOCLE sur lequel les uns et les autres reposent. Le seul
|
|
|
|
|
# a s'y adresser est le runner du site, et c'est tout son objet — materialiser.
|
2026-08-24 17:15:19 -04:00
|
|
|
MOTS_PAIR = {"edge", "flotte", "externe", "localhost", "expositions", "derive", "admin",
|
l heure vient de la frontiere : la derniere dependance vivante tombe
Mesure sur les quatorze : aucune sortie TCP vers une adresse publique, apt
par le cache, noms autoritaires en local, unattended-upgrades masked. Et
quatre pairs NTP publics par machine. Le role chrony posait le fuseau et
installait le demon sans jamais toucher a ses sources : le defaut de Debian
tenait depuis le premier jour, herite et jamais choisi.
L autorite est la frontiere, par decision de l exploitant. Elle etait deja
stratum 2 et ecoutait en 123 ; il ne manquait que le passage. 9 anciennes
regles port 123 vers !SETOPS_INTERNES retirees, 9 regles nommees vers
SETOPS_FRONTIERE posees : le changement resserre autant qu il centralise.
Deux chemins parce que la topologie en a deux. Un tenant n atteint pas la
frontiere par sa passerelle de zone — tenue par le SDN — mais par le lien de
transit. Une machine du site a la frontiere pour passerelle directe. Les deux
valeurs sont derivees de l underlay, via reseau_transit() plutot que d un
prefixe d adresse qui aurait menti chez le prochain hebergeur.
La patte face aux tenants manquait a opnsense_if_zones, pour la meme raison
que grappe-controle la veille.
Sonde horloge ecrite en meme temps : synchronisee ET contre la source
DECLAREE. Une machine peut etre parfaitement a l heure contre quatre serveurs
publics — c est exactement l etat d avant.
14 tenant + 7 site, toutes disciplinees, sources publiques = 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-11 12:08:13 -04:00
|
|
|
"voisins_site", "fabric", "frontiere", "runner_site"}
|
2026-07-07 03:08:09 -04:00
|
|
|
GROUPE_EDGE = "serveur_nginx"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ErreurFlux(Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def charger_flux() -> dict[str, list[dict]]:
|
|
|
|
|
"""{role: [flux, ...]} pour chaque role portant un meta/flux.yml."""
|
|
|
|
|
resultat: dict[str, list[dict]] = {}
|
|
|
|
|
for chemin in sorted(glob.glob(str(ROLES / "*" / "meta" / "flux.yml"))):
|
|
|
|
|
role = Path(chemin).parents[1].name
|
|
|
|
|
data = yaml.safe_load(Path(chemin).read_text(encoding="utf-8")) or {}
|
|
|
|
|
flux = data.get("flux")
|
|
|
|
|
if not isinstance(flux, list):
|
|
|
|
|
raise ErreurFlux(f"{chemin} : cle 'flux' (liste) attendue.")
|
|
|
|
|
resultat[role] = flux
|
|
|
|
|
return resultat
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pairs(flux: dict) -> list:
|
|
|
|
|
pair = flux.get("pair")
|
|
|
|
|
return pair if isinstance(pair, list) else [pair]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def valider(flux_par_role: dict[str, list[dict]]) -> tuple[int, int]:
|
|
|
|
|
"""Valide le schema + la coherence de matrice. Leve ErreurFlux sinon."""
|
|
|
|
|
roles_existants = {p.name for p in ROLES.iterdir() if p.is_dir()}
|
|
|
|
|
erreurs: list[str] = []
|
|
|
|
|
ingress: dict[str, set] = {}
|
|
|
|
|
egress: list[tuple[str, str, object]] = []
|
|
|
|
|
total = 0
|
|
|
|
|
for role, flux in flux_par_role.items():
|
|
|
|
|
for i, fl in enumerate(flux):
|
|
|
|
|
total += 1
|
|
|
|
|
ref = f"{role}/meta/flux.yml[{i}]"
|
|
|
|
|
for cle in ("sens", "port", "protocole", "pair", "chiffrement", "raison"):
|
|
|
|
|
if cle not in fl:
|
|
|
|
|
erreurs.append(f"{ref} : cle manquante {cle}")
|
|
|
|
|
if fl.get("sens") not in SENS:
|
|
|
|
|
erreurs.append(f"{ref} : sens invalide {fl.get('sens')!r}")
|
|
|
|
|
if fl.get("protocole") not in PROTO:
|
|
|
|
|
erreurs.append(f"{ref} : protocole invalide {fl.get('protocole')!r}")
|
|
|
|
|
if fl.get("chiffrement") not in CHIFFREMENT:
|
|
|
|
|
erreurs.append(f"{ref} : chiffrement invalide {fl.get('chiffrement')!r}")
|
|
|
|
|
for p in _pairs(fl):
|
|
|
|
|
if p not in MOTS_PAIR and p not in roles_existants and not str(p).startswith(("serveur_", "client_")):
|
|
|
|
|
erreurs.append(f"{ref} : pair inconnu {p!r}")
|
|
|
|
|
if fl.get("sens") == "ingress":
|
|
|
|
|
ingress.setdefault(role, set()).add(fl.get("port"))
|
|
|
|
|
elif fl.get("sens") == "egress":
|
|
|
|
|
for p in _pairs(fl):
|
|
|
|
|
if str(p).startswith(("serveur_", "client_")):
|
|
|
|
|
egress.append((role, p, fl.get("port")))
|
|
|
|
|
for role, pair, port in egress:
|
|
|
|
|
if port not in ingress.get(pair, set()):
|
|
|
|
|
erreurs.append(f"matrice : {role} -> {pair}:{port} sans ingress correspondant sur {pair}")
|
|
|
|
|
if erreurs:
|
|
|
|
|
raise ErreurFlux("\n - ".join(["Flux incoherents :"] + erreurs))
|
|
|
|
|
return len(flux_par_role), total
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Registre d'audit (niveau moteur, sans instance) ---------------------------
|
|
|
|
|
|
2026-08-06 13:59:11 -04:00
|
|
|
def _cle_port(port) -> tuple:
|
|
|
|
|
"""Cle de tri stable pour un port, qu'il soit numerique ou symbolique.
|
|
|
|
|
|
|
|
|
|
Tous les ports ne sont pas des nombres : `derive` (serveur_nginx) et
|
|
|
|
|
`frag-needed` (ICMP, serveur_debian) en sont. Comparer directement
|
|
|
|
|
laisserait `int < str` lever une TypeError des que deux ports de types
|
|
|
|
|
differents se retrouvent dans le meme sens pour un meme role.
|
|
|
|
|
"""
|
|
|
|
|
if isinstance(port, list):
|
|
|
|
|
port = port[0] if port else 0
|
|
|
|
|
try:
|
|
|
|
|
return (0, int(port), "")
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return (1, 0, str(port))
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 03:08:09 -04:00
|
|
|
def generer_registre(flux_par_role: dict[str, list[dict]]) -> str:
|
|
|
|
|
lignes = [
|
|
|
|
|
"# Registre des flux réseau — matrice d'audit (GÉNÉRÉ)",
|
|
|
|
|
"",
|
|
|
|
|
"> Généré par `scripts/resoudre_flux.py` (`make flux`) depuis les `roles/*/meta/flux.yml`.",
|
|
|
|
|
"> **Ne pas éditer à la main.** Matrice source→destination pour l'audit de sécurité et le",
|
|
|
|
|
"> label de certification. `ingress` = le rôle écoute ; `egress` = le rôle se connecte.",
|
|
|
|
|
"",
|
|
|
|
|
"| Rôle (propriétaire) | Sens | Port | Proto | Pair | Chiffrement | Raison |",
|
|
|
|
|
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
|
|
|
]
|
|
|
|
|
rangs = {s: i for i, s in enumerate(("ingress", "egress"))}
|
|
|
|
|
for role in sorted(flux_par_role):
|
2026-08-06 13:59:11 -04:00
|
|
|
for fl in sorted(flux_par_role[role], key=lambda f: (rangs.get(f.get("sens"), 9), _cle_port(f.get("port")))):
|
2026-07-07 03:08:09 -04:00
|
|
|
pair = ", ".join(str(p) for p in _pairs(fl))
|
|
|
|
|
port = fl.get("port")
|
|
|
|
|
port = ", ".join(str(x) for x in port) if isinstance(port, list) else str(port)
|
|
|
|
|
lignes.append(
|
|
|
|
|
f"| `{role}` | {fl.get('sens')} | {port} | {fl.get('protocole')} "
|
|
|
|
|
f"| {pair} | {fl.get('chiffrement')} | {fl.get('raison', '')} |"
|
|
|
|
|
)
|
|
|
|
|
# Synthese chiffrement
|
|
|
|
|
compte: dict[str, int] = {}
|
|
|
|
|
for flux in flux_par_role.values():
|
|
|
|
|
for fl in flux:
|
|
|
|
|
compte[fl.get("chiffrement")] = compte.get(fl.get("chiffrement"), 0) + 1
|
|
|
|
|
lignes += ["", "## Synthèse chiffrement", ""]
|
|
|
|
|
for chi in sorted(compte):
|
|
|
|
|
lignes.append(f"- **{chi}** : {compte[chi]} flux")
|
|
|
|
|
return "\n".join(lignes) + "\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Apercus nftables par hote (requiert l'inventaire) -------------------------
|
|
|
|
|
|
|
|
|
|
def _inventaire() -> Path:
|
|
|
|
|
for nom in ("principal", "production"):
|
|
|
|
|
p = INSTANCE / "inventories" / nom / "hosts.yml"
|
|
|
|
|
if p.exists():
|
|
|
|
|
return p
|
|
|
|
|
raise ErreurFlux(f"Inventaire introuvable sous {INSTANCE}/inventories/(principal|production).")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _enfants(data: dict) -> dict:
|
|
|
|
|
return data.get("all", {}).get("children", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _hotes_du_groupe(data: dict, groupe: str) -> dict:
|
|
|
|
|
return _enfants(data).get(groupe, {}).get("hosts", {}) or {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sources_admin_ssh() -> list[str]:
|
|
|
|
|
"""CIDR d'administration SSH toujours autorises (intrant nftables_admin_ssh).
|
|
|
|
|
|
|
|
|
|
Garde anti-lockout : le contexte d'ou l'on administre (controleur/VPN) doit
|
|
|
|
|
rester joignable en SSH quelle que soit la resolution des flux.
|
|
|
|
|
"""
|
|
|
|
|
dossier = _inventaire().parent / "group_vars" / "all"
|
|
|
|
|
for fichier in sorted(dossier.glob("*.yml")):
|
|
|
|
|
if "vault" in fichier.name:
|
|
|
|
|
continue
|
|
|
|
|
data = yaml.safe_load(fichier.read_text(encoding="utf-8")) or {}
|
|
|
|
|
if isinstance(data, dict) and data.get("nftables_admin_ssh"):
|
|
|
|
|
sources = data["nftables_admin_ssh"]
|
|
|
|
|
return [str(s) for s in sources] if isinstance(sources, list) else [str(sources)]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ip_par_hote(data: dict) -> dict[str, str]:
|
|
|
|
|
"""{hote: ansible_host} — l'IP vit dans hotes_actifs ; l'appartenance ailleurs est null."""
|
|
|
|
|
table: dict[str, str] = {}
|
|
|
|
|
for membres in _enfants(data).values():
|
|
|
|
|
for hote, v in (membres.get("hosts") or {}).items():
|
|
|
|
|
if isinstance(v, dict) and v.get("ansible_host"):
|
|
|
|
|
table[hote] = v["ansible_host"]
|
|
|
|
|
return table
|
|
|
|
|
|
|
|
|
|
|
une source vide n est pas « tout le monde »
Les journaux de la fabric, et le defaut de classe que leur mise en place a
revele. 10 hotes dans Loki (7 VM du site + 3 hyperviseurs), site a 67
services tous OK contre 51 avant.
METRIQUES TIREES, JOURNAUX POUSSES. Un scrape part du collecteur et doit
REVENIR : il exige un chemin symetrique, que la route par defaut gelee
interdit. Un push part de la source et n attend qu un accuse.
Trois trous dans les generateurs. Le devis de la frontiere ne connaissait
fabric qu en DESTINATION - un flux entrant tombait dans « rien d autre n
entre » sans rien dire. La regle etait posee sur la patte de la destination
alors que D-61 raisonne en ARRIVEE, et opt7 manquait a la table des zones.
Et le plus grave : fabric etait un mot RECONNU mais NON RESOLU. Source
vide, donc regle sans saddr - tcp dport 3100 accept, ouvert a tous. Un mot
reconnu mais non resolu est pire qu un mot inconnu : celui-ci serait refuse
a la validation, celui-la produit une porte grande ouverte qui a l air d un
flux precis.
LA CLASSE ENTIERE EST REFERMEE. Le defaut n etait pas propre a fabric :
toute paire nommant un ensemble et ne resolvant rien ouvrait le port.
Trouve en lisant les fichiers generes, l API de supervision d un tenant
etait ouverte a tous parce que pair: serveur_backup ne resout rien - cet
ecosysteme depose chez le site. expositions et externe, eux, veulent bien
dire tout le monde : c est une intention. Ailleurs, pas de source, pas de
regle, et le generateur le DIT.
Trois regles se referment, chacune verifiee AVANT d appliquer. Deux etaient
sans effet ; la troisieme portait Grafana, qui ecoute bien sur 3000. Sa
declaration disait pair: edge - juste chez un tenant, faux au site qui n a
pas d edge. Ajout de admin : ce qui etait accidentel devient explicite.
Et la mesure a montre autre chose : Grafana etait DEJA injoignable depuis
le poste, la frontiere ne laissant pas passer le 3000. Le site a une
console deployee et sans chemin d acces. Ce n est pas corrige ici, mais
c est dit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 19:14:23 -04:00
|
|
|
def _adresses_fabric() -> list[str]:
|
|
|
|
|
"""Les adresses du materiel de l'hebergeur : hyperviseurs et frontiere.
|
|
|
|
|
|
|
|
|
|
MEME SOURCE QUE L'ALIAS `SETOPS_FABRIC` du devis de la frontiere — la carte de
|
|
|
|
|
l'hebergeur. Ecrire ces adresses ailleurs les ferait mentir le jour ou un noeud
|
|
|
|
|
change de patte, ce qui est deja arrive : la frontiere a porte `.1` puis `.17`, et
|
|
|
|
|
les hyperviseurs ont continue de renvoyer vers une adresse que plus personne ne
|
|
|
|
|
portait, « sans qu'aucun journal ne le dise ».
|
|
|
|
|
|
|
|
|
|
Rend [] quand aucun underlay n'est monte : degrader, jamais deviner.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
import underlay as _u
|
|
|
|
|
carte = _u.charger()
|
|
|
|
|
if not carte:
|
|
|
|
|
return []
|
|
|
|
|
return sorted({str(h["ip"]) for h in _u.hotes(carte)
|
|
|
|
|
if h.get("role") in ("hyperviseur", "frontiere") and h.get("ip")})
|
|
|
|
|
except Exception:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
l heure vient de la frontiere : la derniere dependance vivante tombe
Mesure sur les quatorze : aucune sortie TCP vers une adresse publique, apt
par le cache, noms autoritaires en local, unattended-upgrades masked. Et
quatre pairs NTP publics par machine. Le role chrony posait le fuseau et
installait le demon sans jamais toucher a ses sources : le defaut de Debian
tenait depuis le premier jour, herite et jamais choisi.
L autorite est la frontiere, par decision de l exploitant. Elle etait deja
stratum 2 et ecoutait en 123 ; il ne manquait que le passage. 9 anciennes
regles port 123 vers !SETOPS_INTERNES retirees, 9 regles nommees vers
SETOPS_FRONTIERE posees : le changement resserre autant qu il centralise.
Deux chemins parce que la topologie en a deux. Un tenant n atteint pas la
frontiere par sa passerelle de zone — tenue par le SDN — mais par le lien de
transit. Une machine du site a la frontiere pour passerelle directe. Les deux
valeurs sont derivees de l underlay, via reseau_transit() plutot que d un
prefixe d adresse qui aurait menti chez le prochain hebergeur.
La patte face aux tenants manquait a opnsense_if_zones, pour la meme raison
que grappe-controle la veille.
Sonde horloge ecrite en meme temps : synchronisee ET contre la source
DECLAREE. Une machine peut etre parfaitement a l heure contre quatre serveurs
publics — c est exactement l etat d avant.
14 tenant + 7 site, toutes disciplinees, sources publiques = 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-11 12:08:13 -04:00
|
|
|
def _adresses_frontiere() -> list[str]:
|
|
|
|
|
"""Les pattes de la frontiere ACTIVE — l'autorite de temps de l'ecosysteme.
|
|
|
|
|
|
|
|
|
|
PLUS ETROIT QUE `fabric`, ET C'EST LE POINT. `_adresses_fabric()` rend les
|
|
|
|
|
hyperviseurs ET la frontiere : s'en servir pour l'heure ouvrirait le 123 vers des
|
|
|
|
|
machines qui ne le servent pas. Une paire dit CE QU'EST le pair, pas ou il se trouve.
|
|
|
|
|
|
|
|
|
|
`etat: reserve` est exclu : `bifrost-2` est declaree et ne porte aucune adresse. La
|
|
|
|
|
compter rendrait une regle vers une machine qui n'existe pas encore — et le jour ou
|
|
|
|
|
elle existera, elle heritera de la regle sans que personne ne l'ait decide.
|
|
|
|
|
|
|
|
|
|
Rend [] quand aucun underlay n'est monte : degrader, jamais deviner.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
import underlay as _u
|
|
|
|
|
carte = _u.charger()
|
|
|
|
|
if not carte:
|
|
|
|
|
return []
|
|
|
|
|
return sorted({str(h["ip"]) for h in _u.hotes(carte)
|
|
|
|
|
if h.get("role") == "frontiere" and h.get("ip")
|
|
|
|
|
and str(h.get("etat", "actif")) != "reserve"})
|
|
|
|
|
except Exception:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict[str, str],
|
|
|
|
|
site: bool = False) -> list[str]:
|
2026-07-07 03:08:09 -04:00
|
|
|
"""pair -> IP sources concretes (pour une regle ingress). [] = pas de regle inter-noeud."""
|
|
|
|
|
noms: set[str] = set()
|
|
|
|
|
for p in (pair if isinstance(pair, list) else [pair]):
|
|
|
|
|
if p in ("localhost", "externe", "expositions", "derive"):
|
|
|
|
|
continue # lo (auto), frontiere OPNsense, ou hors perimetre noeud
|
insemination : declarer le lien, l'emettre d'un seul cote, et un test rouge
L'insemination avait un nom depuis ce matin ; elle n'avait pas de flux. Deux
declarations, aux deux bouts, et rien d'autre :
serveur_ops_site egress 22/tcp -> serveur_ops_tenant
serveur_ops_tenant ingress 22/tcp <- runner_site partage: true
ETROIT PAR CONSTRUCTION : il vise le GROUPE `serveur_ops_tenant`, qu'un ecosysteme
ne pose que sur une machine. Au socle, il aurait ouvert le SSH du site vers toute
la flotte du tenant.
L'en-tete disait « ce role n'entre JAMAIS chez un tenant ». Frontiere intenable :
`creer-vm` exige `_instance-requise`, et le runner du site avait deja du basculer
son symlink `instance` sur OPS-Chezlepro pour materialiser ses VM. Declarer ne cree
pas ce pouvoir — ca rend limitable un pouvoir qui s'exercait sans borne. Ce qui
reste interdit n'est pas une regle mais un FAIT : il n'a pas la voute du tenant.
LA REGLE EST EMISE D'UN SEUL COTE, et pas celui qu'on croit. Le paquet penetre le
pare-feu par la patte du SITE, pas par le transit : la regle appartient au cote
site du devis. L'emettre aussi depuis l'`ingress` du tenant aurait produit une
seconde regle sur la mauvaise interface — jamais evaluee, indiscernable d'une regle
utile. La declaration du tenant pose sa regle nftables, et elle seule :
ip saddr { 10.0.31.11 } tcp dport 22 accept
L'adresse DERIVE du plan du site. Ecrite a la main, elle aurait survecu au prochain
deplacement du runner sans bruit — le site a deja deplace ses machines le 08-25.
Plan de la frontiere : 2 objets a creer, 0 a retirer, 126 inchanges. RIEN D'APPLIQUE.
P41 APPLIQUEE AU PLAN DU SITE : `resoudre_flux` en avait besoin a son tour ; les
trois lecteurs demenagent dans `underlay` et `devis_opnsense` delegue.
DEUX GARDES ONT TRAVAILLE : P33 a refuse `ingress 22` sur un hote portant deja le
sshd du socle (reponse : `partage: true`, comme `serveur_backup`), et le devis a
refuse d'emettre vers un alias vide.
UN TEST ROUGE DEPUIS TROIS JOURS. `test_adressage_derive` construisait un site avec
un `index` — or un SITE n'en a pas depuis add94f2 (08-25), remplace par
`bande_basse_de`. Invisible parce que le geste quotidien est `make prouver`, qui ne
joue pas les tests. Remis sur le contrat actuel, avec sa contrepartie : sans
`bande_basse_de`, aucun chevauchement n'est tolere.
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 13:09:28 -04:00
|
|
|
if p in ("admin", "voisins_site", "runner_site"):
|
|
|
|
|
# Aucun des trois n'est un groupe d'hotes DE CET ECOSYSTEME : ce sont des
|
|
|
|
|
# sources qui lui sont EXTERIEURES. `admin` vient de l'intrant
|
|
|
|
|
# `nftables_admin_ssh` ; `voisins_site` des supernets des autres tenants
|
|
|
|
|
# federes ; `runner_site` du plan du SITE. Traites a part, plus bas.
|
2026-08-07 17:26:35 -04:00
|
|
|
continue
|
l heure vient de la frontiere : la derniere dependance vivante tombe
Mesure sur les quatorze : aucune sortie TCP vers une adresse publique, apt
par le cache, noms autoritaires en local, unattended-upgrades masked. Et
quatre pairs NTP publics par machine. Le role chrony posait le fuseau et
installait le demon sans jamais toucher a ses sources : le defaut de Debian
tenait depuis le premier jour, herite et jamais choisi.
L autorite est la frontiere, par decision de l exploitant. Elle etait deja
stratum 2 et ecoutait en 123 ; il ne manquait que le passage. 9 anciennes
regles port 123 vers !SETOPS_INTERNES retirees, 9 regles nommees vers
SETOPS_FRONTIERE posees : le changement resserre autant qu il centralise.
Deux chemins parce que la topologie en a deux. Un tenant n atteint pas la
frontiere par sa passerelle de zone — tenue par le SDN — mais par le lien de
transit. Une machine du site a la frontiere pour passerelle directe. Les deux
valeurs sont derivees de l underlay, via reseau_transit() plutot que d un
prefixe d adresse qui aurait menti chez le prochain hebergeur.
La patte face aux tenants manquait a opnsense_if_zones, pour la meme raison
que grappe-controle la veille.
Sonde horloge ecrite en meme temps : synchronisee ET contre la source
DECLAREE. Une machine peut etre parfaitement a l heure contre quatre serveurs
publics — c est exactement l etat d avant.
14 tenant + 7 site, toutes disciplinees, sources publiques = 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-11 12:08:13 -04:00
|
|
|
if p == "frontiere":
|
|
|
|
|
# LA FRONTIERE EST UN PAIR A ELLE SEULE (2026-09-11). Comme `fabric`, elle
|
|
|
|
|
# vit dans la carte de l'hebergeur et non dans un groupe d'hotes — mais plus
|
|
|
|
|
# etroite : `fabric` couvre aussi les hyperviseurs, qui ne servent pas
|
|
|
|
|
# l'heure. Resolue plus bas, jamais laissee tomber dans le cas general : une
|
|
|
|
|
# source vide produirait une regle sans `saddr`.
|
|
|
|
|
continue
|
une source vide n est pas « tout le monde »
Les journaux de la fabric, et le defaut de classe que leur mise en place a
revele. 10 hotes dans Loki (7 VM du site + 3 hyperviseurs), site a 67
services tous OK contre 51 avant.
METRIQUES TIREES, JOURNAUX POUSSES. Un scrape part du collecteur et doit
REVENIR : il exige un chemin symetrique, que la route par defaut gelee
interdit. Un push part de la source et n attend qu un accuse.
Trois trous dans les generateurs. Le devis de la frontiere ne connaissait
fabric qu en DESTINATION - un flux entrant tombait dans « rien d autre n
entre » sans rien dire. La regle etait posee sur la patte de la destination
alors que D-61 raisonne en ARRIVEE, et opt7 manquait a la table des zones.
Et le plus grave : fabric etait un mot RECONNU mais NON RESOLU. Source
vide, donc regle sans saddr - tcp dport 3100 accept, ouvert a tous. Un mot
reconnu mais non resolu est pire qu un mot inconnu : celui-ci serait refuse
a la validation, celui-la produit une porte grande ouverte qui a l air d un
flux precis.
LA CLASSE ENTIERE EST REFERMEE. Le defaut n etait pas propre a fabric :
toute paire nommant un ensemble et ne resolvant rien ouvrait le port.
Trouve en lisant les fichiers generes, l API de supervision d un tenant
etait ouverte a tous parce que pair: serveur_backup ne resout rien - cet
ecosysteme depose chez le site. expositions et externe, eux, veulent bien
dire tout le monde : c est une intention. Ailleurs, pas de source, pas de
regle, et le generateur le DIT.
Trois regles se referment, chacune verifiee AVANT d appliquer. Deux etaient
sans effet ; la troisieme portait Grafana, qui ecoute bien sur 3000. Sa
declaration disait pair: edge - juste chez un tenant, faux au site qui n a
pas d edge. Ajout de admin : ce qui etait accidentel devient explicite.
Et la mesure a montre autre chose : Grafana etait DEJA injoignable depuis
le poste, la frontiere ne laissant pas passer le 3000. Le site a une
console deployee et sans chemin d acces. Ce n est pas corrige ici, mais
c est dit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 19:14:23 -04:00
|
|
|
if p == "fabric":
|
|
|
|
|
# LA FABRIC N'EST PAS UN GROUPE D'HOTES, ELLE EST DANS LA CARTE (2026-09-10).
|
|
|
|
|
#
|
|
|
|
|
# `fabric` figurait dans `MOTS_PAIR` — donc accepte comme mot valide — sans
|
|
|
|
|
# aucune branche pour le resoudre. Il tombait dans le cas general, ne trouvait
|
|
|
|
|
# aucun hote, et une source VIDE produit une regle SANS `saddr` :
|
|
|
|
|
#
|
|
|
|
|
# tcp dport 3100 accept # ouvert a tout le monde
|
|
|
|
|
#
|
|
|
|
|
# Un mot reconnu mais non resolu est pire qu'un mot inconnu : celui-ci serait
|
|
|
|
|
# refuse a la validation, celui-la produit une porte grande ouverte qui a
|
|
|
|
|
# l'air d'un flux precis.
|
|
|
|
|
#
|
|
|
|
|
# Les adresses viennent de l'underlay, comme celles du devis de la frontiere :
|
|
|
|
|
# hyperviseurs et frontiere, le materiel qu'aucun agent n'habite.
|
|
|
|
|
continue
|
2026-07-07 03:08:09 -04:00
|
|
|
if p == "flotte":
|
|
|
|
|
noms |= actifs_noms
|
|
|
|
|
else:
|
|
|
|
|
groupe = GROUPE_EDGE if p == "edge" else p
|
|
|
|
|
noms |= {h for h in _hotes_du_groupe(data, groupe) if h in actifs_noms}
|
2026-08-07 17:26:35 -04:00
|
|
|
srcs = {ip_par_hote[h] for h in noms if h in ip_par_hote}
|
2026-08-24 17:15:19 -04:00
|
|
|
demandes = pair if isinstance(pair, list) else [pair]
|
|
|
|
|
if "admin" in demandes:
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
srcs |= set(_sources_admin_ssh_site() if site else _sources_admin_ssh())
|
une source vide n est pas « tout le monde »
Les journaux de la fabric, et le defaut de classe que leur mise en place a
revele. 10 hotes dans Loki (7 VM du site + 3 hyperviseurs), site a 67
services tous OK contre 51 avant.
METRIQUES TIREES, JOURNAUX POUSSES. Un scrape part du collecteur et doit
REVENIR : il exige un chemin symetrique, que la route par defaut gelee
interdit. Un push part de la source et n attend qu un accuse.
Trois trous dans les generateurs. Le devis de la frontiere ne connaissait
fabric qu en DESTINATION - un flux entrant tombait dans « rien d autre n
entre » sans rien dire. La regle etait posee sur la patte de la destination
alors que D-61 raisonne en ARRIVEE, et opt7 manquait a la table des zones.
Et le plus grave : fabric etait un mot RECONNU mais NON RESOLU. Source
vide, donc regle sans saddr - tcp dport 3100 accept, ouvert a tous. Un mot
reconnu mais non resolu est pire qu un mot inconnu : celui-ci serait refuse
a la validation, celui-la produit une porte grande ouverte qui a l air d un
flux precis.
LA CLASSE ENTIERE EST REFERMEE. Le defaut n etait pas propre a fabric :
toute paire nommant un ensemble et ne resolvant rien ouvrait le port.
Trouve en lisant les fichiers generes, l API de supervision d un tenant
etait ouverte a tous parce que pair: serveur_backup ne resout rien - cet
ecosysteme depose chez le site. expositions et externe, eux, veulent bien
dire tout le monde : c est une intention. Ailleurs, pas de source, pas de
regle, et le generateur le DIT.
Trois regles se referment, chacune verifiee AVANT d appliquer. Deux etaient
sans effet ; la troisieme portait Grafana, qui ecoute bien sur 3000. Sa
declaration disait pair: edge - juste chez un tenant, faux au site qui n a
pas d edge. Ajout de admin : ce qui etait accidentel devient explicite.
Et la mesure a montre autre chose : Grafana etait DEJA injoignable depuis
le poste, la frontiere ne laissant pas passer le 3000. Le site a une
console deployee et sans chemin d acces. Ce n est pas corrige ici, mais
c est dit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 19:14:23 -04:00
|
|
|
if "fabric" in demandes:
|
|
|
|
|
srcs |= set(_adresses_fabric())
|
l heure vient de la frontiere : la derniere dependance vivante tombe
Mesure sur les quatorze : aucune sortie TCP vers une adresse publique, apt
par le cache, noms autoritaires en local, unattended-upgrades masked. Et
quatre pairs NTP publics par machine. Le role chrony posait le fuseau et
installait le demon sans jamais toucher a ses sources : le defaut de Debian
tenait depuis le premier jour, herite et jamais choisi.
L autorite est la frontiere, par decision de l exploitant. Elle etait deja
stratum 2 et ecoutait en 123 ; il ne manquait que le passage. 9 anciennes
regles port 123 vers !SETOPS_INTERNES retirees, 9 regles nommees vers
SETOPS_FRONTIERE posees : le changement resserre autant qu il centralise.
Deux chemins parce que la topologie en a deux. Un tenant n atteint pas la
frontiere par sa passerelle de zone — tenue par le SDN — mais par le lien de
transit. Une machine du site a la frontiere pour passerelle directe. Les deux
valeurs sont derivees de l underlay, via reseau_transit() plutot que d un
prefixe d adresse qui aurait menti chez le prochain hebergeur.
La patte face aux tenants manquait a opnsense_if_zones, pour la meme raison
que grappe-controle la veille.
Sonde horloge ecrite en meme temps : synchronisee ET contre la source
DECLAREE. Une machine peut etre parfaitement a l heure contre quatre serveurs
publics — c est exactement l etat d avant.
14 tenant + 7 site, toutes disciplinees, sources publiques = 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-11 12:08:13 -04:00
|
|
|
if "frontiere" in demandes:
|
|
|
|
|
srcs |= set(_adresses_frontiere())
|
2026-08-24 17:15:19 -04:00
|
|
|
if "voisins_site" in demandes:
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
# DU COTE DU SITE, « VOISIN » N'EXCLUT PERSONNE (mesure du 2026-09-02).
|
|
|
|
|
#
|
|
|
|
|
# `_supernets_voisins()` retire l'instance montee : c'est juste chez un TENANT,
|
|
|
|
|
# ou l'on declare des flux vers ses voisins et ou nul n'est son propre voisin.
|
|
|
|
|
# Applique au SITE, ce filtre retire le locataire qu'on pilote au moment ou l'on
|
|
|
|
|
# genere — c'est-a-dire, presque toujours, celui a qui l'on tient le plus.
|
|
|
|
|
#
|
|
|
|
|
# CE QUE CA AURAIT COUTE : le jeu de regles produit pour `site-dns-01` autorisait
|
|
|
|
|
# 10.23.0.0/16 et 10.29.0.0/16 sur le port 53, et PAS 10.17.0.0/16. Chezlepro
|
|
|
|
|
# resout encore sur le resolveur du site : l'appliquer aurait prive de DNS les
|
|
|
|
|
# quinze machines qu'on venait de reconstruire. Le defaut a ete vu en LISANT le
|
|
|
|
|
# fichier avant de le poser, pas en le posant.
|
|
|
|
|
#
|
|
|
|
|
# C'est le meme piege que `site_inventaire` documente pour les ACL du resolveur,
|
|
|
|
|
# et il s'est retendu ici parce que la regle est portee par la fonction, pas par
|
|
|
|
|
# le lieu qui l'appelle. On prend donc la decouverte SANS son filtre : le site
|
|
|
|
|
# sert tous ceux qu'il heberge, y compris l'instance montee.
|
|
|
|
|
if site:
|
|
|
|
|
try:
|
|
|
|
|
import devis_reseau
|
|
|
|
|
from inventory_rules import supernet_de
|
|
|
|
|
srcs |= {supernet_de(int(n["index"]))
|
|
|
|
|
for _nom, _p, n in devis_reseau.decouvrir_du_site()
|
|
|
|
|
if n.get("index") is not None}
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
srcs |= set(_supernets_voisins())
|
insemination : declarer le lien, l'emettre d'un seul cote, et un test rouge
L'insemination avait un nom depuis ce matin ; elle n'avait pas de flux. Deux
declarations, aux deux bouts, et rien d'autre :
serveur_ops_site egress 22/tcp -> serveur_ops_tenant
serveur_ops_tenant ingress 22/tcp <- runner_site partage: true
ETROIT PAR CONSTRUCTION : il vise le GROUPE `serveur_ops_tenant`, qu'un ecosysteme
ne pose que sur une machine. Au socle, il aurait ouvert le SSH du site vers toute
la flotte du tenant.
L'en-tete disait « ce role n'entre JAMAIS chez un tenant ». Frontiere intenable :
`creer-vm` exige `_instance-requise`, et le runner du site avait deja du basculer
son symlink `instance` sur OPS-Chezlepro pour materialiser ses VM. Declarer ne cree
pas ce pouvoir — ca rend limitable un pouvoir qui s'exercait sans borne. Ce qui
reste interdit n'est pas une regle mais un FAIT : il n'a pas la voute du tenant.
LA REGLE EST EMISE D'UN SEUL COTE, et pas celui qu'on croit. Le paquet penetre le
pare-feu par la patte du SITE, pas par le transit : la regle appartient au cote
site du devis. L'emettre aussi depuis l'`ingress` du tenant aurait produit une
seconde regle sur la mauvaise interface — jamais evaluee, indiscernable d'une regle
utile. La declaration du tenant pose sa regle nftables, et elle seule :
ip saddr { 10.0.31.11 } tcp dport 22 accept
L'adresse DERIVE du plan du site. Ecrite a la main, elle aurait survecu au prochain
deplacement du runner sans bruit — le site a deja deplace ses machines le 08-25.
Plan de la frontiere : 2 objets a creer, 0 a retirer, 126 inchanges. RIEN D'APPLIQUE.
P41 APPLIQUEE AU PLAN DU SITE : `resoudre_flux` en avait besoin a son tour ; les
trois lecteurs demenagent dans `underlay` et `devis_opnsense` delegue.
DEUX GARDES ONT TRAVAILLE : P33 a refuse `ingress 22` sur un hote portant deja le
sshd du socle (reponse : `partage: true`, comme `serveur_backup`), et le devis a
refuse d'emettre vers un alias vide.
UN TEST ROUGE DEPUIS TROIS JOURS. `test_adressage_derive` construisait un site avec
un `index` — or un SITE n'en a pas depuis add94f2 (08-25), remplace par
`bande_basse_de`. Invisible parce que le geste quotidien est `make prouver`, qui ne
joue pas les tests. Remis sur le contrat actuel, avec sa contrepartie : sans
`bande_basse_de`, aucun chevauchement n'est tolere.
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 13:09:28 -04:00
|
|
|
if "runner_site" in demandes:
|
|
|
|
|
srcs |= set(_adresses_runner_site())
|
2026-08-07 17:26:35 -04:00
|
|
|
return sorted(srcs)
|
2026-07-07 03:08:09 -04:00
|
|
|
|
|
|
|
|
|
insemination : declarer le lien, l'emettre d'un seul cote, et un test rouge
L'insemination avait un nom depuis ce matin ; elle n'avait pas de flux. Deux
declarations, aux deux bouts, et rien d'autre :
serveur_ops_site egress 22/tcp -> serveur_ops_tenant
serveur_ops_tenant ingress 22/tcp <- runner_site partage: true
ETROIT PAR CONSTRUCTION : il vise le GROUPE `serveur_ops_tenant`, qu'un ecosysteme
ne pose que sur une machine. Au socle, il aurait ouvert le SSH du site vers toute
la flotte du tenant.
L'en-tete disait « ce role n'entre JAMAIS chez un tenant ». Frontiere intenable :
`creer-vm` exige `_instance-requise`, et le runner du site avait deja du basculer
son symlink `instance` sur OPS-Chezlepro pour materialiser ses VM. Declarer ne cree
pas ce pouvoir — ca rend limitable un pouvoir qui s'exercait sans borne. Ce qui
reste interdit n'est pas une regle mais un FAIT : il n'a pas la voute du tenant.
LA REGLE EST EMISE D'UN SEUL COTE, et pas celui qu'on croit. Le paquet penetre le
pare-feu par la patte du SITE, pas par le transit : la regle appartient au cote
site du devis. L'emettre aussi depuis l'`ingress` du tenant aurait produit une
seconde regle sur la mauvaise interface — jamais evaluee, indiscernable d'une regle
utile. La declaration du tenant pose sa regle nftables, et elle seule :
ip saddr { 10.0.31.11 } tcp dport 22 accept
L'adresse DERIVE du plan du site. Ecrite a la main, elle aurait survecu au prochain
deplacement du runner sans bruit — le site a deja deplace ses machines le 08-25.
Plan de la frontiere : 2 objets a creer, 0 a retirer, 126 inchanges. RIEN D'APPLIQUE.
P41 APPLIQUEE AU PLAN DU SITE : `resoudre_flux` en avait besoin a son tour ; les
trois lecteurs demenagent dans `underlay` et `devis_opnsense` delegue.
DEUX GARDES ONT TRAVAILLE : P33 a refuse `ingress 22` sur un hote portant deja le
sshd du socle (reponse : `partage: true`, comme `serveur_backup`), et le devis a
refuse d'emettre vers un alias vide.
UN TEST ROUGE DEPUIS TROIS JOURS. `test_adressage_derive` construisait un site avec
un `index` — or un SITE n'en a pas depuis add94f2 (08-25), remplace par
`bande_basse_de`. Invisible parce que le geste quotidien est `make prouver`, qui ne
joue pas les tests. Remis sur le contrat actuel, avec sa contrepartie : sans
`bande_basse_de`, aucun chevauchement n'est tolere.
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 13:09:28 -04:00
|
|
|
def _adresses_runner_site() -> list[str]:
|
|
|
|
|
"""L'adresse du runner du SITE — la source du flux d'insemination.
|
|
|
|
|
|
|
|
|
|
DERIVEE DU PLAN DU SITE, jamais ecrite ici. Le site est le seul a savoir ou vit son
|
|
|
|
|
runner, et il l'a deja deplace une fois (decoupage en zones du 2026-08-25). Une
|
|
|
|
|
adresse recopiee ici aurait survecu a ce deplacement en silence, et le tenant se
|
|
|
|
|
serait retrouve avec une regle qui n'autorise personne — la panne la plus muette de
|
|
|
|
|
cette couche.
|
|
|
|
|
|
|
|
|
|
Rend [] quand le depot de l'hebergeur n'est pas monte : un tenant clone sans lui
|
|
|
|
|
degrade proprement, il ne casse pas.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
import underlay as underlay_mod
|
|
|
|
|
return underlay_mod.adresses_site_portant("serveur_ops_site")
|
|
|
|
|
except Exception:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
2026-08-24 17:15:19 -04:00
|
|
|
def _supernets_voisins() -> list[str]:
|
|
|
|
|
"""Les supernets des AUTRES tenants que CE SITE heberge.
|
|
|
|
|
|
|
|
|
|
ON REUTILISE `devis_reseau.decouvrir_du_site()`, on n'en ecrit pas une deuxieme
|
|
|
|
|
version. C'est la lecon de P41 appliquee ici : deux recensements de tenants finiraient
|
|
|
|
|
par diverger, et la divergence se lirait « tout va bien ».
|
|
|
|
|
|
|
|
|
|
Elle porte aussi la bonne semantique. « Voisin » veut dire *sur ma fabric*, pas
|
|
|
|
|
*quelque part dans la federation* : un tenant pose sur un autre site n'est joignable
|
|
|
|
|
par aucune regle de cette frontiere-ci, et lui ouvrir un flux serait un chemin qui ne
|
|
|
|
|
correspondra jamais a un paquet.
|
|
|
|
|
|
|
|
|
|
Elle porte enfin la regle du `federe` ABSENT = federe (devis_reseau, ligne 568). Ma
|
|
|
|
|
premiere version lisait l'absence comme « non federe » et excluait Chezlepro et
|
|
|
|
|
Technolibre en silence — leur nomenclature est anterieure a cette cle.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
import devis_reseau
|
|
|
|
|
from inventory_rules import supernet_de
|
|
|
|
|
tenants = devis_reseau.decouvrir_du_site()
|
|
|
|
|
except Exception:
|
|
|
|
|
# Sans underlay designe (ex. `make flux` hors site), il n'y a pas de voisinage a
|
|
|
|
|
# resoudre. Rendre [] plutot qu'echouer : l'absence de voisins n'est pas une faute.
|
|
|
|
|
return []
|
|
|
|
|
moi = INSTANCE.resolve().name
|
|
|
|
|
return sorted({supernet_de(int(n["index"])) for nom, _p, n in tenants if nom != moi})
|
|
|
|
|
|
|
|
|
|
|
2026-08-06 21:47:54 -04:00
|
|
|
# Codes ICMP de destination inatteignable qu'un flux peut nommer dans `port`. L'ICMP n'a
|
|
|
|
|
# PAS de port : il a un type et un code. Emettre `icmp dport frag-needed` produit un jeu
|
|
|
|
|
# que `nft` refuse — et le service ne demarre plus du tout, donc l'hote perd sa barriere
|
|
|
|
|
# au lieu d'en gagner une. Constate le 2026-08-06 sur les 14 hotes a la fois.
|
|
|
|
|
CODES_ICMP = {
|
|
|
|
|
"frag-needed": "icmp type destination-unreachable icmp code frag-needed",
|
icinga : l hote de supervision saturait par sa propre demonstration
« mon-01 tape dans l fond. » Il tapait : load 5,10 sur 4 coeurs, 8
processus check_disk a 70-99 % de CPU chacun, jusqu a 28 minutes de vie.
check_disk 2.4.0-3+deb13u1 ne rend jamais la main sur cet hote (etat R).
Icinga en relancait un a chaque intervalle pour le service `disk` de son
hote de DEMONSTRATION, et aucun ne mourait.
RETIRE : conf.d/hosts.conf, l hote NodeName livre par le paquet. Les apply
Service s y accrochaient — disk, http, swap, apt, load, procs, users — et
trois etaient rouges en permanence (swap sur une VM sans swap, http sur un
port ou rien n ecoute, apt pour un paquet). On retire l HOTE et non les
services : sans lui les apply ne s accrochent a rien, et on ne touche pas
a un fichier que le paquet remplacera. Ca garde ping4, qui vise nos hotes
et sert vraiment.
avant : load 5,10 — 8 check_disk — 3 alarmes rouges permanentes
apres : load 0,77 — 0 check_disk — certificat 14/14, sante 14/14, ping4 14/14
TROUVE EN VERIFIANT : l Icinga du SITE tenait 6 de ses 7 machines pour
MORTES (1/7 UP, contre 14/14 au tenant). hostalive est un ping, le site
est decoupe en zones, et l ICMP inter-zones n etait declare nulle part —
100 % de perte, mesure. Or Icinga SUPPRIME les notifications des services
d un hote DOWN : une supervision qui croit tout mort n alerte plus de
rien, tout en ayant l air de fonctionner.
Le flux est declare des DEUX cotes, et le registre a refuse la premiere
moitie seule — exactement sa raison d etre. CODES_ICMP apprend
echo-request. Regles d hote posees sur les 21 machines.
RESTE OUVERT : le generateur de la frontiere ne sait pas traduire un TYPE
ICMP pour un pair INTERNE — il le note et n emet rien. Le site reste a
1/7. Corriger devis_opnsense.py est le prochain geste.
make prouver : CONFORME, 64 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-09 22:34:32 -04:00
|
|
|
# `echo-request` est un TYPE, pas un code de destination-unreachable — d'ou l'absence
|
|
|
|
|
# de `icmp code` ici. La supervision s'en sert pour `hostalive` : sans ce flux,
|
|
|
|
|
# l'Icinga du site tenait 6 de ses 7 machines pour mortes, et Icinga SUPPRIME les
|
|
|
|
|
# notifications des services d'un hote DOWN. Une supervision qui croit tout mort
|
|
|
|
|
# n'alerte plus de rien (2026-09-09).
|
|
|
|
|
"echo-request": "icmp type echo-request",
|
2026-08-06 21:47:54 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
flux + postfix : un port symbolique dans le pare-feu, un rechargement qui n applique rien
Le deploiement depuis ops-01 passe de 3 plays a 21 : douze machines sur quinze
deployees completement. Deux defauts restaient.
UN PORT SYMBOLIQUE ECRIT TEL QUEL.
/etc/nftables.conf:36: Could not resolve service: Servname not supported
ip saddr { ... } udp dport derive accept # serveur_powerdns
`port: derive` dit que le port depend du deploiement. Le devis de la frontiere le
resout depuis le 2026-08-25 ; le generateur nftables ecrivait le mot, et nft
refusait TOUT le fichier. Il resout desormais par le plan et n emet rien quand il
ne peut pas, en le DISANT — un flux tu en silence est une porte qu on croit
ouverte. Verifie que ca ne ferme rien : infra-dns-01 garde son 53 par
serveur_resolveur, et l omission de PowerDNS est juste puisqu il ecoute en loopback
derriere lui.
LA VALIDATION NE POSE PAS LA MEME QUESTION QUE L EMISSION. Ma premiere garde
refusait tout port non numerique et a fait echouer P09, qui valide les roles HORS
instance — la ou derive est legitime. PORTS_SYMBOLIQUES nomme le vocabulaire : le
mot passe a la validation, jamais dans un fichier, et un mot inconnu reste refuse
des deux cotes.
RECHARGER N APPLIQUE PAS UN CHANGEMENT D ECOUTE, deuxieme fois.
warning: to change inet_protocols, stop and start Postfix
fatal: :::submission: Address family for hostname not supported
main.cf porte inet_protocols, que Postfix refuse de changer a chaud. Le master garde
all, tente d ouvrir :::submission en IPv6 et meurt — APRES avoir accepte une
configuration valide. postfix check ne dit rien parce que la configuration EST
valide : c est la transition qui ne l est pas. Meme famille que nginx. main.cf
notifie desormais le redemarrage.
Diagnostic corrige en chemin : j ai d abord accuse postfix@-.service, dont
l assertion echouait — c est moi qui l avais declenche par un demarrage manuel,
postfix.service le declare en Conflicts.
make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 13:00:54 -04:00
|
|
|
# LES PORTS QUI NE SONT PAS DES NOMBRES, ET QUI SONT LEGITIMES.
|
|
|
|
|
#
|
|
|
|
|
# `derive` dit « ce port depend du deploiement » : Forgejo ecoute 3000 derriere un edge et
|
|
|
|
|
# 443 quand il sert son propre TLS, PowerDNS 53 seul sur son hote et 5300 en loopback
|
|
|
|
|
# derriere le resolveur. Seul le PLAN sait lequel — le role, lui, ne peut que le nommer.
|
|
|
|
|
#
|
|
|
|
|
# Ce mot est donc VALIDE quand on verifie un role, et INTERDIT quand on emet un fichier :
|
|
|
|
|
# ce sont deux questions differentes, et les confondre coute des deux cotes. Ecrit tel
|
|
|
|
|
# quel dans un ruleset, `nft` refuse TOUT le fichier ; refuse a la validation, un role
|
|
|
|
|
# parfaitement correct devient une faute.
|
|
|
|
|
PORTS_SYMBOLIQUES = {"derive"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _selecteur_nft(protocole: str, port, resolu: bool = True) -> str:
|
2026-08-06 21:47:54 -04:00
|
|
|
"""Selecteur nftables pour un flux : `tcp dport 22`, ou l'equivalent ICMP.
|
|
|
|
|
|
|
|
|
|
Un `port` non numerique sur de l'ICMP designe un code, jamais un port. Un code
|
|
|
|
|
inconnu est refuse ici plutot que de produire un fichier que `nft` rejettera —
|
|
|
|
|
l'erreur se lit au moment de la generation, pas trois deploiements plus tard.
|
|
|
|
|
"""
|
|
|
|
|
p = str(protocole).lower()
|
|
|
|
|
if p in ("icmp", "icmpv6"):
|
|
|
|
|
cle = str(port).strip()
|
|
|
|
|
if cle in CODES_ICMP:
|
|
|
|
|
return CODES_ICMP[cle]
|
|
|
|
|
raise ErreurFlux(
|
|
|
|
|
f"Flux ICMP avec `port: {port}` : l'ICMP n'a pas de port. Nommer un code "
|
|
|
|
|
f"connu ({', '.join(sorted(CODES_ICMP))}) ou etendre CODES_ICMP."
|
|
|
|
|
)
|
flux + postfix : un port symbolique dans le pare-feu, un rechargement qui n applique rien
Le deploiement depuis ops-01 passe de 3 plays a 21 : douze machines sur quinze
deployees completement. Deux defauts restaient.
UN PORT SYMBOLIQUE ECRIT TEL QUEL.
/etc/nftables.conf:36: Could not resolve service: Servname not supported
ip saddr { ... } udp dport derive accept # serveur_powerdns
`port: derive` dit que le port depend du deploiement. Le devis de la frontiere le
resout depuis le 2026-08-25 ; le generateur nftables ecrivait le mot, et nft
refusait TOUT le fichier. Il resout desormais par le plan et n emet rien quand il
ne peut pas, en le DISANT — un flux tu en silence est une porte qu on croit
ouverte. Verifie que ca ne ferme rien : infra-dns-01 garde son 53 par
serveur_resolveur, et l omission de PowerDNS est juste puisqu il ecoute en loopback
derriere lui.
LA VALIDATION NE POSE PAS LA MEME QUESTION QUE L EMISSION. Ma premiere garde
refusait tout port non numerique et a fait echouer P09, qui valide les roles HORS
instance — la ou derive est legitime. PORTS_SYMBOLIQUES nomme le vocabulaire : le
mot passe a la validation, jamais dans un fichier, et un mot inconnu reste refuse
des deux cotes.
RECHARGER N APPLIQUE PAS UN CHANGEMENT D ECOUTE, deuxieme fois.
warning: to change inet_protocols, stop and start Postfix
fatal: :::submission: Address family for hostname not supported
main.cf porte inet_protocols, que Postfix refuse de changer a chaud. Le master garde
all, tente d ouvrir :::submission en IPv6 et meurt — APRES avoir accepte une
configuration valide. postfix check ne dit rien parce que la configuration EST
valide : c est la transition qui ne l est pas. Meme famille que nginx. main.cf
notifie desormais le redemarrage.
Diagnostic corrige en chemin : j ai d abord accuse postfix@-.service, dont
l assertion echouait — c est moi qui l avais declenche par un demarrage manuel,
postfix.service le declare en Conflicts.
make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 13:00:54 -04:00
|
|
|
# UN PORT SYMBOLIQUE NE DOIT JAMAIS ATTEINDRE LE FICHIER (2026-08-30).
|
|
|
|
|
#
|
|
|
|
|
# `port: derive` dit « ce port depend du deploiement ». Il etait ecrit TEL QUEL dans
|
|
|
|
|
# le ruleset, et `nft` le refusait a l'application — donc trois deploiements plus
|
|
|
|
|
# tard, sur la machine, avec un message qui parle de `ai_socktype` :
|
|
|
|
|
#
|
|
|
|
|
# /etc/nftables.conf:36: Error: Could not resolve service: Servname not supported
|
|
|
|
|
# ip saddr { ... } udp dport derive accept # serveur_powerdns
|
|
|
|
|
#
|
|
|
|
|
# L'appelant le resout depuis le plan AVANT d'arriver ici. S'il n'y arrive pas, il
|
|
|
|
|
# n'emet pas de regle — comme le devis OPNsense le fait depuis le 2026-08-25. Cette
|
|
|
|
|
# garde est le dernier filet : elle transforme un fichier casse en erreur de
|
|
|
|
|
# GENERATION, lisible la ou on peut encore la corriger.
|
|
|
|
|
mot = str(port).strip()
|
|
|
|
|
if not mot.isdigit():
|
|
|
|
|
if mot in PORTS_SYMBOLIQUES and not resolu:
|
|
|
|
|
# Validation d'un role, hors instance : le mot est declare, le plan le
|
|
|
|
|
# resoudra. On rend un selecteur de forme, jamais ecrit dans un fichier.
|
|
|
|
|
return f"{p} dport <{mot}>"
|
|
|
|
|
raise ErreurFlux(
|
|
|
|
|
f"Flux {p} avec `port: {port}` : port non numerique"
|
|
|
|
|
+ (" et non resolu par le plan" if mot in PORTS_SYMBOLIQUES
|
|
|
|
|
else f" et inconnu (attendus : un nombre, ou {', '.join(sorted(PORTS_SYMBOLIQUES))})")
|
|
|
|
|
+ ". Le declarer au plan (`applications.yml`, cle `port`) ou ne pas emettre "
|
|
|
|
|
"la regle : ecrit tel quel, `nft` refuse tout le fichier."
|
|
|
|
|
)
|
2026-08-06 21:47:54 -04:00
|
|
|
return f"{p} dport {port}"
|
|
|
|
|
|
|
|
|
|
|
flux + postfix : un port symbolique dans le pare-feu, un rechargement qui n applique rien
Le deploiement depuis ops-01 passe de 3 plays a 21 : douze machines sur quinze
deployees completement. Deux defauts restaient.
UN PORT SYMBOLIQUE ECRIT TEL QUEL.
/etc/nftables.conf:36: Could not resolve service: Servname not supported
ip saddr { ... } udp dport derive accept # serveur_powerdns
`port: derive` dit que le port depend du deploiement. Le devis de la frontiere le
resout depuis le 2026-08-25 ; le generateur nftables ecrivait le mot, et nft
refusait TOUT le fichier. Il resout desormais par le plan et n emet rien quand il
ne peut pas, en le DISANT — un flux tu en silence est une porte qu on croit
ouverte. Verifie que ca ne ferme rien : infra-dns-01 garde son 53 par
serveur_resolveur, et l omission de PowerDNS est juste puisqu il ecoute en loopback
derriere lui.
LA VALIDATION NE POSE PAS LA MEME QUESTION QUE L EMISSION. Ma premiere garde
refusait tout port non numerique et a fait echouer P09, qui valide les roles HORS
instance — la ou derive est legitime. PORTS_SYMBOLIQUES nomme le vocabulaire : le
mot passe a la validation, jamais dans un fichier, et un mot inconnu reste refuse
des deux cotes.
RECHARGER N APPLIQUE PAS UN CHANGEMENT D ECOUTE, deuxieme fois.
warning: to change inet_protocols, stop and start Postfix
fatal: :::submission: Address family for hostname not supported
main.cf porte inet_protocols, que Postfix refuse de changer a chaud. Le master garde
all, tente d ouvrir :::submission en IPv6 et meurt — APRES avoir accepte une
configuration valide. postfix check ne dit rien parce que la configuration EST
valide : c est la transition qui ne l est pas. Meme famille que nginx. main.cf
notifie desormais le redemarrage.
Diagnostic corrige en chemin : j ai d abord accuse postfix@-.service, dont
l assertion echouait — c est moi qui l avais declenche par un demarrage manuel,
postfix.service le declare en Conflicts.
make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 13:00:54 -04:00
|
|
|
def _ports_du_plan() -> dict[str, str]:
|
|
|
|
|
"""Port declare au plan pour chaque groupe — ce que `port: derive` vaut ici.
|
|
|
|
|
|
|
|
|
|
MEME SOURCE QUE LE DEVIS DE LA FRONTIERE (`devis_opnsense._ports_du_plan_site`), pour
|
|
|
|
|
le plan du TENANT au lieu de celui du site. Un service dit `derive` quand son port
|
|
|
|
|
depend du deploiement : Forgejo ecoute 3000 derriere un edge et 443 quand il sert son
|
|
|
|
|
propre TLS. Seul le plan sait lequel.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from inventory_rules import plan_de
|
|
|
|
|
f = Path(plan_de()) / "applications.yml"
|
|
|
|
|
apps = (yaml.safe_load(f.read_text(encoding="utf-8")) or {}).get("applications") or {}
|
|
|
|
|
return {str(a["groupe"]): str(a["port"]) for a in apps.values()
|
|
|
|
|
if a.get("groupe") and a.get("port")}
|
|
|
|
|
except Exception:
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
frontiere posee, et cinq sondes qui disaient faux
Application de ce que l entree precedente avait prepare : 49 regles creees,
5 retirees. Cibles Prometheus 2/8 -> 8/8, hotes dans Loki 1/7 -> 7/7.
Puis correction de ce que l application a revele. Cinq defauts, tous de la
meme famille : un reglage qui ne suit pas l interrupteur dont il depend.
- la sonde de Loki interrogeait en HTTPS un Loki servant en clair ;
- la sonde de la forge visait http:// sur un port 443 chiffre, puis
validait un certificat emis pour le FQDN en interrogeant 127.0.0.1 ;
- la sonde du runner comparait six depots a UNE branche quand le plan en
declare une par depot. Elle contredisait la declaration qu elle etait
censee verifier : elle n accusait pas la machine, elle s accusait
elle-meme.
Le tenant restait juste dans les cinq cas. C est le site, qui deploie ces
roles autrement, qui les a tous reveles d un coup.
La sonde des journaux, elle, avait deux defauts a la naissance. Elle criait
une perte deja reparee, faute de dire sur QUELLE fenetre - un ecart sans sa
fenetre n est pas une mesure, c est un nombre. Et elle alertait sur la
cicatrice plutot que sur la plaie : le compteur d Alloy est cumulatif, donc
six machines restaient orange pour toujours. Ce qui alerte est desormais la
CROISSANCE. En echange, un cas muet leve maintenant : zero envoi et zero
rejet n est pas la sante, c est un agent qui ne fait rien.
Enfin, le runner ne pouvait plus cloner le genome. Defaut PREEXISTANT,
revele par le redeploiement et non cause par lui - le diff des regles
generees le montre. generer_nftables(site=True) lisait le plan du TENANT :
derive se resolvait donc a 3000, le port que Forgejo ecoute derriere un
edge, alors que le plan du site dit 443 et explique pourquoi. La forge
ouvrait un port que personne n ecoute et laissait 443 ferme a toute la
flotte.
Mesure finale : Icinga 51/51, Prometheus 8/8, Loki 7/7, prouver 64 OK,
lint 0 defaut sur 82 fichiers. Controles negatifs sur des COPIES des
sondes : les quatre rendent CRITICAL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 10:27:12 -04:00
|
|
|
def _ports_du_plan_site() -> dict[str, str]:
|
|
|
|
|
"""Port declare par le plan du SITE pour chaque groupe — l'autre moitie de `derive`.
|
|
|
|
|
|
|
|
|
|
LES DEUX PLANS NE DISENT PAS LA MEME CHOSE, ET C'EST TOUT L'INTERET (2026-09-10).
|
|
|
|
|
|
|
|
|
|
`derive` veut dire « ce port depend du deploiement ». Forgejo en est l'exemple meme :
|
|
|
|
|
chez un tenant il ecoute 3000 DERRIERE un edge qui termine le TLS ; au site il n'a pas
|
|
|
|
|
d'edge et sert son propre TLS sur 443. Le plan du site le dit noir sur blanc :
|
|
|
|
|
|
|
|
|
|
forgejo:
|
|
|
|
|
# 443, ET NON 3000. [...] garder 3000 aurait grave `:3000` dans ROOT_URL
|
|
|
|
|
|
|
|
|
|
`generer_nftables(site=True)` interrogeait pourtant le plan du TENANT. La regle d'hote
|
|
|
|
|
de la forge du site ouvrait donc 3000 — un port que personne n'ecoute — et laissait 443
|
|
|
|
|
ferme a toute la flotte du site.
|
|
|
|
|
|
|
|
|
|
CE QUE CA A COUTE : le runner ne pouvait plus cloner le genome. Mesure du 2026-09-10,
|
|
|
|
|
depuis les sept machines, `10.0.33.11:443` etait injoignable de partout SAUF depuis la
|
|
|
|
|
forge elle-meme — y compris depuis une machine du MEME sous-reseau, ce qui excluait la
|
|
|
|
|
frontiere et designait le pare-feu de l'hote. Le tenant, lui, restait juste : son
|
|
|
|
|
Forgejo ecoute bien 3000. Un seul registre pour deux verites opposees.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
import underlay as _u
|
|
|
|
|
apps = (_u.lire_plan_site("applications.yml") or {}).get("applications") or {}
|
|
|
|
|
return {str(a["groupe"]): str(a["port"]) for a in apps.values()
|
|
|
|
|
if a.get("groupe") and a.get("port")}
|
|
|
|
|
except Exception:
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
def _inventaire_site() -> dict:
|
|
|
|
|
"""L'inventaire du SITE, rendu dans la forme qu'attendent les fonctions ci-dessus.
|
|
|
|
|
|
|
|
|
|
Un site n'a pas de `hosts.yml` : son inventaire est un SCRIPT (`site_inventaire.py`),
|
|
|
|
|
parce que sa declaration EST deja sa forme finale — rien n'y est derive d'un index.
|
|
|
|
|
C'est voulu, et documente la-bas.
|
|
|
|
|
|
|
|
|
|
La consequence, elle, ne l'etait pas : tout ce qui lit l'inventaire en YAML ignorait
|
|
|
|
|
purement et simplement les machines du site. `generer_nftables` n'ecrivait donc
|
|
|
|
|
aucune regle pour elles — et comme le pare-feu n'y etait pas non plus applique,
|
|
|
|
|
personne ne pouvait s'en apercevoir. Deux absences qui se cachaient l'une l'autre.
|
|
|
|
|
|
|
|
|
|
On traduit ici la forme dynamique (`{groupe: {hosts: [...]}}` + `_meta.hostvars`)
|
|
|
|
|
vers la forme statique (`all.children.<groupe>.hosts.<hote>`), plutot que de dupliquer
|
|
|
|
|
la logique de generation. Une seule facon de produire des regles pour tout le monde.
|
|
|
|
|
"""
|
|
|
|
|
import site_inventaire
|
|
|
|
|
src = site_inventaire.inventaire()
|
|
|
|
|
hostvars = (src.get("_meta") or {}).get("hostvars") or {}
|
|
|
|
|
enfants: dict = {}
|
|
|
|
|
for groupe, contenu in src.items():
|
|
|
|
|
if groupe == "_meta" or not isinstance(contenu, dict):
|
|
|
|
|
continue
|
|
|
|
|
enfants[groupe] = {"hosts": {h: hostvars.get(h) for h in (contenu.get("hosts") or [])}}
|
|
|
|
|
return {"all": {"children": enfants}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sources_admin_ssh_site() -> list[str]:
|
supervision du site, et la panne qui dormait dans un mot
Le site a son temoin : site-mon-01 (VLAN 36) porte PostgreSQL, Icinga et un
relais. Le depot lui rapporte, et son premier verdict fut un vrai defaut —
site-mon-01 n avait jamais depose son propre etat. Les trois sont au vert.
serveur_backup_verification_locale repasse a true sur le depot : le drapeau ne
dit plus on renonce mais verifie ce que tu peux ouvrir . La liste des noeuds
attendus derive de l inventaire ou tourne le role, donc du site seul.
serveur_postfix gagne un mode relais : un site n heberge aucune boite, il
expedie. Directement par sa frontiere — emprunter le MTA d un locataire ferait
dependre l hebergeur d un ecosysteme qu il peut outvivre.
LA PANNE DE FOND : le modele de routes OPNsense lit enabled ; on lui envoyait
disabled=0, un champ ignore. enabled restait a son defaut, ETEINT. Chaque route
creee par Set-OPS depuis l origine l etait desactivee — invisible, parce qu une
route eteinte EXISTE dans le modele et compte posee . Le rechargement lance
pour activer la nouvelle patte a fait reprendre au noyau sa table depuis le
modele : 14 routes sur 15 disparues, 11 machines de Chezlepro injoignables le
lendemain de sa reconstruction, et le devis toujours vert.
Trois corrections empilees : la cause (enabled), la cecite (le plan lit la table
du NOYAU et signale absent ou eteint), l inaction (la reconfiguration ne se
declenchait que sur un changement du modele).
Aussi : un role du site peut enfin en appeler un autre a travers la frontiere —
le devis traitait tout pair nomme comme l exterieur . Et nftables_admin_ssh se
DERIVE de la carte : recopie a la main, il retardait d une zone, et m a
verrouille dehors de la machine que je venais de creer.
Reste ouvert : le destinataire des alertes est encore root@localhost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 17:34:01 -04:00
|
|
|
"""Par ou l'administration entre sur le SITE — DERIVE de la carte, plus recopie.
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
|
supervision du site, et la panne qui dormait dans un mot
Le site a son temoin : site-mon-01 (VLAN 36) porte PostgreSQL, Icinga et un
relais. Le depot lui rapporte, et son premier verdict fut un vrai defaut —
site-mon-01 n avait jamais depose son propre etat. Les trois sont au vert.
serveur_backup_verification_locale repasse a true sur le depot : le drapeau ne
dit plus on renonce mais verifie ce que tu peux ouvrir . La liste des noeuds
attendus derive de l inventaire ou tourne le role, donc du site seul.
serveur_postfix gagne un mode relais : un site n heberge aucune boite, il
expedie. Directement par sa frontiere — emprunter le MTA d un locataire ferait
dependre l hebergeur d un ecosysteme qu il peut outvivre.
LA PANNE DE FOND : le modele de routes OPNsense lit enabled ; on lui envoyait
disabled=0, un champ ignore. enabled restait a son defaut, ETEINT. Chaque route
creee par Set-OPS depuis l origine l etait desactivee — invisible, parce qu une
route eteinte EXISTE dans le modele et compte posee . Le rechargement lance
pour activer la nouvelle patte a fait reprendre au noyau sa table depuis le
modele : 14 routes sur 15 disparues, 11 machines de Chezlepro injoignables le
lendemain de sa reconstruction, et le devis toujours vert.
Trois corrections empilees : la cause (enabled), la cecite (le plan lit la table
du NOYAU et signale absent ou eteint), l inaction (la reconfiguration ne se
declenchait que sur un changement du modele).
Aussi : un role du site peut enfin en appeler un autre a travers la frontiere —
le devis traitait tout pair nomme comme l exterieur . Et nftables_admin_ssh se
DERIVE de la carte : recopie a la main, il retardait d une zone, et m a
verrouille dehors de la machine que je venais de creer.
Reste ouvert : le destinataire des alertes est encore root@localhost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 17:34:01 -04:00
|
|
|
L'exploitant administre PAR REBOND : la connexion vers une machine du site est
|
|
|
|
|
ouverte par la frontiere, et arrive avec l'adresse de SA PATTE DANS LA ZONE DE LA
|
|
|
|
|
MACHINE VISEE — pas avec celle du poste. Il faut donc la passerelle de chaque zone.
|
|
|
|
|
|
|
|
|
|
POURQUOI DERIVE (paye le 2026-09-02, une heure apres avoir ecrit le contraire).
|
|
|
|
|
Cette liste etait un intrant, ecrit a la main, avec ce commentaire dans le plan :
|
|
|
|
|
« oublier une seule de ces adresses rend une zone entiere inadministrable ». Une zone
|
|
|
|
|
a ete ajoutee le meme jour ; la liste ne l'a pas suivie ; `site-mon-01` s'est
|
|
|
|
|
retrouvee injoignable des l'application de son pare-feu, et il a fallu la rouvrir par
|
|
|
|
|
l'agent invite de l'hyperviseur.
|
|
|
|
|
|
|
|
|
|
Une liste recopiee retarde toujours d'une zone sur la carte. Celle-ci LIT la carte :
|
|
|
|
|
ajouter un reseau au site suffit, il n'y a plus rien a penser a mettre a jour.
|
|
|
|
|
|
|
|
|
|
L'intrant reste lu et AJOUTE a la derivation — un site peut avoir une voie
|
|
|
|
|
d'administration que la carte ne decrit pas (un VPN, un poste sur un lien hors
|
|
|
|
|
fabric). On elargit, on ne remplace pas.
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
"""
|
|
|
|
|
import underlay as underlay_mod
|
supervision du site, et la panne qui dormait dans un mot
Le site a son temoin : site-mon-01 (VLAN 36) porte PostgreSQL, Icinga et un
relais. Le depot lui rapporte, et son premier verdict fut un vrai defaut —
site-mon-01 n avait jamais depose son propre etat. Les trois sont au vert.
serveur_backup_verification_locale repasse a true sur le depot : le drapeau ne
dit plus on renonce mais verifie ce que tu peux ouvrir . La liste des noeuds
attendus derive de l inventaire ou tourne le role, donc du site seul.
serveur_postfix gagne un mode relais : un site n heberge aucune boite, il
expedie. Directement par sa frontiere — emprunter le MTA d un locataire ferait
dependre l hebergeur d un ecosysteme qu il peut outvivre.
LA PANNE DE FOND : le modele de routes OPNsense lit enabled ; on lui envoyait
disabled=0, un champ ignore. enabled restait a son defaut, ETEINT. Chaque route
creee par Set-OPS depuis l origine l etait desactivee — invisible, parce qu une
route eteinte EXISTE dans le modele et compte posee . Le rechargement lance
pour activer la nouvelle patte a fait reprendre au noyau sa table depuis le
modele : 14 routes sur 15 disparues, 11 machines de Chezlepro injoignables le
lendemain de sa reconstruction, et le devis toujours vert.
Trois corrections empilees : la cause (enabled), la cecite (le plan lit la table
du NOYAU et signale absent ou eteint), l inaction (la reconfiguration ne se
declenchait que sur un changement du modele).
Aussi : un role du site peut enfin en appeler un autre a travers la frontiere —
le devis traitait tout pair nomme comme l exterieur . Et nftables_admin_ssh se
DERIVE de la carte : recopie a la main, il retardait d une zone, et m a
verrouille dehors de la machine que je venais de creer.
Reste ouvert : le destinataire des alertes est encore root@localhost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 17:34:01 -04:00
|
|
|
srcs: list[str] = []
|
|
|
|
|
u = underlay_mod.charger() or {}
|
|
|
|
|
for r in (u.get("reseaux") or []):
|
|
|
|
|
# Les zones du SITE seulement : les reseaux de fabric (management, ceph, iscsi)
|
|
|
|
|
# ne portent pas de machines du site et n'ont pas a ouvrir SSH chez elles.
|
|
|
|
|
if str(r.get("nom", "")).startswith("site-") and r.get("passerelle"):
|
|
|
|
|
srcs.append(f"{r['passerelle']}/32")
|
|
|
|
|
# La zone de pilotage en entier : le runner du site y vit, c'est de la qu'il
|
|
|
|
|
# deploie. Une passerelle seule ne suffirait pas a le laisser entrer.
|
|
|
|
|
if str(r.get("nom", "")) == "site-pilotage" and r.get("sous_reseau"):
|
|
|
|
|
srcs.append(str(r["sous_reseau"]))
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
src = (underlay_mod.lire_plan_site("10-intrants.yml") or {}).get("nftables_admin_ssh")
|
supervision du site, et la panne qui dormait dans un mot
Le site a son temoin : site-mon-01 (VLAN 36) porte PostgreSQL, Icinga et un
relais. Le depot lui rapporte, et son premier verdict fut un vrai defaut —
site-mon-01 n avait jamais depose son propre etat. Les trois sont au vert.
serveur_backup_verification_locale repasse a true sur le depot : le drapeau ne
dit plus on renonce mais verifie ce que tu peux ouvrir . La liste des noeuds
attendus derive de l inventaire ou tourne le role, donc du site seul.
serveur_postfix gagne un mode relais : un site n heberge aucune boite, il
expedie. Directement par sa frontiere — emprunter le MTA d un locataire ferait
dependre l hebergeur d un ecosysteme qu il peut outvivre.
LA PANNE DE FOND : le modele de routes OPNsense lit enabled ; on lui envoyait
disabled=0, un champ ignore. enabled restait a son defaut, ETEINT. Chaque route
creee par Set-OPS depuis l origine l etait desactivee — invisible, parce qu une
route eteinte EXISTE dans le modele et compte posee . Le rechargement lance
pour activer la nouvelle patte a fait reprendre au noyau sa table depuis le
modele : 14 routes sur 15 disparues, 11 machines de Chezlepro injoignables le
lendemain de sa reconstruction, et le devis toujours vert.
Trois corrections empilees : la cause (enabled), la cecite (le plan lit la table
du NOYAU et signale absent ou eteint), l inaction (la reconfiguration ne se
declenchait que sur un changement du modele).
Aussi : un role du site peut enfin en appeler un autre a travers la frontiere —
le devis traitait tout pair nomme comme l exterieur . Et nftables_admin_ssh se
DERIVE de la carte : recopie a la main, il retardait d une zone, et m a
verrouille dehors de la machine que je venais de creer.
Reste ouvert : le destinataire des alertes est encore root@localhost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 17:34:01 -04:00
|
|
|
if src:
|
|
|
|
|
srcs += [str(s) for s in src] if isinstance(src, list) else [str(src)]
|
|
|
|
|
return sorted(set(srcs))
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def generer_nftables(flux_par_role: dict[str, list[dict]], site: bool = False) -> list[Path]:
|
|
|
|
|
if site:
|
|
|
|
|
data = _inventaire_site()
|
|
|
|
|
admin_ssh = _sources_admin_ssh_site()
|
|
|
|
|
if not admin_ssh:
|
|
|
|
|
raise ErreurFlux(
|
|
|
|
|
"Le plan du site ne declare pas `nftables_admin_ssh`. Sans lui, les regles "
|
|
|
|
|
"generees refuseraient SSH sur TOUTES les machines du site — et le seul "
|
|
|
|
|
"moyen de les reparer passe par SSH. Declarer l'intrant avant de generer.")
|
|
|
|
|
else:
|
|
|
|
|
data = yaml.safe_load(_inventaire().read_text(encoding="utf-8")) or {}
|
|
|
|
|
admin_ssh = _sources_admin_ssh()
|
2026-07-07 03:08:09 -04:00
|
|
|
actifs_noms = set(_hotes_du_groupe(data, "hotes_actifs"))
|
|
|
|
|
ip_par_hote = _ip_par_hote(data)
|
frontiere posee, et cinq sondes qui disaient faux
Application de ce que l entree precedente avait prepare : 49 regles creees,
5 retirees. Cibles Prometheus 2/8 -> 8/8, hotes dans Loki 1/7 -> 7/7.
Puis correction de ce que l application a revele. Cinq defauts, tous de la
meme famille : un reglage qui ne suit pas l interrupteur dont il depend.
- la sonde de Loki interrogeait en HTTPS un Loki servant en clair ;
- la sonde de la forge visait http:// sur un port 443 chiffre, puis
validait un certificat emis pour le FQDN en interrogeant 127.0.0.1 ;
- la sonde du runner comparait six depots a UNE branche quand le plan en
declare une par depot. Elle contredisait la declaration qu elle etait
censee verifier : elle n accusait pas la machine, elle s accusait
elle-meme.
Le tenant restait juste dans les cinq cas. C est le site, qui deploie ces
roles autrement, qui les a tous reveles d un coup.
La sonde des journaux, elle, avait deux defauts a la naissance. Elle criait
une perte deja reparee, faute de dire sur QUELLE fenetre - un ecart sans sa
fenetre n est pas une mesure, c est un nombre. Et elle alertait sur la
cicatrice plutot que sur la plaie : le compteur d Alloy est cumulatif, donc
six machines restaient orange pour toujours. Ce qui alerte est desormais la
CROISSANCE. En echange, un cas muet leve maintenant : zero envoi et zero
rejet n est pas la sante, c est un agent qui ne fait rien.
Enfin, le runner ne pouvait plus cloner le genome. Defaut PREEXISTANT,
revele par le redeploiement et non cause par lui - le diff des regles
generees le montre. generer_nftables(site=True) lisait le plan du TENANT :
derive se resolvait donc a 3000, le port que Forgejo ecoute derriere un
edge, alors que le plan du site dit 443 et explique pourquoi. La forge
ouvrait un port que personne n ecoute et laissait 443 ferme a toute la
flotte.
Mesure finale : Icinga 51/51, Prometheus 8/8, Loki 7/7, prouver 64 OK,
lint 0 defaut sur 82 fichiers. Controles negatifs sur des COPIES des
sondes : les quatre rendent CRITICAL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 10:27:12 -04:00
|
|
|
ports_plan = _ports_du_plan_site() if site else _ports_du_plan()
|
2026-07-07 03:08:09 -04:00
|
|
|
regles_admin = [
|
|
|
|
|
f" ip saddr {{ {cidr} }} tcp dport 22 accept # administration (garde anti-lockout, intrant nftables_admin_ssh)"
|
|
|
|
|
for cidr in admin_ssh
|
|
|
|
|
]
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
if site:
|
|
|
|
|
# A cote du plan du site, comme sa carte et sa voute : un artefact du site vit
|
|
|
|
|
# chez le site, pas dans l'instance montee au moment ou on l'a genere.
|
|
|
|
|
import underlay as underlay_mod
|
|
|
|
|
sortie_dir = (underlay_mod.chemin().resolve().parent / "flux-genere")
|
|
|
|
|
else:
|
|
|
|
|
sortie_dir = _inventaire().parent.parent.parent / "flux-genere"
|
2026-07-07 03:08:09 -04:00
|
|
|
sortie_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
ecrits: list[Path] = []
|
|
|
|
|
for hote in sorted(actifs_noms):
|
|
|
|
|
groupes = [g for g, membres in _enfants(data).items()
|
|
|
|
|
if hote in (membres.get("hosts") or {}) and est_groupe_operationnel(g)]
|
|
|
|
|
regles: list[str] = list(regles_admin)
|
|
|
|
|
for g in sorted(groupes):
|
|
|
|
|
for fl in flux_par_role.get(g, []):
|
|
|
|
|
if fl.get("sens") != "ingress":
|
|
|
|
|
continue
|
|
|
|
|
ports = fl["port"] if isinstance(fl["port"], list) else [fl["port"]]
|
flux + postfix : un port symbolique dans le pare-feu, un rechargement qui n applique rien
Le deploiement depuis ops-01 passe de 3 plays a 21 : douze machines sur quinze
deployees completement. Deux defauts restaient.
UN PORT SYMBOLIQUE ECRIT TEL QUEL.
/etc/nftables.conf:36: Could not resolve service: Servname not supported
ip saddr { ... } udp dport derive accept # serveur_powerdns
`port: derive` dit que le port depend du deploiement. Le devis de la frontiere le
resout depuis le 2026-08-25 ; le generateur nftables ecrivait le mot, et nft
refusait TOUT le fichier. Il resout desormais par le plan et n emet rien quand il
ne peut pas, en le DISANT — un flux tu en silence est une porte qu on croit
ouverte. Verifie que ca ne ferme rien : infra-dns-01 garde son 53 par
serveur_resolveur, et l omission de PowerDNS est juste puisqu il ecoute en loopback
derriere lui.
LA VALIDATION NE POSE PAS LA MEME QUESTION QUE L EMISSION. Ma premiere garde
refusait tout port non numerique et a fait echouer P09, qui valide les roles HORS
instance — la ou derive est legitime. PORTS_SYMBOLIQUES nomme le vocabulaire : le
mot passe a la validation, jamais dans un fichier, et un mot inconnu reste refuse
des deux cotes.
RECHARGER N APPLIQUE PAS UN CHANGEMENT D ECOUTE, deuxieme fois.
warning: to change inet_protocols, stop and start Postfix
fatal: :::submission: Address family for hostname not supported
main.cf porte inet_protocols, que Postfix refuse de changer a chaud. Le master garde
all, tente d ouvrir :::submission en IPv6 et meurt — APRES avoir accepte une
configuration valide. postfix check ne dit rien parce que la configuration EST
valide : c est la transition qui ne l est pas. Meme famille que nginx. main.cf
notifie desormais le redemarrage.
Diagnostic corrige en chemin : j ai d abord accuse postfix@-.service, dont
l assertion echouait — c est moi qui l avais declenche par un demarrage manuel,
postfix.service le declare en Conflicts.
make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 13:00:54 -04:00
|
|
|
# `derive` se resout par le plan ; sans lui on n'emet RIEN plutot qu'une
|
|
|
|
|
# regle que `nft` refusera — et l'on DIT ce qu'on ne pose pas. Un flux
|
|
|
|
|
# tu en silence est une porte qu'on croit ouverte.
|
|
|
|
|
if str(fl.get("protocole", "")).lower() in ("tcp", "udp"):
|
|
|
|
|
reels = []
|
|
|
|
|
for pt in ports:
|
|
|
|
|
if str(pt).isdigit():
|
|
|
|
|
reels.append(pt)
|
|
|
|
|
continue
|
|
|
|
|
connu = ports_plan.get(g)
|
|
|
|
|
if connu:
|
|
|
|
|
reels.append(connu)
|
|
|
|
|
else:
|
|
|
|
|
print(f"note : {g} declare un port `{pt}` que le plan ne "
|
|
|
|
|
f"resout pas — aucune regle emise pour {hote}.",
|
|
|
|
|
file=sys.stderr)
|
|
|
|
|
if not reels:
|
|
|
|
|
continue
|
|
|
|
|
ports = reels
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
srcs = _resoudre_sources(data, fl.get("pair"), actifs_noms, ip_par_hote, site=site)
|
2026-07-07 03:08:09 -04:00
|
|
|
saddr = "" if not srcs else "ip saddr { " + ", ".join(srcs) + " } "
|
une source vide n est pas « tout le monde »
Les journaux de la fabric, et le defaut de classe que leur mise en place a
revele. 10 hotes dans Loki (7 VM du site + 3 hyperviseurs), site a 67
services tous OK contre 51 avant.
METRIQUES TIREES, JOURNAUX POUSSES. Un scrape part du collecteur et doit
REVENIR : il exige un chemin symetrique, que la route par defaut gelee
interdit. Un push part de la source et n attend qu un accuse.
Trois trous dans les generateurs. Le devis de la frontiere ne connaissait
fabric qu en DESTINATION - un flux entrant tombait dans « rien d autre n
entre » sans rien dire. La regle etait posee sur la patte de la destination
alors que D-61 raisonne en ARRIVEE, et opt7 manquait a la table des zones.
Et le plus grave : fabric etait un mot RECONNU mais NON RESOLU. Source
vide, donc regle sans saddr - tcp dport 3100 accept, ouvert a tous. Un mot
reconnu mais non resolu est pire qu un mot inconnu : celui-ci serait refuse
a la validation, celui-la produit une porte grande ouverte qui a l air d un
flux precis.
LA CLASSE ENTIERE EST REFERMEE. Le defaut n etait pas propre a fabric :
toute paire nommant un ensemble et ne resolvant rien ouvrait le port.
Trouve en lisant les fichiers generes, l API de supervision d un tenant
etait ouverte a tous parce que pair: serveur_backup ne resout rien - cet
ecosysteme depose chez le site. expositions et externe, eux, veulent bien
dire tout le monde : c est une intention. Ailleurs, pas de source, pas de
regle, et le generateur le DIT.
Trois regles se referment, chacune verifiee AVANT d appliquer. Deux etaient
sans effet ; la troisieme portait Grafana, qui ecoute bien sur 3000. Sa
declaration disait pair: edge - juste chez un tenant, faux au site qui n a
pas d edge. Ajout de admin : ce qui etait accidentel devient explicite.
Et la mesure a montre autre chose : Grafana etait DEJA injoignable depuis
le poste, la frontiere ne laissant pas passer le 3000. Le site a une
console deployee et sans chemin d acces. Ce n est pas corrige ici, mais
c est dit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 19:14:23 -04:00
|
|
|
_pairs_fl = fl.get("pair") if isinstance(fl.get("pair"), list) else [fl.get("pair")]
|
|
|
|
|
local = "localhost" in _pairs_fl
|
|
|
|
|
# UNE SOURCE VIDE N'EST PAS « TOUT LE MONDE » (mesure du 2026-09-10).
|
|
|
|
|
#
|
|
|
|
|
# Une paire qui NOMME un ensemble — un role, `flotte`, `fabric`, `edge` —
|
|
|
|
|
# et qui ne resout rien decrivait jusqu'ici une regle SANS `saddr`, donc
|
|
|
|
|
# ouverte a l'Internet. Le silence de la resolution devenait une porte.
|
|
|
|
|
#
|
|
|
|
|
# CE QUE CA A COUTE, trouve en lisant les fichiers generes : sur le
|
|
|
|
|
# `mon-01` d'un tenant,
|
|
|
|
|
#
|
|
|
|
|
# tcp dport 5665 accept # serveur_icinga
|
|
|
|
|
#
|
|
|
|
|
# L'API de supervision ouverte a tous, parce que `pair: serveur_backup` ne
|
|
|
|
|
# resout rien — cet ecosysteme depose son etat chez le site et n'a pas de
|
|
|
|
|
# depot a lui. La regle avait l'air d'un flux precis.
|
|
|
|
|
#
|
|
|
|
|
# `expositions` et `externe`, EUX, veulent bien dire « tout le monde » :
|
|
|
|
|
# ce sont des services publies, et leur ouverture est une intention, pas un
|
|
|
|
|
# accident. On ne les touche pas.
|
|
|
|
|
#
|
|
|
|
|
# Ailleurs : pas de source, pas de regle — et on le DIT, parce qu'un flux
|
|
|
|
|
# tu en silence est une porte qu'on croit ouverte (ou fermee).
|
|
|
|
|
_public = {"expositions", "externe"} & set(_pairs_fl)
|
|
|
|
|
if not srcs and not local and not _public:
|
|
|
|
|
print(f"note : {g} declare un flux entrant depuis "
|
|
|
|
|
f"`{'/'.join(str(x) for x in _pairs_fl)}` qui ne resout aucune "
|
|
|
|
|
f"source — aucune regle emise pour {hote} (une source vide "
|
|
|
|
|
f"ouvrirait le port a tous).", file=sys.stderr)
|
|
|
|
|
continue
|
2026-07-07 03:08:09 -04:00
|
|
|
for port in ports:
|
|
|
|
|
if local and not srcs:
|
|
|
|
|
continue # lo deja accepte
|
2026-08-06 21:47:54 -04:00
|
|
|
regles.append(f" {saddr}{_selecteur_nft(fl['protocole'], port)} accept"
|
|
|
|
|
f" # {g}: {fl.get('raison','')}")
|
2026-07-07 03:08:09 -04:00
|
|
|
contenu = _rendre_nft(hote, regles)
|
|
|
|
|
chemin = sortie_dir / f"{hote}.nft"
|
|
|
|
|
chemin.write_text(contenu, encoding="utf-8")
|
|
|
|
|
ecrits.append(chemin)
|
2026-09-13 20:03:58 -04:00
|
|
|
# UN HOTE RETIRE DU PLAN LAISSAIT SON PARE-FEU DERRIERE LUI (2026-09-13).
|
|
|
|
|
#
|
|
|
|
|
# Cette boucle ECRIVAIT sans jamais RETIRER. En retirant `backup-01` du plan d'un
|
|
|
|
|
# locataire, son `backup-01.nft` est reste sur le disque — un ruleset complet pour une
|
|
|
|
|
# machine qui n'existe plus, impossible a distinguer des autres au premier coup d'oeil.
|
|
|
|
|
#
|
|
|
|
|
# CE QUE CA COUTE : `flux-genere/` cesse d'etre une IMAGE du plan pour devenir un
|
|
|
|
|
# cumul de tous les plans successifs. Quiconque lit le dossier pour savoir ce que
|
|
|
|
|
# l'ecosysteme expose lit alors un etat qui n'a jamais existe.
|
|
|
|
|
#
|
|
|
|
|
# C'est le meme patron que les neuf resolutions d'instance et que les listes qui se
|
|
|
|
|
# suivent : un dossier GENERE doit etre le reflet exact de sa source, donc la
|
|
|
|
|
# generation doit aussi SUPPRIMER. La garde est posee en meme temps que le remede —
|
|
|
|
|
# **P53** voyait deja l'orphelin, puisqu'un ruleset perime n'a pas de refus audible.
|
|
|
|
|
for orphelin in sorted(sortie_dir.glob("*.nft")):
|
|
|
|
|
if orphelin not in ecrits:
|
|
|
|
|
orphelin.unlink()
|
|
|
|
|
print(f"note : {orphelin.name} retire — cet hote n'est plus au plan.",
|
|
|
|
|
file=sys.stderr)
|
2026-07-07 03:08:09 -04:00
|
|
|
return ecrits
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _rendre_nft(hote: str, regles: list[str]) -> str:
|
|
|
|
|
corps = "\n".join(regles) if regles else " # (aucun flux ingress inter-noeud)"
|
|
|
|
|
return (
|
|
|
|
|
f"#!/usr/sbin/nft -f\n"
|
|
|
|
|
f"# GENERE par scripts/resoudre_flux.py depuis les meta/flux.yml — hote {hote}.\n"
|
portabilite : le repli nftables survivait a la bascule et annulait tout
Le socle pose l'un de DEUX fichiers dans /etc/nftables.conf : le ruleset
derive (table setops_flux) s'il existe, sinon un gabarit de repli plat (table
setops_filter) qui n'ouvre que le 22. Ce sont des ALTERNATIVES, jamais des
couches.
Mais le rechargement est `nft -f`, qui AJOUTE sans purger, et le fichier
derive retirait setops_flux — jamais setops_filter. Un hote passe du repli au
derive gardait donc DEUX chaines input sur le meme hook, toutes deux en
policy drop. Le paquet traverse les deux : seule l'intersection de leurs
accept passait.
Symptome parfaitement trompeur : AC debout, 8443 en ecoute, regle posee et
acceptante, ICMP entre les deux VM a 0,15 ms — et step ca bootstrap qui
expire. Il a fallu lire le ruleset entier pour voir la seconde table.
Le fichier derive retire desormais les DEUX tables (le repli portait deja
flush ruleset). Pas de flush ruleset cote derive : choix du depot pour ne pas
detruire de tables etrangeres, respecte.
Ce qui l'avait rendu possible : `make reconstruire` ne generait JAMAIS les
flux. `make flux` en est maintenant la premiere etape.
Et `no_log` a masque la cause trois fois dans la journee — dont au terme d'un
deploiement de 157 taches. Les taches concernees extraient desormais le
verdict a part (ce qui a echoue, quel code, quel message) sans toucher aux
en-tetes. Une garde qui protege un secret ne doit pas emporter le diagnostic.
Verifie : TLS OK depuis infra-dns-01 vers l'AC, une seule table nftables,
13/14 hotes deployes sans echec, prouver.py 34 OK, ansible-lint production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:24:41 -04:00
|
|
|
f"# Deploye par le role `nftables_baseline` en tant que /etc/nftables.conf.\n\n"
|
|
|
|
|
f"# Remplace UNIQUEMENT les tables setops (pas de `flush ruleset` : préserve les\n"
|
|
|
|
|
f"# tables etrangeres — DNAT/forward de conteneurs, par exemple).\n"
|
|
|
|
|
f"#\n"
|
|
|
|
|
f"# `setops_filter` est le gabarit de REPLI, pose quand aucun ruleset resolu\n"
|
|
|
|
|
f"# n'existe encore. Les deux fichiers sont des ALTERNATIVES, jamais des couches —\n"
|
|
|
|
|
f"# mais `nft -f` AJOUTE sans purger, et le repli survivait donc a la bascule.\n"
|
|
|
|
|
f"# Deux chaines `input` sur le meme hook, toutes deux en `policy drop` : le\n"
|
|
|
|
|
f"# paquet traverse les DEUX, et seule l'intersection de leurs `accept` passait.\n"
|
|
|
|
|
f"# Mesure du 2026-08-10 sur Technolibre : l'AC etait debout, sa regle 8443 posee\n"
|
|
|
|
|
f"# et acceptante, l'ICMP passait en 0,15 ms — et le TCP expirait, arrete par le\n"
|
|
|
|
|
f"# `policy drop` du repli. Chaque fichier retire donc desormais la table de\n"
|
|
|
|
|
f"# l'autre, et la bascule converge dans les deux sens.\n"
|
2026-07-07 06:45:12 -04:00
|
|
|
f"table inet setops_flux {{}}\n"
|
portabilite : le repli nftables survivait a la bascule et annulait tout
Le socle pose l'un de DEUX fichiers dans /etc/nftables.conf : le ruleset
derive (table setops_flux) s'il existe, sinon un gabarit de repli plat (table
setops_filter) qui n'ouvre que le 22. Ce sont des ALTERNATIVES, jamais des
couches.
Mais le rechargement est `nft -f`, qui AJOUTE sans purger, et le fichier
derive retirait setops_flux — jamais setops_filter. Un hote passe du repli au
derive gardait donc DEUX chaines input sur le meme hook, toutes deux en
policy drop. Le paquet traverse les deux : seule l'intersection de leurs
accept passait.
Symptome parfaitement trompeur : AC debout, 8443 en ecoute, regle posee et
acceptante, ICMP entre les deux VM a 0,15 ms — et step ca bootstrap qui
expire. Il a fallu lire le ruleset entier pour voir la seconde table.
Le fichier derive retire desormais les DEUX tables (le repli portait deja
flush ruleset). Pas de flush ruleset cote derive : choix du depot pour ne pas
detruire de tables etrangeres, respecte.
Ce qui l'avait rendu possible : `make reconstruire` ne generait JAMAIS les
flux. `make flux` en est maintenant la premiere etape.
Et `no_log` a masque la cause trois fois dans la journee — dont au terme d'un
deploiement de 157 taches. Les taches concernees extraient desormais le
verdict a part (ce qui a echoue, quel code, quel message) sans toucher aux
en-tetes. Une garde qui protege un secret ne doit pas emporter le diagnostic.
Verifie : TLS OK depuis infra-dns-01 vers l'AC, une seule table nftables,
13/14 hotes deployes sans echec, prouver.py 34 OK, ansible-lint production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:24:41 -04:00
|
|
|
f"delete table inet setops_flux\n"
|
|
|
|
|
f"table inet setops_filter {{}}\n"
|
|
|
|
|
f"delete table inet setops_filter\n\n"
|
2026-07-07 03:08:09 -04:00
|
|
|
f"table inet setops_flux {{\n"
|
|
|
|
|
f" chain input {{\n"
|
|
|
|
|
f" type filter hook input priority 0; policy drop;\n\n"
|
|
|
|
|
f" iif \"lo\" accept\n"
|
|
|
|
|
f" ct state established,related accept\n"
|
|
|
|
|
f" ct state invalid drop\n"
|
|
|
|
|
f" ip protocol icmp accept\n"
|
|
|
|
|
f" ip6 nexthdr icmpv6 accept\n\n"
|
flux : l interne refuse a voix haute, la bordure se tait (P53)
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
2026-08-28 16:43:22 -04:00
|
|
|
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"
|
2026-07-07 03:08:09 -04:00
|
|
|
f" }}\n\n"
|
flux : l interne refuse a voix haute, la bordure se tait (P53)
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
2026-08-28 16:43:22 -04:00
|
|
|
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"
|
2026-07-07 06:45:12 -04:00
|
|
|
f" chain forward {{\n"
|
|
|
|
|
f" type filter hook forward priority 0; policy drop;\n"
|
|
|
|
|
f" ct state established,related accept\n"
|
|
|
|
|
f" iifname \"docker0\" accept\n"
|
|
|
|
|
f" oifname \"docker0\" accept\n"
|
|
|
|
|
f" }}\n"
|
2026-07-07 03:08:09 -04:00
|
|
|
f" chain output {{ type filter hook output priority 0; policy accept; }}\n"
|
|
|
|
|
f"}}\n"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
|
|
|
commande = argv[0] if argv else "registre"
|
|
|
|
|
try:
|
|
|
|
|
flux = charger_flux()
|
|
|
|
|
if commande == "verifier":
|
|
|
|
|
r, t = valider(flux)
|
2026-08-06 21:47:54 -04:00
|
|
|
# Chaque flux doit produire un selecteur nftables VALIDE. Le verifier ici, et
|
|
|
|
|
# pas seulement a la generation, ferme le cas ou un role declare un flux que
|
|
|
|
|
# personne ne porte encore : le devis passerait, et la panne arriverait le
|
|
|
|
|
# jour ou un hote prend ce role. `nft -c` local demanderait des privileges
|
|
|
|
|
# (netlink) ; construire le selecteur ne coute rien et attrape la meme faute.
|
|
|
|
|
for role, liste in sorted(flux.items()):
|
|
|
|
|
for fl in liste:
|
|
|
|
|
ports = fl["port"] if isinstance(fl["port"], list) else [fl["port"]]
|
|
|
|
|
for port in ports:
|
|
|
|
|
try:
|
flux + postfix : un port symbolique dans le pare-feu, un rechargement qui n applique rien
Le deploiement depuis ops-01 passe de 3 plays a 21 : douze machines sur quinze
deployees completement. Deux defauts restaient.
UN PORT SYMBOLIQUE ECRIT TEL QUEL.
/etc/nftables.conf:36: Could not resolve service: Servname not supported
ip saddr { ... } udp dport derive accept # serveur_powerdns
`port: derive` dit que le port depend du deploiement. Le devis de la frontiere le
resout depuis le 2026-08-25 ; le generateur nftables ecrivait le mot, et nft
refusait TOUT le fichier. Il resout desormais par le plan et n emet rien quand il
ne peut pas, en le DISANT — un flux tu en silence est une porte qu on croit
ouverte. Verifie que ca ne ferme rien : infra-dns-01 garde son 53 par
serveur_resolveur, et l omission de PowerDNS est juste puisqu il ecoute en loopback
derriere lui.
LA VALIDATION NE POSE PAS LA MEME QUESTION QUE L EMISSION. Ma premiere garde
refusait tout port non numerique et a fait echouer P09, qui valide les roles HORS
instance — la ou derive est legitime. PORTS_SYMBOLIQUES nomme le vocabulaire : le
mot passe a la validation, jamais dans un fichier, et un mot inconnu reste refuse
des deux cotes.
RECHARGER N APPLIQUE PAS UN CHANGEMENT D ECOUTE, deuxieme fois.
warning: to change inet_protocols, stop and start Postfix
fatal: :::submission: Address family for hostname not supported
main.cf porte inet_protocols, que Postfix refuse de changer a chaud. Le master garde
all, tente d ouvrir :::submission en IPv6 et meurt — APRES avoir accepte une
configuration valide. postfix check ne dit rien parce que la configuration EST
valide : c est la transition qui ne l est pas. Meme famille que nginx. main.cf
notifie desormais le redemarrage.
Diagnostic corrige en chemin : j ai d abord accuse postfix@-.service, dont
l assertion echouait — c est moi qui l avais declenche par un demarrage manuel,
postfix.service le declare en Conflicts.
make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 13:00:54 -04:00
|
|
|
# `resolu=False` : on verifie un ROLE, pas un fichier. Un
|
|
|
|
|
# port symbolique y est legitime — c'est le plan qui le
|
|
|
|
|
# resoudra, et `generer_nftables` refuse s'il n'y arrive pas.
|
|
|
|
|
_selecteur_nft(fl.get("protocole", "tcp"), port, resolu=False)
|
2026-08-06 21:47:54 -04:00
|
|
|
except ErreurFlux as e:
|
|
|
|
|
raise ErreurFlux(f"{role} : {e}") from e
|
2026-07-07 03:08:09 -04:00
|
|
|
print(f"Flux coherents : {r} rôles, {t} flux, schéma + matrice OK.")
|
|
|
|
|
elif commande == "registre":
|
|
|
|
|
valider(flux)
|
|
|
|
|
REGISTRE.write_text(generer_registre(flux), encoding="utf-8")
|
|
|
|
|
print(f"{REGISTRE.relative_to(RACINE)} (re)généré depuis les meta/flux.yml.")
|
|
|
|
|
elif commande == "nftables":
|
|
|
|
|
valider(flux)
|
|
|
|
|
ecrits = generer_nftables(flux)
|
|
|
|
|
print(f"{len(ecrits)} aperçu(s) nftables générés (NON activés) :")
|
|
|
|
|
for p in ecrits:
|
|
|
|
|
print(f" {p}")
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
elif commande == "nftables-site":
|
|
|
|
|
valider(flux)
|
|
|
|
|
ecrits = generer_nftables(flux, site=True)
|
|
|
|
|
print(f"{len(ecrits)} aperçu(s) nftables du SITE générés (NON activés) :")
|
|
|
|
|
for p in ecrits:
|
|
|
|
|
print(f" {p}")
|
2026-07-07 03:08:09 -04:00
|
|
|
else:
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
print(f"Commande inconnue : {commande} "
|
|
|
|
|
"(registre | nftables | nftables-site | verifier)", file=sys.stderr)
|
2026-07-07 03:08:09 -04:00
|
|
|
return 2
|
|
|
|
|
except ErreurFlux as e:
|
|
|
|
|
print(f"ERREUR flux :\n{e}", file=sys.stderr)
|
|
|
|
|
return 1
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main(sys.argv[1:]))
|