diff --git a/CHANGELOG.md b/CHANGELOG.md index 9337002..1da6f3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Makefile b/Makefile index 07fe982..69f2c25 100644 --- a/Makefile +++ b/Makefile @@ -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= 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) diff --git a/docs/couches-deploiement.yml b/docs/couches-deploiement.yml index 2d38502..5b0b945 100644 --- a/docs/couches-deploiement.yml +++ b/docs/couches-deploiement.yml @@ -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. diff --git a/playbooks/site.yml b/playbooks/site.yml index 70398d8..15d493a 100644 --- a/playbooks/site.yml +++ b/playbooks/site.yml @@ -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 diff --git a/scripts/tests/test_verifier_flux_site.py b/scripts/tests/test_verifier_flux_site.py new file mode 100644 index 0000000..003a19a --- /dev/null +++ b/scripts/tests/test_verifier_flux_site.py @@ -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/.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()) diff --git a/scripts/verifier_flux_site.py b/scripts/verifier_flux_site.py new file mode 100644 index 0000000..eed37d1 --- /dev/null +++ b/scripts/verifier_flux_site.py @@ -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/.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:]))