From ea67b75a15cc4fb108a847cd8cfbb6f6f3421f17 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Tue, 25 Aug 2026 13:57:48 -0400 Subject: [PATCH] dns : le site resout chez lui, et le socle cesse de le defaire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mise au point de l'exploitant : la frontiere n'a pas de service DNS actif et gere. Un Unbound y tourne — il repondait, ce qui m'a induit en erreur — mais un processus n'est pas un service. La delegation de zone que j'y avais posee est retiree. Et le site a son propre DNS : `dns_amorcage` pointait encore sur la frontiere, valeur d'un moment ou site-dns-01 n'existait pas. Elle vaut desormais 10.0.3.51. Deux defauts revelés au passage : - devis_opnsense lisait encore underlay.machines(), vide depuis le deplacement du plan. Il proposait de RETIRER 36 objets — tous les alias et regles du site. Aucune preuve ne couvre ce devis : c'est le plan avant application qui l'a attrape. - le socle defaisait la bascule de client_resolveur a chaque passage. Sa garde ne protegeait que l'hote du resolveur (127.0.0.1). Invisible chez un tenant, ou dns_amorcage vaut l'adresse du resolveur : la coincidence masquait le defaut. Ca ne s'est vu qu'en retirant la regle de pare-feu — les cinq machines ont perdu la resolution d'un coup. L'amorcage ne s'applique plus que si rien de sense n'est en place. Verifie : socle `changed=0`, les cinq machines restent sur 10.0.3.51. 42 preuves vertes. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 38 ++++++++++++++++++++++++ docs/audit/preuve-2026-08-25.md | 2 +- playbooks/groupes/serveur_debian.yml | 28 +++++++++++++++++- scripts/devis_opnsense.py | 43 ++++++++++++++++++++++++++-- 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f8f41..d4ee5c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # CHANGELOG — Set-OPS +## 2026-08-25 — Le site résout chez lui, et le socle cesse de le défaire + +**Mise au point de l'exploitant** : la frontière OPNsense n'a pas de service DNS actif et +géré. Un Unbound y *tourne* — il répondait, ce qui m'a induit en erreur — mais un +processus n'est pas un service. La délégation de zone que j'y avais posée est retirée : +on ne règle pas ce que rien ne déclare ni ne prouve. + +**Et surtout, le site a son propre DNS.** `dns_amorcage` pointait encore sur la frontière +(`10.0.3.1`), valeur d'un moment où `site-dns-01` n'existait pas. Elle a survécu à sa +raison d'être. Il vaut désormais `10.0.3.51`. + +*Reste un cas étroit, nommé plutôt que résolu : la machine qui PORTE le DNS ne peut pas +résoudre chez elle avant de l'avoir installé. Un site bâti depuis zéro doit créer et +déployer `site-dns-01` en premier.* + +### Le devis de la frontière ne voyait plus le site + +En déplaçant le plan hors de `underlay.yml`, j'ai vidé `machines()` sans rebrancher +`devis_opnsense`. Il proposait de **retirer 36 objets** — tous les alias et toutes les +règles du site. Aucune preuve ne couvre le devis de la frontière du site, donc +`make prouver` restait vert : **c'est le plan avant application qui l'a attrapé**, et rien +d'autre ne l'aurait fait. + +### Le socle défaisait la bascule du résolveur + +Sa garde ne protégeait que l'hôte du résolveur (`127.0.0.1`). Partout ailleurs il +réécrivait `/etc/resolv.conf` avec `dns_amorcage`, annulant `client_resolveur` à chaque +passage. + +Invisible chez un tenant : `dns_amorcage` y vaut l'adresse du résolveur du tenant, donc +réécrire remettait la même valeur. **La coïncidence masquait le défaut.** Sur le site les +deux ont divergé quelques heures — et ça ne s'est vu qu'en retirant la règle de pare-feu +vers la frontière : les cinq machines ont perdu la résolution d'un coup. + +L'amorçage ne s'applique plus que si rien de sensé n'est en place : ni la loopback, ni un +résolveur déclaré de l'écosystème. Vérifié — le socle repasse `changed=0` et les cinq +machines restent sur `10.0.3.51`. + ## 2026-08-25 — Le génome vit sur la forge du SITE Six dépôts, 606 commits, chaque tête confrontée entre le poste et la forge : **identique**. diff --git a/docs/audit/preuve-2026-08-25.md b/docs/audit/preuve-2026-08-25.md index 8ae6cbb..215c4f1 100644 --- a/docs/audit/preuve-2026-08-25.md +++ b/docs/audit/preuve-2026-08-25.md @@ -36,7 +36,7 @@ | P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 instance(s) federee(s), aucun index en collision. | | P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). | | P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 8 reseau(x), aucune collision avec la plage tenant. | -| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 65 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,10.29.19.41/32,192.168.254.2/32,192.168.255.2/32. | +| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 86 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,10.29.19.41/32,192.168.254.2/32,192.168.255.2/32. | | P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 50 groupe(s), 79 regle(s). | | P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 5 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). | | P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. | diff --git a/playbooks/groupes/serveur_debian.yml b/playbooks/groupes/serveur_debian.yml index e9a9ea8..84d5e77 100644 --- a/playbooks/groupes/serveur_debian.yml +++ b/playbooks/groupes/serveur_debian.yml @@ -46,10 +46,36 @@ {% for serveur in dns_amorcage.split(',') if serveur | trim %} nameserver {{ serveur | trim }} {% endfor %} + # L'AMORÇAGE NE DOIT PAS DÉFAIRE CE QUI EST DÉJÀ EN PLACE (2026-08-25). + # + # La garde ne protégeait que l'hôte du résolveur lui-même (`127.0.0.1`). Partout + # ailleurs, le socle réécrivait `/etc/resolv.conf` avec `dns_amorcage` — donc + # **défaisait la bascule** de `client_resolveur` à chaque passage. + # + # Ça ne s'était jamais vu chez un tenant : `dns_amorcage` y vaut l'adresse du + # résolveur du tenant, si bien que réécrire remettait la même valeur. La + # coïncidence masquait le défaut. + # + # Sur le site, les deux ont divergé quelques heures — l'amorçage pointait encore sur + # la frontière alors que `client_resolveur` avait basculé les machines sur + # `site-dns-01`. Chaque déploiement du socle les ramenait en arrière, en silence, et + # ça ne s'est vu qu'en retirant la règle de pare-feu vers la frontière : les cinq + # machines ont alors perdu la résolution d'un coup. + # + # On ne pose donc l'amorçage que si rien de sensé n'est déjà là : ni la loopback, ni + # un résolveur déclaré de cet écosystème. + vars: + serveur_debian_resolv_texte: >- + {{ serveur_debian_resolv_actuel.content | default('') | b64decode | string }} + serveur_debian_resolveurs_declares: >- + {{ (groups['serveur_resolveur'] | default([])) + | map('extract', hostvars, 'ansible_host') | select | list }} when: - dns_amorcage is defined - dns_amorcage | string | length > 0 - - "'127.0.0.1' not in (serveur_debian_resolv_actuel.content | default('') | b64decode | string)" + - "'127.0.0.1' not in serveur_debian_resolv_texte" + - serveur_debian_resolveurs_declares + | select('in', serveur_debian_resolv_texte) | list | length == 0 roles: - common_packages diff --git a/scripts/devis_opnsense.py b/scripts/devis_opnsense.py index 54b85e5..e240c13 100644 --- a/scripts/devis_opnsense.py +++ b/scripts/devis_opnsense.py @@ -310,6 +310,35 @@ def _instance_active() -> tuple[str, dict] | tuple[None, None]: return None, None +def _plan_site() -> Path | None: + """Le `plan/` du SITE, a cote de son `underlay.yml`.""" + c = underlay_mod.chemin() + return (c.resolve().parent / "plan") if c else None + + +def _lire_plan_site(nom: str) -> dict: + d = _plan_site() + f = (d / nom) if d else None + if not f or not f.is_file(): + return {} + return yaml.safe_load(f.read_text(encoding="utf-8")) or {} + + +def _machines_du_plan_site() -> list[dict]: + srv = (_lire_plan_site("serveurs.yml") or {}).get("serveurs") or {} + return [{**s, "nom": nom} for nom, s in srv.items() + if str(s.get("etat", "actif")) == "actif"] + + +def _services_du_plan_site() -> dict[str, list[str]]: + apps = (_lire_plan_site("applications.yml") or {}).get("applications") or {} + out: dict[str, list[str]] = {} + for app in apps.values(): + if app.get("hote") and app.get("groupe"): + out.setdefault(str(app["hote"]), []).append(str(app["groupe"])) + return out + + def construire(tenants: list[tuple[str, str, dict]]) -> dict: """Le devis, en structure — rendue ensuite en texte ou en JSON.""" flux_par_role = charger_flux() @@ -558,7 +587,15 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: # Une regle posee sur la mauvaise interface ne correspond jamais — c'est la panne la # plus silencieuse de cette couche. _u = underlay_mod.charger() - _machines_site = underlay_mod.machines(_u) + # LES MACHINES DU SITE VIENNENT DE SON PLAN, plus de l'underlay (2026-08-25). + # + # En deplacant le plan hors de `underlay.yml`, j'ai vide `machines()` sans reprendre + # ici : le devis a cesse de voir le site et a propose de RETIRER ses 36 objets — tous + # ses alias et toutes ses regles. Aucune preuve ne couvre le devis de la frontiere du + # site, donc `make prouver` restait vert. C'est le plan avant application qui l'a + # attrape, et rien d'autre ne l'aurait fait. + _machines_site = _machines_du_plan_site() + _services_site = _services_du_plan_site() if _machines_site: _res_site = {r.get("nom"): r for r in underlay_mod.reseaux(_u)} _sous_site = sorted({str(_res_site[m["reseau"]]["sous_reseau"]) @@ -629,12 +666,12 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: "passerelle : le site n'a pas de resolveur a lui.", }) - _roles_site = sorted({s for m in _machines_site for s in (m.get("services") or [])} + _roles_site = sorted({g for gs in _services_site.values() for g in gs} | {"serveur_debian"}) for _role in _roles_site: _ips = sorted({str(m["ip"]) for m in _machines_site if _role == "serveur_debian" - or _role in (m.get("services") or [])}) + or _role in _services_site.get(m["nom"], [])}) _a = nom_alias(_role, "SITE") alias[_a] = { "type": "host",