site : l'ordre d'un site neuf, le depot avant l'autorite et le cache avant tous

- Couche depot_etat juste apres le socle (serveur_backup, serveur_backup_site) : l'AC, les
  bases et la supervision trouvent un depot qui repond quand elles naissent.
- site-deployer-tout amorce d'abord la machine du cache, seule (socle, serveur_artefacts,
  serveur_cache_site) : aucune couche ne peut exprimer cet ordre par machine.
- verifier_flux_site.py refuse le deploiement si une machine active n'a pas ses regles
  nftables generees ; test_verifier_flux_site.py l'eprouve.

Simulation complete contre le site de TechnoLibre : aucun echec sur les neuf machines.
95 preuves (make verifier), make test passe. CHANGELOG 147.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-10-14 00:31:56 -04:00
parent a9428be752
commit ca731eedc4
6 changed files with 169 additions and 7 deletions

View file

@ -1,3 +1,29 @@
## 2026-10-14 (147) — L'ordre d'un site neuf : le dépôt avant l'autorité, le cache avant tous
**95 preuves.** `make verifier` passe (site de Chezlepro), `make test` passe. Simulation complète
de `site-deployer-tout` contre le site de TechnoLibre avec le nouvel ordre : code 0, aucun échec
sur les neuf machines, trois à quatre changements simulés par machine — le nouvel ordre
n'ajoute rien à un site déjà monté. **Limite** : la preuve qu'il suffit à monter un site neuf
du premier coup viendra de la prochaine construction depuis zéro.
L'entrée 146 avait contourné à la main deux arrêts du premier site construit depuis zéro ;
ils sont désormais dans l'ordre de déploiement lui-même.
- **Couche `depot_etat`, juste après le socle** : `serveur_backup` et `serveur_backup_site`.
L'AC, les bases et la supervision demandent au dépôt, en naissant, si une incarnation
précédente les attend — et refusent, à juste titre, un dépôt illisible. Rangé dans
`services`, il n'existait pas encore. Il ne dépend que du socle ; `orchestrer.py verifier` :
43 groupes, aucun cycle, aucune arête en arrière.
- **L'amorçage du cache dans `site-deployer-tout`** : toute machine du site s'amorce sur le
cache dès son socle, et le cache n'existe qu'après le sien. Aucune couche ne peut le dire
(une couche vaut pour toutes les machines à la fois) : la machine du cache passe d'abord,
seule (`--limit serveur_cache_site`), par son socle, `serveur_artefacts` et
`serveur_cache_site`.
- **`verifier_flux_site.py`, avant tout déploiement du site** : une machine active dont le
pare-feu s'arme sans ses règles générées fait refuser le déploiement, avec `make flux` pour
remède. Sans lui, `nftables_baseline` pose en silence son gabarit générique.
`test_verifier_flux_site.py` l'éprouve (témoin : la machine sans fichier est nommée).
## 2026-10-13 (146) — Le premier site construit depuis zéro : ce que le moteur supposait déjà là
**95 preuves.** `make verifier` passe (site de Chezlepro), `make test` passe. Témoins : l'inventaire

View file

@ -349,6 +349,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
python3 scripts/tests/test_underlay_tunnel.py
python3 scripts/tests/test_face_publique_relais.py
python3 scripts/tests/test_mode_nat_sortant.py
python3 scripts/tests/test_verifier_flux_site.py
python3 scripts/tests/test_frontiere_refus.py
python3 scripts/tests/test_contexte.py
python3 scripts/tests/test_appels_locataire.py
@ -1284,6 +1285,12 @@ fiches: ## Regenere la fiche de chaque role (docs/roles/) — ROLE=<nom> pour un
.PHONY: site-deployer-tout
site-deployer-tout: ansible-runtime ## Deploie TOUT le site dans l'ordre des couches — CONFIRMER=true
@# L'AMORCAGE DU CACHE (2026-10-13). Toute machine du site s'amorce sur le cache des son
@# socle — or le cache n'existe qu'apres le sien. Aucune COUCHE ne peut le dire : une couche
@# vaut pour toutes les machines a la fois. Le premier site construit depuis zero
@# (TechnoLibre) l'a montre ; la machine du cache passe donc d'abord, seule, par son socle
@# et son cache. Sur un site deja monte, ce passage ne change rien. Les regles nftables
@# generees sont exigees avant tout (`verifier_flux_site.py`) : sans elles, repli generique.
@# LE SITE AVAIT L'ORCHESTRATION, PAS LE MOYEN DE LA LANCER (2026-09-14).
@#
@# `playbooks/site.yml` est genere par `orchestrer.py` et ordonne les couches pour
@ -1313,11 +1320,21 @@ site-deployer-tout: ansible-runtime ## Deploie TOUT le site dans l'ordre des cou
exit 2; \
fi; \
python3 scripts/verifier_genome_a_jour.py || exit 2; \
python3 scripts/verifier_flux_site.py || exit 2; \
python3 scripts/orchestrer.py verifier; \
python3 scripts/orchestrer.py ecrire; \
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
mode="$$([[ -n "$(MODE_CHECK)" ]] && printf -- '--check --diff' || true)"; \
cache="$$(python3 scripts/site_inventaire.py --list | python3 -c \
'import json,sys; print(" ".join((json.load(sys.stdin).get("serveur_cache_site") or {}).get("hosts") or []))')"; \
if [[ -n "$$cache" ]]; then \
printf '\n=== Amorcage : le cache du site d abord (%s) ===\n' "$$cache"; \
for g in serveur_debian serveur_durci serveur_artefacts serveur_cache_site; do \
ansible-playbook -i scripts/site_inventaire.py "playbooks/groupes/$$g.yml" \
--limit serveur_cache_site "$${supp[@]}" $$mode $(ARGS); \
done; \
fi; \
ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml \
--limit "$(GROUPE_HOTES_ACTIFS)" "$${supp[@]}" $$mode $(ARGS)

View file

@ -23,6 +23,19 @@ couches:
- serveur_debian
- serveur_durci
# LE DEPOT DE L'ETAT AVANT TOUT CE QUI EN RESTAURE (2026-10-13). L'AC, les bases, la
# supervision demandent au depot « mon incarnation precedente attend-elle ? » au moment de
# naitre — et refusent, a juste titre, un depot ILLISIBLE (conclure « rien a restaurer »
# faute de le joindre ferait naitre neuf ce qui attend). Range dans `services`, le depot
# n'existait pas encore : le premier site construit depuis zero (TechnoLibre) s'est arrete
# sur sa PKI. Il ne depend que du socle.
- nom: depot_etat
raison: "Le depot de sauvegarde : il doit repondre (vide ou non) avant que l'AC, les bases et la supervision ne naissent."
groupes:
- serveur_backup
# Le depot du site, meme nature : il recoit l'etat des locataires et le sien.
- serveur_backup_site
- nom: pki_racine
raison: "L'autorité de certification interne (step_ca) : la racine de confiance de tous les flux TLS est-ouest."
groupes:
@ -75,7 +88,6 @@ couches:
- serveur_rspamd
- serveur_dovecot
- serveur_postfix
- serveur_backup
# La source d'artefacts vient AVANT ceux qui installent des paquets — c'est tout
# son objet. Placee plus tard, elle serait remplie apres avoir servi.
- serveur_artefacts
@ -90,8 +102,6 @@ couches:
- serveur_forge_site
# Le resolveur du site, meme nature : prete aux locataires pendant leur jeunesse.
- serveur_resolveur_site
# Le depot de sauvegarde du site, meme nature : il recoit l'etat des locataires.
- serveur_backup_site
# LE RELAIS (2026-10-13) : la face publique d'un site qui n'a qu'une adresse DHCP. Un
# equipement HORS du site (VPS), inventorie depuis la carte comme un hyperviseur. Il
# vient avant tout ce qui se publie : sans lui, une adresse publique ne mene nulle part.

View file

@ -11,6 +11,12 @@
- name: Couche socle — groupe serveur_durci
import_playbook: groupes/serveur_durci.yml
# --- couche depot_etat ---
- name: Couche depot_etat — groupe serveur_backup
import_playbook: groupes/serveur_backup.yml
- name: Couche depot_etat — groupe serveur_backup_site
import_playbook: groupes/serveur_backup_site.yml
# --- couche pki_racine ---
- name: Couche pki_racine — groupe serveur_step_ca
import_playbook: groupes/serveur_step_ca.yml
@ -50,8 +56,6 @@
import_playbook: groupes/serveur_nginx.yml
- name: Couche services — groupe serveur_rspamd
import_playbook: groupes/serveur_rspamd.yml
- name: Couche services — groupe serveur_backup
import_playbook: groupes/serveur_backup.yml
- name: Couche services — groupe serveur_artefacts
import_playbook: groupes/serveur_artefacts.yml
- name: Couche services — groupe serveur_cache_site
@ -60,8 +64,6 @@
import_playbook: groupes/serveur_forge_site.yml
- name: Couche services — groupe serveur_resolveur_site
import_playbook: groupes/serveur_resolveur_site.yml
- name: Couche services — groupe serveur_backup_site
import_playbook: groupes/serveur_backup_site.yml
- name: Couche services — groupe serveur_relais
import_playbook: groupes/serveur_relais.yml
- name: Couche services — groupe serveur_dns_public

View file

@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Le deploiement du site refuse une machine sans regles nftables generees.
POURQUOI (2026-10-13). Sans `flux-genere/<machine>.nft`, `nftables_baseline` pose en silence
son gabarit generique. Le premier site construit depuis zero n'avait jamais eu `make flux`.
Sur un inventaire SYNTHETIQUE (aucun site requis) :
- toutes les machines ont leur fichier : accepte ;
- une machine sans fichier : refuse, et la nomme (temoin) ;
- une machine qui n'arme pas son pare-feu n'est pas exigee.
"""
from __future__ import annotations
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import verifier_flux_site as V # noqa: E402
def main() -> int:
with tempfile.TemporaryDirectory() as d:
a = Path(d) / "a.nft"
a.write_text("table inet filter {}\n")
inv = {"hotes_actifs": {"hosts": ["a", "b", "c"]},
"_meta": {"hostvars": {
"a": {"nftables_baseline_enabled": True, "nftables_baseline_ruleset_genere": str(a)},
"b": {"nftables_baseline_enabled": True, "nftables_baseline_ruleset_genere": str(Path(d) / "b.nft")},
"c": {"nftables_baseline_enabled": False}}}}
cas = [("une machine sans fichier", inv, ["b"]),
("toutes ont leur fichier", {**inv, "hotes_actifs": {"hosts": ["a", "c"]}}, [])]
echecs = 0
for nom, i, attendu in cas:
vu = [x.split(" :")[0] for x in V.manquants(i)]
ok = vu == attendu
echecs += not ok
print(f"{'OK ' if ok else 'ECHEC '} {nom} : {vu or 'rien a signaler'}")
return 1 if echecs else 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Chaque machine du site a-t-elle ses regles nftables generees, AVANT de deployer ?
POURQUOI (2026-10-13, premier site construit depuis zero). `nftables_baseline` cherche le
jeu de regles DERIVE des flux (`flux-genere/<machine>.nft`, a cote de la carte du site) ;
absent, il pose en silence son GABARIT GENERIQUE, qui ne connait aucun flux reel. Au site de
TechnoLibre, `make flux` n'avait jamais tourne : les neuf machines seraient nees avec un pare-
feu qui refuse leurs propres services, et rien ne l'aurait dit avant le premier refus.
Lit l'inventaire du site (donc `SETOPS_UNDERLAY` s'il est pose) et refuse si une machine
active qui arme son pare-feu n'a pas son fichier. `--inventaire FICHIER` lit un inventaire
deja rendu (JSON de `site_inventaire.py --list`) — c'est ainsi que le test l'eprouve.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
RACINE = Path(__file__).resolve().parents[1]
def manquants(inv: dict) -> list[str]:
hv = (inv.get("_meta") or {}).get("hostvars") or {}
actifs = ((inv.get("hotes_actifs") or {}).get("hosts")) or []
out = []
for h in sorted(actifs):
v = hv.get(h) or {}
if not v.get("nftables_baseline_enabled", False):
continue
f = str(v.get("nftables_baseline_ruleset_genere") or "")
if not f or not Path(f).is_file():
out.append(f"{h} : {f or '(aucun chemin)'}")
return out
def main(argv: list[str]) -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--inventaire", default="")
a = ap.parse_args(argv)
if a.inventaire:
inv = json.loads(Path(a.inventaire).read_text(encoding="utf-8"))
else:
r = subprocess.run([sys.executable, str(RACINE / "scripts" / "site_inventaire.py"),
"--list"], capture_output=True, text=True, cwd=RACINE)
if r.returncode != 0:
print("Inventaire du site illisible :\n" + r.stderr.strip()[:400], file=sys.stderr)
return 2
inv = json.loads(r.stdout)
m = manquants(inv)
if m:
print("Refus : ces machines du site n'ont pas leurs regles nftables generees — leur "
"pare-feu retomberait sur le gabarit generique :", file=sys.stderr)
for x in m:
print(f" - {x}", file=sys.stderr)
print("Lancer `make flux` (avec le meme SETOPS_UNDERLAY), puis relancer.", file=sys.stderr)
return 2
print("OK : chaque machine active du site a ses regles nftables generees.")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))