dns : le site resout chez lui, et le socle cesse de le defaire

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 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-25 13:57:48 -04:00
parent b60946f19f
commit ea67b75a15
4 changed files with 106 additions and 5 deletions

View file

@ -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**.

View file

@ -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. |

View file

@ -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

View file

@ -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",