diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da6f3b..b1e91b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +## 2026-10-14 (148) — Le lien entre les sites, et ce qu'il a montré en ne devant rien ouvrir + +**95 preuves.** `make verifier` passe (site de Chezlepro), `make test` passe. Témoin : avec le lien +déclaré dans sa carte, le devis de la frontière de Chezlepro est **identique au caractère près** +à celui d'avant-hier, et son inventaire ne diffère que par le drapeau du cache ; les fiches +remises aux locataires ne changent que par l'empreinte de la carte. + +**Le tunnel entre les sites (phase 1).** L'ancien tunnel `bifrost-1` ↔ site de TechnoLibre était +mort depuis le remplacement du boîtier, et ses adresses autorisées (`10.23.0.0/16`, l'ancien +index du site) recouvraient l'écosystème OPS-Technolibre hébergé à Chezlepro. Refait : +`requin` l'initie (DHCP derrière une traduction d'adresse) vers `69.70.26.62:51820` ; chaque +bout n'admet que l'autre site et les deux primaires DNS des locataires ; routes WireGuard +désactivées des deux côtés (la route `10.23.0.0/16` de `bifrost-1` a disparu) ; interface +`INTERSITES` sans règle sur `requin` — rien ne passe encore. Prouvé : poignée de main des deux +côtés, compteurs qui montent. Déclaré dans les deux cartes (réseau `inter-sites`, rôle d'hôte +`frontiere_pair`). Sur `bifrost-1`, enregistrer l'instance avec un seul champ a vidé son lien +vers le pair (l'API remet à vide les relations absentes) : vu au relevé suivant, rétabli. + +**Une régression de l'entrée 146, attrapée avant toute application.** La valeur VIDE +d'`artefacts_amorcage` (machine du cache) activait la sortie conditionnelle 80/443 de +`serveur_debian` — que le devis ouvre pour tout le groupe, pas pour la machine qui remplit la +condition : toutes les machines du site seraient sorties sur l'Internet. Ni `requin` ni +`bifrost-1` n'ont reçu ce devis. Remplacé par un drapeau à part, `artefacts_amorcage_direct` ; +P79 revient à sa forme stricte. + +**Les pattes de tunnel ne sont pas des pattes de zone.** Le relais puis le lien ont donné à la +frontière des adresses qui devenaient sources admises des journaux et des métriques — en +désaccord avec les règles des locataires. `underlay.reseaux_tunnel()` les écarte des six +dérivations : réseaux de gestion, `SETOPS_FABRIC`, `SETOPS_FRONTIERE`, fabric et frontière dans +`resoudre_flux`, supervision des pattes. Et le devis ne retient plus que les rôles des machines +du PLAN : `serveur_relais`, porté par le seul relais, y faisait un alias vide. + ## 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 diff --git a/playbooks/groupes/serveur_debian.yml b/playbooks/groupes/serveur_debian.yml index cd2dbfa..aa3ed74 100644 --- a/playbooks/groupes/serveur_debian.yml +++ b/playbooks/groupes/serveur_debian.yml @@ -78,18 +78,16 @@ when: - artefacts_amorcage is defined - artefacts_amorcage | string | length > 0 + - not (artefacts_amorcage_direct | default(false) | bool) - # LA MACHINE QUI PORTE LE CACHE VA DROIT CHEZ DEBIAN (2026-10-13). L'inventaire lui - # donne `artefacts_amorcage` VIDE (et non absent) : le fichier herite du gabarit vise - # un cache qui n'existe pas encore — le sien — ou une adresse perimee. Absent, il - # survivrait ; vide, on le retire. + # LA MACHINE QUI PORTE LE CACHE VA DROIT CHEZ DEBIAN (2026-10-13). Le fichier herite du + # gabarit vise un cache qui n'existe pas encore — le sien — ou une adresse perimee : + # l'inventaire le dit par `artefacts_amorcage_direct`, et on le retire. - name: Retirer l'amorcage sur la machine qui porte le cache ansible.builtin.file: path: /etc/apt/apt.conf.d/00-setops-amorcage-artefacts state: absent - when: - - artefacts_amorcage is defined - - artefacts_amorcage | string | length == 0 + when: artefacts_amorcage_direct | default(false) | bool # RETIRER CE QUI DESIGNE UN CACHE QUI N'EXISTE PLUS (2026-09-10). # diff --git a/scripts/devis_opnsense.py b/scripts/devis_opnsense.py index d2f6297..46ce2cb 100644 --- a/scripts/devis_opnsense.py +++ b/scripts/devis_opnsense.py @@ -178,6 +178,8 @@ def reseaux_locaux_frontiere(underlay: dict | None, transit: dict | None) -> lis sur = {str(h.get("reseau")) for h in underlay_mod.hotes(underlay) if str(h.get("nom") or "") == nom} sur.discard(str((transit or {}).get("nom"))) + # Un tunnel arrive par SON interface, jamais par la gestion (relais, lien entre sites). + sur -= underlay_mod.reseaux_tunnel(underlay) cidrs = [] for r in underlay_mod.reseaux(underlay): if str(r.get("nom")) in sur and r.get("sous_reseau"): @@ -907,7 +909,8 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: # par `!SETOPS_INTERNES` (la forme de « vers Internet ») l'aurait exclu tout en # ayant l'air d'ouvrir le flux. D'ou le mot `fabric`, et cet alias-ci. _fabric = sorted({str(h["ip"]) for h in underlay_mod.hotes(_u) - if h.get("role") in ("hyperviseur", "frontiere") and h.get("ip")}) + if h.get("role") in ("hyperviseur", "frontiere") and h.get("ip") + and str(h.get("reseau")) not in underlay_mod.reseaux_tunnel(_u)}) alias["SETOPS_FABRIC"] = { "type": "host", "contenu": _fabric, @@ -1020,7 +1023,12 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: "description": "Autoritatifs des tenants heberges — source du DNS public", } - _roles_site = sorted({g for gs in _services_site.values() for g in gs} + # LES ROLES DES MACHINES DU PLAN, PAS DE TOUT L'INVENTAIRE (2026-10-14). L'inventaire du + # site compte aussi des EQUIPEMENTS hors du plan — les hyperviseurs, le relais —, mais + # les adresses des alias ne viennent que du plan : un role porte par le seul relais + # (`serveur_relais`) donnait un alias VIDE, et le devis refusait de se rendre. + _noms_plan = {str(m["nom"]) for m in _machines_site} + _roles_site = sorted({g for h, gs in _services_site.items() if h in _noms_plan for g in gs} | {"serveur_debian"}) for _role in _roles_site: _ips = sorted({str(m["ip"]) for m in _machines_site @@ -1583,7 +1591,8 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: if _flux_frontiere: _pattes_frontiere = sorted({str(h["ip"]) for h in underlay_mod.hotes(_u) if h.get("role") == "frontiere" and h.get("ip") - and str(h.get("etat", "actif")) != "reserve"}) + and str(h.get("etat", "actif")) != "reserve" + and str(h.get("reseau")) not in underlay_mod.reseaux_tunnel(_u)}) if _pattes_frontiere: alias["SETOPS_FRONTIERE"] = { "type": "host", diff --git a/scripts/prouver.py b/scripts/prouver.py index 451e017..61e0373 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -4079,19 +4079,10 @@ def _repli_hotes_du_site(enfants: dict, hv: dict) -> list[str]: fautes.append(f"rechargement : {h} renouvelle son certificat sans recharger " f"nginx — le nouveau reste sur disque, l'ancien est servi") if membres("serveur_artefacts"): - porteurs = set(membres("serveur_artefacts")) for h, v in sorted(hv.items()): - v = v or {} - if not v.get("dns_amorcage") or v.get("artefacts_amorcage"): - continue - # VIDE SUR LA MACHINE QUI PORTE LE CACHE, ET SEULEMENT LA (2026-10-13). Elle ne - # peut pas s'amorcer sur un cache qu'elle n'a pas encore : le socle RETIRE alors - # le fichier du gabarit (`artefacts_amorcage` defini et vide). Absent, ou vide - # ailleurs, c'est toujours le repli silencieux que cette garde existe pour voir. - if h in porteurs and "artefacts_amorcage" in v: - continue - fautes.append(f"jumeaux : {h} recoit `dns_amorcage` sans " - f"`artefacts_amorcage` — le gabarit garde son ancien proxy apt") + if (v or {}).get("dns_amorcage") and not (v or {}).get("artefacts_amorcage"): + fautes.append(f"jumeaux : {h} recoit `dns_amorcage` sans " + f"`artefacts_amorcage` — le gabarit garde son ancien proxy apt") return fautes diff --git a/scripts/resoudre_flux.py b/scripts/resoudre_flux.py index be695b1..c69f0eb 100644 --- a/scripts/resoudre_flux.py +++ b/scripts/resoudre_flux.py @@ -368,8 +368,11 @@ def _adresses_fabric() -> list[str]: carte = _u.charger() if not carte: return [] + # Meme exclusion que `SETOPS_FABRIC` : une patte de tunnel n'est pas du materiel + # que l'on joint (voir `underlay.reseaux_tunnel`). return sorted({str(h["ip"]) for h in _u.hotes(carte) - if h.get("role") in ("hyperviseur", "frontiere") and h.get("ip")}) + if h.get("role") in ("hyperviseur", "frontiere") and h.get("ip") + and str(h.get("reseau")) not in _u.reseaux_tunnel(carte)}) except Exception: return [] @@ -394,7 +397,8 @@ def _adresses_frontiere() -> list[str]: return [] return sorted({str(h["ip"]) for h in _u.hotes(carte) if h.get("role") == "frontiere" and h.get("ip") - and str(h.get("etat", "actif")) != "reserve"}) + and str(h.get("etat", "actif")) != "reserve" + and str(h.get("reseau")) not in _u.reseaux_tunnel(carte)}) except Exception: return [] diff --git a/scripts/site_inventaire.py b/scripts/site_inventaire.py index 114d52c..fd47279 100755 --- a/scripts/site_inventaire.py +++ b/scripts/site_inventaire.py @@ -611,6 +611,10 @@ def inventaire() -> dict: # est une alarme qu'on cesse de lire. if str(_h.get("etat") or "actif") != "actif": continue + # Une patte de TUNNEL n'est pas une patte de zone : le relais et le lien entre les + # sites se surveillent par leur passerelle (RELAIS_GW), pas comme une zone. + if str(_h.get("reseau") or "") in U.reseaux_tunnel(u): + continue _ip = str(_h.get("ip") or "") _res = str(_h.get("reseau") or "") if not _ip: @@ -981,12 +985,16 @@ def inventaire() -> dict: # LE CACHE NE S'AMORCE PAS SUR LUI-MEME (2026-10-13). Toute machine du site # recevait `artefacts_amorcage` = le cache — y compris le cache, et des le # socle, alors qu'il n'est deploye qu'a la couche `services`. Le premier `apt` - # d'un site construit depuis zero aurait echoue partout ; celui de Chezlepro - # n'a jamais ete reconstruit, son cache existait deja. VIDE ici, et non absent : - # le socle RETIRE alors le fichier du gabarit (qui vise `10.0.33.21`, une adresse - # d'avant le renumerotage) et la machine va droit chez Debian, ce que la - # frontiere permet au cache du site. - **({"artefacts_amorcage": ""} + # d'un site construit depuis zero aurait echoue partout. + # + # UN DRAPEAU A PART, ET NON UNE VALEUR VIDE (corrige le 2026-10-14). Le vide + # activait la sortie conditionnelle 80/443 de `serveur_debian` (`seulement_si: + # artefacts_amorcage non_vide: false`) — et le devis l'ouvre pour TOUT le groupe, + # pas pour la machine qui remplit la condition : toutes les machines du site + # seraient sorties sur l'Internet. Vu au devis avant toute application. Le cache + # garde sa valeur ; ce drapeau dit au socle de RETIRER le fichier du gabarit (qui + # vise `10.0.33.21`, d'avant le renumerotage). Il sort par sa propre regle. + **({"artefacts_amorcage_direct": True} if _cache_site and str(srv.get("ip")) == _cache_site else {}), # En dernier : ce qu'une machine declare d'elle-meme prime sur ce que le # plan declare pour toutes. diff --git a/scripts/underlay.py b/scripts/underlay.py index 4603bd1..aec4864 100644 --- a/scripts/underlay.py +++ b/scripts/underlay.py @@ -434,7 +434,12 @@ def _ports_a_plat(hote: dict) -> list[str]: # `serveur_relais`), et qui porte sa face publique. Le site de TechnoLibre n'a qu'une # adresse DHCP ; ses adresses fixes sont louees sur un VPS et lui arrivent par un tunnel. # Ce n'est pas une `passerelle_amont` — celle-ci, nous ne la configurons jamais. -ROLES_HOTE = ("switch", "hyperviseur", "frontiere", "passerelle_amont", "relais") +# +# `frontiere_pair` (2026-10-13) : la frontiere d'un AUTRE site, au bout du tunnel entre les +# sites. Declaree dans les deux cartes, chacune avec sa propre frontiere en face. Ce n'est +# pas la notre (aucun devis ne la configure d'ici) ; ce n'est pas un routeur qu'on ignore. +ROLES_HOTE = ("switch", "hyperviseur", "frontiere", "passerelle_amont", "relais", + "frontiere_pair") # UN LIEN QUI N'EST NI UN VLAN NI UN PORT (2026-10-13). Le tunnel vers le relais porte # un sous-reseau de liaison, mais aucune etiquette : il ne traverse aucun commutateur. @@ -443,6 +448,18 @@ ROLES_HOTE = ("switch", "hyperviseur", "frontiere", "passerelle_amont", "relais" TUNNELS = ("wireguard",) +def reseaux_tunnel(underlay: dict | None) -> set[str]: + """Les noms des reseaux qui sont des TUNNELS (`tunnel:`). + + ECARTES DES PATTES DE LA FRONTIERE (2026-10-14). Le relais et le lien entre les sites + donnent a la frontiere une adresse de plus — mais rien de ce qu'elle EMET (journaux, + metriques, heure) n'en part, et rien de ce qui y arrive ne vient du plan de gestion. Les + compter a rendu `172.31.0.2` source admise des journaux d'un locataire, en desaccord + avec ses regles : un tunnel ne vit que par les regles que le devis declare pour lui. + """ + return {str(r.get("nom")) for r in reseaux(underlay) if r.get("tunnel") is not None} + + def role_hote(hote: dict) -> str: """Nature d'un equipement declare. `switch` par defaut (compatibilite).