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:
parent
a9428be752
commit
ca731eedc4
6 changed files with 169 additions and 7 deletions
26
CHANGELOG.md
26
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à
|
## 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
|
**95 preuves.** `make verifier` passe (site de Chezlepro), `make test` passe. Témoins : l'inventaire
|
||||||
|
|
|
||||||
17
Makefile
17
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_underlay_tunnel.py
|
||||||
python3 scripts/tests/test_face_publique_relais.py
|
python3 scripts/tests/test_face_publique_relais.py
|
||||||
python3 scripts/tests/test_mode_nat_sortant.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_frontiere_refus.py
|
||||||
python3 scripts/tests/test_contexte.py
|
python3 scripts/tests/test_contexte.py
|
||||||
python3 scripts/tests/test_appels_locataire.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
|
.PHONY: site-deployer-tout
|
||||||
site-deployer-tout: ansible-runtime ## Deploie TOUT le site dans l'ordre des couches — CONFIRMER=true
|
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).
|
@# 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
|
@# `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; \
|
exit 2; \
|
||||||
fi; \
|
fi; \
|
||||||
python3 scripts/verifier_genome_a_jour.py || exit 2; \
|
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 verifier; \
|
||||||
python3 scripts/orchestrer.py ecrire; \
|
python3 scripts/orchestrer.py ecrire; \
|
||||||
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
voute="$$(dirname "$$(readlink -f "$${SETOPS_UNDERLAY:-underlay.yml}")")/underlay.vault.yml"; \
|
||||||
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
||||||
mode="$$([[ -n "$(MODE_CHECK)" ]] && printf -- '--check --diff' || true)"; \
|
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 \
|
ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml \
|
||||||
--limit "$(GROUPE_HOTES_ACTIFS)" "$${supp[@]}" $$mode $(ARGS)
|
--limit "$(GROUPE_HOTES_ACTIFS)" "$${supp[@]}" $$mode $(ARGS)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,19 @@ couches:
|
||||||
- serveur_debian
|
- serveur_debian
|
||||||
- serveur_durci
|
- 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
|
- nom: pki_racine
|
||||||
raison: "L'autorité de certification interne (step_ca) : la racine de confiance de tous les flux TLS est-ouest."
|
raison: "L'autorité de certification interne (step_ca) : la racine de confiance de tous les flux TLS est-ouest."
|
||||||
groupes:
|
groupes:
|
||||||
|
|
@ -75,7 +88,6 @@ couches:
|
||||||
- serveur_rspamd
|
- serveur_rspamd
|
||||||
- serveur_dovecot
|
- serveur_dovecot
|
||||||
- serveur_postfix
|
- serveur_postfix
|
||||||
- serveur_backup
|
|
||||||
# La source d'artefacts vient AVANT ceux qui installent des paquets — c'est tout
|
# 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.
|
# son objet. Placee plus tard, elle serait remplie apres avoir servi.
|
||||||
- serveur_artefacts
|
- serveur_artefacts
|
||||||
|
|
@ -90,8 +102,6 @@ couches:
|
||||||
- serveur_forge_site
|
- serveur_forge_site
|
||||||
# Le resolveur du site, meme nature : prete aux locataires pendant leur jeunesse.
|
# Le resolveur du site, meme nature : prete aux locataires pendant leur jeunesse.
|
||||||
- serveur_resolveur_site
|
- 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
|
# 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
|
# 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.
|
# vient avant tout ce qui se publie : sans lui, une adresse publique ne mene nulle part.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@
|
||||||
- name: Couche socle — groupe serveur_durci
|
- name: Couche socle — groupe serveur_durci
|
||||||
import_playbook: groupes/serveur_durci.yml
|
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 ---
|
# --- couche pki_racine ---
|
||||||
- name: Couche pki_racine — groupe serveur_step_ca
|
- name: Couche pki_racine — groupe serveur_step_ca
|
||||||
import_playbook: groupes/serveur_step_ca.yml
|
import_playbook: groupes/serveur_step_ca.yml
|
||||||
|
|
@ -50,8 +56,6 @@
|
||||||
import_playbook: groupes/serveur_nginx.yml
|
import_playbook: groupes/serveur_nginx.yml
|
||||||
- name: Couche services — groupe serveur_rspamd
|
- name: Couche services — groupe serveur_rspamd
|
||||||
import_playbook: groupes/serveur_rspamd.yml
|
import_playbook: groupes/serveur_rspamd.yml
|
||||||
- name: Couche services — groupe serveur_backup
|
|
||||||
import_playbook: groupes/serveur_backup.yml
|
|
||||||
- name: Couche services — groupe serveur_artefacts
|
- name: Couche services — groupe serveur_artefacts
|
||||||
import_playbook: groupes/serveur_artefacts.yml
|
import_playbook: groupes/serveur_artefacts.yml
|
||||||
- name: Couche services — groupe serveur_cache_site
|
- name: Couche services — groupe serveur_cache_site
|
||||||
|
|
@ -60,8 +64,6 @@
|
||||||
import_playbook: groupes/serveur_forge_site.yml
|
import_playbook: groupes/serveur_forge_site.yml
|
||||||
- name: Couche services — groupe serveur_resolveur_site
|
- name: Couche services — groupe serveur_resolveur_site
|
||||||
import_playbook: groupes/serveur_resolveur_site.yml
|
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
|
- name: Couche services — groupe serveur_relais
|
||||||
import_playbook: groupes/serveur_relais.yml
|
import_playbook: groupes/serveur_relais.yml
|
||||||
- name: Couche services — groupe serveur_dns_public
|
- name: Couche services — groupe serveur_dns_public
|
||||||
|
|
|
||||||
42
scripts/tests/test_verifier_flux_site.py
Normal file
42
scripts/tests/test_verifier_flux_site.py
Normal 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())
|
||||||
65
scripts/verifier_flux_site.py
Normal file
65
scripts/verifier_flux_site.py
Normal 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:]))
|
||||||
Loading…
Reference in a new issue