Set-OPS-Public/scripts/site_inventaire.py

315 lines
16 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Inventaire dynamique des machines de l'HEBERGEUR, lu dans `underlay.yml`.
ansible-playbook -i scripts/site_inventaire.py playbooks/groupes/serveur_ops_site.yml
POURQUOI DYNAMIQUE, ET PAS UN `hosts.yml` GENERE.
La regle du depot est qu'un inventaire de tenant est un ARTEFACT GENERE, jamais edite a
la main : on edite le plan, puis `make instancier`. Cette regle existe parce qu'un tenant
DERIVE tout de son index — le fichier intermediaire est le prix a payer pour rendre la
derivation inspectable.
Un site ne derive de rien. Sa declaration EST deja sa forme finale : `underlay.yml` dit
l'adresse, le nœud, le pont, les services. Materialiser un second fichier qui repete la
meme chose ne rendrait rien plus inspectable — ca creerait seulement un artefact de plus
a garder en phase, et une occasion de plus de le laisser deriver de sa source.
LES GROUPES SONT LES SERVICES. Une machine qui declare `services: [serveur_ops_site]`
atterrit dans le groupe `serveur_ops_site`, et `playbooks/groupes/serveur_ops_site.yml`
la trouve sans qu'on ait rien a cabler. C'est la meme convention que pour les tenants :
ce qui se partage entre un site et un tenant, ce sont les ROLES, pas la forme du plan.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
import yaml # noqa: E402
import underlay as U # noqa: E402
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
from inventory_rules import integrations_universelles # noqa: E402
# L'inventaire des tenants passe par ce compte ; le site n'a aucune raison d'en differer.
UTILISATEUR_DEFAUT = "ansible"
# LE SOCLE S'APPLIQUE AUSSI AU SITE. Une machine de l'hebergeur n'est pas d'une autre
# espece : elle veut le meme durcissement SSH, les memes horloges, le meme pare-feu que
# n'importe quelle machine de la flotte. L'oublier lui aurait donne un SSH par defaut et
# aucune regle de frontiere — la machine la plus puissante du site, la moins protegee.
GROUPE_SOCLE = "serveur_debian"
site : un ecosysteme complet — PKI, DNS, forge, cache, runner Le site portait trois services sur les sept du modele `origine`. Sa forge servait LE GENOME EN CLAIR et aucune de ses machines n'avait de certificat — donc aucun chiffrement est-ouest, ce que la doctrine zero-confiance interdit. site-pki-01 (step-ca) et site-dns-01 (PowerDNS + resolveur colocalises) rejoignent les trois autres. Quatre defauts que le site a fait tomber, chacun invisible chez un tenant : - DNS bloque par notre propre default-deny. Un tenant a son resolveur DANS son reseau et ne traverse jamais la frontiere ; le site interroge la sienne. La regle est derivee de `site.dns_amorcage`, destination declaree, jamais `any`. - serveur_cache_site n'installe rien : il marque un cache et lit les variables de serveur_artefacts. Les defauts d'un role ne sont en portee que dans le play qui l'inclut — une dependance de role regle l'ordre ET la portee. - resoudre_idp partait meme avec OIDC desactive, et exigeait un plan. La resolution suit desormais l'usage. - le plancher /etc/hosts etait VIDE : `hotes_actifs` n'existait pas dans l'inventaire du site. Un role qui reussit en n'ecrivant rien est la pire forme d'echec. Une machine du site peut desormais se configurer (`variables:`), appliquee en dernier : ce qu'une machine declare d'elle-meme prime sur ce que le site declare pour toutes. Verifie et non suppose : systemd disait `active` mais rien n'ecoutait sur 443 — step-ca sert sur 8443. La zone souveraine resout et la recursion marche, mesurees sur la machine. 42 preuves vertes, ansible-lint profil production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 12:00:17 -04:00
# LE PLANCHER `/etc/hosts` SE DERIVE DE CE GROUPE, et de nul autre.
#
# `hosts_statiques` construit sa liste depuis `hotes_actifs` + `hotes_planifies` — des
# groupes qu'un inventaire de tenant fabrique, et que celui du site ne fabriquait pas. Le
# role tournait donc sans erreur et ecrivait un plancher VIDE : `localhost`, et rien
# d'autre. Un succes qui n'a rien fait, la pire forme d'echec.
#
# Le site n'a pas d'hotes « planifies » — une machine y est declaree ou elle n'existe pas.
# Elles vont donc toutes dans `hotes_actifs`.
GROUPE_FLOTTE = "hotes_actifs"
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
def plan_dir() -> Path | None:
"""Le `plan/` du SITE, a cote de son `underlay.yml` — derive du symlink, jamais redit.
UN SITE A BIEN UN PLAN (2026-08-25). Ce qui le distingue d'un tenant n'est pas
l'absence de plan mais l'absence de DERIVATION : un tenant tire son adressage de son
`index`, un site le declare, parce qu'il EST le terrain. Il ne passe donc pas par
`instancier` — mais il a bien des machines, des services et des intrants a declarer.
Faute d'avoir fait cette distinction, ce plan a d'abord ete reconstruit morceau par
morceau dans `underlay.yml`, sans etre nomme : chaque manque est arrive par surprise
au lieu d'etre previsible. `underlay.yml` decrit ce qui est MESURE (la fabric) ; le
plan decrit ce qui est VOULU.
"""
c = U.chemin()
return (c.resolve().parent / "plan") if c else None
def _charger(nom: str) -> dict:
d = plan_dir()
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 inventaire() -> dict:
u = U.charger()
if u is None:
# Aucun underlay monte : un inventaire VIDE, pas une erreur. Ansible doit pouvoir
# etre lance sans site sans que ca ressemble a une panne.
return {"_meta": {"hostvars": {}}}
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
intrants = _charger("10-intrants.yml")
serveurs = (_charger("serveurs.yml") or {}).get("serveurs") or {}
applications = (_charger("applications.yml") or {}).get("applications") or {}
if not serveurs:
return {"_meta": {"hostvars": {}}}
resolveur du site : le troisieme service prete pendant la jeunesse d un tenant Apres les paquets (serveur_cache_site) et le genome (serveur_forge_site), les NOMS. Meme patron : un installateur, un marqueur. CE QUE CA CORRIGE, mesure sur infra-pki-01, premiere machine a porter l autorite de Chezlepro : DNS BLOQUE un tenant resout chez lui, sa requete ne sort pas 443 sortant OK par adresse apt via le cache OK le mandataire resout a sa place apt s en sortait ; TOUT LE RESTE ETAIT AVEUGLE AUX NOMS. Versionner la cle de signature Smallstep a fait passer une tache et l echec s est deplace d un cran : le depot lui-meme devait etre joint par son nom. POURQUOI PAS L UNBOUND DE LA FRONTIERE : il tourne sur le boitier sans etre un service gere, et le plan du site l avait deja ecrit une fois — un processus n est pas un service. site-dns-01 est deploye par le moteur et verifie par le harnais. OU VIT LA DERIVATION : dans site_inventaire.py, qui connait le plan du site ET le registre des tenants. Le role tourne sur une machine qui n a aucune raison de lire la carte de l hebergeur — la derivation appartient a qui detient les deux sources. PIEGE DE FILTRE, ATTRAPE EN LE BRANCHANT : _supernets_voisins() EXCLUT l instance montee (personne n est son propre voisin). Le site sert TOUS ceux qu il heberge — y compris celui qu on deploie. Reutilise tel quel, il privait de resolution le seul tenant en cours de montage. On reutilise donc la DECOUVERTE partagee sans son filtre : deux recensements divergent, deux filtres non. Le marqueur verifie deux choses : que le resolveur ECOUTE, et que sa liste d autorisation ADMET reellement les tenants. Sans la seconde, la panne chez le locataire ressemblerait a un DNS mort alors que c est une ACL. make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 12:37:33 -04:00
# LES SUPERNETS DES TENANTS QUE CE SITE HEBERGE.
#
# LA MEME SOURCE QUE LES FLUX, UN FILTRE DIFFERENT — et la difference compte.
#
# `resoudre_flux._supernets_voisins()` EXCLUT l'instance montee : il sert a declarer
# les flux d'un tenant vers ses VOISINS, et personne n'est son propre voisin. Le site,
# lui, sert TOUS ceux qu'il heberge — y compris celui qu'on pilote au moment ou l'on
# genere cet inventaire. Reutiliser la fonction telle quelle aurait prive de
# resolution le seul tenant qu'on est en train de deployer, et la panne serait apparue
# chez lui seul (constate en la branchant, le 2026-08-30 : Chezlepro manquait).
#
# On reutilise donc la DECOUVERTE partagee (`decouvrir_du_site`, lecon de P41) sans
# son filtre : deux recensements de tenants finiraient par diverger, deux filtres non.
#
# Rend [] si la carte de l'hebergeur n'est pas montee : le site se decrit alors comme
# avant, sans preter son resolveur. Degrader, jamais deviner.
try:
import devis_reseau
from inventory_rules import supernet_de
_supernets_tenants = sorted({supernet_de(int(n["index"]))
for _nom, _p, n in devis_reseau.decouvrir_du_site()
if n.get("index") is not None})
except Exception:
_supernets_tenants = []
# Les groupes d'une machine du site, tels que `plan/applications.yml` les declare.
def _groupes_de(nom_hote: str) -> set:
return {str(a.get("groupe")) for a in applications.values()
if str(a.get("hote")) == nom_hote and a.get("groupe")}
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
par_reseau = {r.get("nom"): r for r in U.reseaux(u)}
site : quatre zones d'autorite, et l'ordre inscrit dans les integrations Le site n'est plus un /24 plat. Une zone par nature d'autorite — pilotage, autorite, genome, service — chacune son VLAN et sa patte sur la frontiere. L'inversion corrigee, mesuree : les cinq VM du site n'avaient AUCUN filtrage est-ouest, contre policy_in=DROP sur une machine de tenant. La plus autoritaire etait la moins protegee. Filtrage nord-sud par choix de l'exploitant : un seul point de police, un seul devis. 90 -> 117 regles. Chaque flux `flotte` produit une regle par zone SOURCE, destination nommee — ce qui etait gratuit devient police. L'ORDRE FAIT PARTIE DE L'INTEGRATION. client_pki tournait en parallele sur tous les hotes ; sur celui qui porte l'autorite il recharge step-ca, et les quatre autres echouaient dans cette fenetre sur `TLS handshake timeout` — un message qui accuse le reseau. Chaque integration declare desormais sa dependance dans meta/integration.yml, les playbooks en sont le miroir genere, et P44 refuse l'ecart (4 controles negatifs). `serveur: ~` est une reponse valable : client_metrique pose un exportateur qu'on vient LIRE. Autres defauts du meme soir : - le plancher /etc/hosts venait APRES le premier apt, qui vise le cache par son NOM : boucle fermee des que les adresses changent. Il ne s'installe pas, il rend installable. - dns_amorcage ecrit en dur a eu tort deux fois ; il se derive de serveur_resolveur. - l'ACL du resolveur derivait d'un seul sous-reseau : trois zones refusees sur quatre. ET UNE ERREUR A MOI : j'ai diagnostique un trou noir de MTU et declare 1450. Faux — pas de VXLAN sur ce chemin, tout est a 1500 de bout en bout. Ma mesure etait reelle, mon interpretation non : je venais de debrancher la carte a chaud, et chaque `ip link set mtu` reconfigurait l'interface. C'est la reconfiguration qui debloquait, pas la valeur. 44 preuves vertes, ansible-lint profil production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:31:07 -04:00
# Les sous-reseaux ou vivent REELLEMENT des machines du site : l'etendue de
# l'ecosysteme, telle que le plan la dessine.
_zones_du_site = sorted({str((par_reseau.get(s.get("reseau")) or {}).get("sous_reseau"))
for s in serveurs.values()
if str(s.get("etat", "actif")) == "actif"
and (par_reseau.get(s.get("reseau")) or {}).get("sous_reseau")})
# LE RESOLVEUR D'AMORCAGE SE DERIVE, IL NE S'ECRIT PAS (2026-08-25).
#
# Il a ete ecrit en dur deux fois, et il a eu tort les deux fois : d'abord la frontiere
# (`10.0.3.1`) alors que le site avait son DNS, puis `10.0.3.51` — juste jusqu'a ce que
# le decoupage en zones deplace la machine en `10.0.34.11`.
#
# Une adresse ecrite a la main est une copie ; une copie se perime. Celle-ci se lit
# desormais la ou elle est vraie : l'hote qui porte `serveur_resolveur` dans ce plan.
# Le plan peut encore la surcharger — pour l'amorcage d'un site tout neuf, ou le
# resolveur n'existe pas encore — mais ce n'est plus le cas normal.
_resolveur = next((str(s.get("ip")) for nom, s in serveurs.items()
if any(a.get("hote") == nom and a.get("groupe") == "serveur_resolveur"
for a in applications.values())
and str(s.get("etat", "actif")) == "actif"), "")
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
# Ce que TOUTE machine du site recoit, des intrants du plan.
communes: dict = {}
site : quatre zones d'autorite, et l'ordre inscrit dans les integrations Le site n'est plus un /24 plat. Une zone par nature d'autorite — pilotage, autorite, genome, service — chacune son VLAN et sa patte sur la frontiere. L'inversion corrigee, mesuree : les cinq VM du site n'avaient AUCUN filtrage est-ouest, contre policy_in=DROP sur une machine de tenant. La plus autoritaire etait la moins protegee. Filtrage nord-sud par choix de l'exploitant : un seul point de police, un seul devis. 90 -> 117 regles. Chaque flux `flotte` produit une regle par zone SOURCE, destination nommee — ce qui etait gratuit devient police. L'ORDRE FAIT PARTIE DE L'INTEGRATION. client_pki tournait en parallele sur tous les hotes ; sur celui qui porte l'autorite il recharge step-ca, et les quatre autres echouaient dans cette fenetre sur `TLS handshake timeout` — un message qui accuse le reseau. Chaque integration declare desormais sa dependance dans meta/integration.yml, les playbooks en sont le miroir genere, et P44 refuse l'ecart (4 controles negatifs). `serveur: ~` est une reponse valable : client_metrique pose un exportateur qu'on vient LIRE. Autres defauts du meme soir : - le plancher /etc/hosts venait APRES le premier apt, qui vise le cache par son NOM : boucle fermee des que les adresses changent. Il ne s'installe pas, il rend installable. - dns_amorcage ecrit en dur a eu tort deux fois ; il se derive de serveur_resolveur. - l'ACL du resolveur derivait d'un seul sous-reseau : trois zones refusees sur quatre. ET UNE ERREUR A MOI : j'ai diagnostique un trou noir de MTU et declare 1450. Faux — pas de VXLAN sur ce chemin, tout est a 1500 de bout en bout. Ma mesure etait reelle, mon interpretation non : je venais de debrancher la carte a chaud, et chaque `ip link set mtu` reconfigurait l'interface. C'est la reconfiguration qui debloquait, pas la valeur. 44 preuves vertes, ansible-lint profil production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:31:07 -04:00
if _resolveur:
communes["dns_amorcage"] = _resolveur
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
for cle in ("domaine_interne", "organisation", "dns_amorcage"):
if intrants.get(cle):
communes[cle] = str(intrants[cle])
if intrants.get("cle_ssh"):
communes["ansible_ssh_private_key_file"] = intrants["cle_ssh"]
if intrants.get("rebond"):
communes["ansible_ssh_common_args"] = (
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
f"-o ProxyJump={intrants['rebond']} -o StrictHostKeyChecking=no")
# Les integrations universelles, moins celles que le site ne peut pas honorer.
exemptees = dict(intrants.get("integrations_exemptes") or {})
universelles = [r for r in integrations_universelles() if r not in exemptees]
# LES EXPOSITIONS : le nom du SERVICE, pas celui de la machine. Un nom de service
# survit au demenagement du service ; un nom de machine, non.
#
# `edge` nomme le GROUPE qui sert ce FQDN — c'est ce qu'attend `hosts_statiques`.
# Chez un tenant c'est l'edge nginx, qui termine le TLS pour tout le monde. Le site
# n'a pas d'edge : chaque service se sert lui-meme, donc le groupe est le sien. La
# forme reste la meme, ce qui la remplit change.
expositions: list[dict] = []
for nom_app, app in applications.items():
for fqdn in (app.get("expose") or []):
expositions.append({"fqdn": str(fqdn), "edge": str(app.get("groupe") or ""),
"application": nom_app})
# --- LES GARDES DU PLAN, LA OU LE PLAN EST LU ----------------------------
#
# Elles vivaient dans `underlay.valider()` tant que les machines habitaient la carte.
# Le plan les emporte avec lui : une garde loin de ce qu'elle garde finit par garder
# autre chose. Elles refusent ici, avant que quoi que ce soit ne soit clone.
erreurs: list[str] = []
noeuds = {h.get("nom") for h in U.hotes(u) if h.get("role") == "hyperviseur"}
vus_ip: dict[str, str] = {str(h.get("ip")): str(h.get("nom")) for h in U.hotes(u)}
vus_vmid: dict[int, str] = {}
for nom, srv in serveurs.items():
if str(srv.get("etat", "actif")) != "actif":
continue
r = par_reseau.get(srv.get("reseau"))
if not r:
erreurs.append(f"serveur '{nom}': reseau '{srv.get('reseau')}' inconnu de "
f"`underlay.yml` — la fabric est la source de ces valeurs")
continue
if not str(r.get("pont", "") or "").strip():
erreurs.append(f"serveur '{nom}': le reseau '{r.get('nom')}' ne declare aucun "
f"`pont:` — aucun pont d'hyperviseur ne le porte, une VM y "
f"naitrait sourde")
if srv.get("noeud") not in noeuds:
erreurs.append(f"serveur '{nom}': noeud '{srv.get('noeud')}' n'est pas un "
f"hyperviseur declare dans `underlay.hotes`")
ip = str(srv.get("ip") or "")
if ip in vus_ip:
erreurs.append(f"serveur '{nom}': IP {ip} deja prise par '{vus_ip[ip]}'")
vus_ip[ip] = nom
vmid = srv.get("vmid")
if not isinstance(vmid, int):
erreurs.append(f"serveur '{nom}': `vmid` absent ou non entier")
elif vmid in vus_vmid:
erreurs.append(f"serveur '{nom}': vmid {vmid} en double avec "
f"'{vus_vmid[vmid]}'")
else:
vus_vmid[vmid] = nom
for nom_app, app in applications.items():
if app.get("hote") not in serveurs:
erreurs.append(f"application '{nom_app}': hote '{app.get('hote')}' n'est pas "
f"declare dans `plan/serveurs.yml`")
sans_service = [n for n in serveurs
if not any(a.get("hote") == n for a in applications.values())]
if sans_service:
erreurs.append("serveur(s) sans aucune application : " + ", ".join(sorted(sans_service))
+ " — une machine du site existe POUR un role")
if erreurs:
raise SystemExit("plan du site refuse :\n - " + "\n - ".join(erreurs))
hostvars: dict[str, dict] = {}
groupes: dict[str, list[str]] = {}
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
for nom, srv in serveurs.items():
if str(srv.get("etat", "actif")) != "actif":
continue
r = par_reseau.get(srv.get("reseau")) or {}
hostvars[nom] = {
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
"ansible_host": str(srv["ip"]),
"ansible_user": srv.get("utilisateur", UTILISATEUR_DEFAUT),
"site_reseau": srv.get("reseau"),
"site_pont": r.get("pont"),
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
"site_noeud": srv.get("noeud"),
"proxmox_vmid": srv.get("vmid"),
# L'espace d'adressage de cet ecosysteme. Un tenant le derive de son index ;
# le site le tient du reseau ou vivent ses machines. Meme sens, autre source.
# `serveur_resolveur` s'en sert pour savoir QUI a le droit de l'interroger.
"setops_supernet": str(r.get("sous_reseau") or ""),
site : quatre zones d'autorite, et l'ordre inscrit dans les integrations Le site n'est plus un /24 plat. Une zone par nature d'autorite — pilotage, autorite, genome, service — chacune son VLAN et sa patte sur la frontiere. L'inversion corrigee, mesuree : les cinq VM du site n'avaient AUCUN filtrage est-ouest, contre policy_in=DROP sur une machine de tenant. La plus autoritaire etait la moins protegee. Filtrage nord-sud par choix de l'exploitant : un seul point de police, un seul devis. 90 -> 117 regles. Chaque flux `flotte` produit une regle par zone SOURCE, destination nommee — ce qui etait gratuit devient police. L'ORDRE FAIT PARTIE DE L'INTEGRATION. client_pki tournait en parallele sur tous les hotes ; sur celui qui porte l'autorite il recharge step-ca, et les quatre autres echouaient dans cette fenetre sur `TLS handshake timeout` — un message qui accuse le reseau. Chaque integration declare desormais sa dependance dans meta/integration.yml, les playbooks en sont le miroir genere, et P44 refuse l'ecart (4 controles negatifs). `serveur: ~` est une reponse valable : client_metrique pose un exportateur qu'on vient LIRE. Autres defauts du meme soir : - le plancher /etc/hosts venait APRES le premier apt, qui vise le cache par son NOM : boucle fermee des que les adresses changent. Il ne s'installe pas, il rend installable. - dns_amorcage ecrit en dur a eu tort deux fois ; il se derive de serveur_resolveur. - l'ACL du resolveur derivait d'un seul sous-reseau : trois zones refusees sur quatre. ET UNE ERREUR A MOI : j'ai diagnostique un trou noir de MTU et declare 1450. Faux — pas de VXLAN sur ce chemin, tout est a 1500 de bout en bout. Ma mesure etait reelle, mon interpretation non : je venais de debrancher la carte a chaud, et chaque `ip link set mtu` reconfigurait l'interface. C'est la reconfiguration qui debloquait, pas la valeur. 44 preuves vertes, ansible-lint profil production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:31:07 -04:00
# QUI A LE DROIT D'INTERROGER LE RESOLVEUR — TOUTES LES ZONES DU SITE.
#
# `serveur_resolveur` derive sa liste d'autorisation de `setops_supernet`, qui
# vaut le sous-reseau de la machine. C'etait juste tant que le site etait un
# `/24` plat. Decoupe en zones, le resolveur n'autorisait plus que la SIENNE :
# les trois autres se faisaient refuser, et la panne ressemble a un DNS mort
# alors que c'est une ACL.
#
# Un resolveur ouvert au monde est un relais d'amplification ; celui-ci est
# ouvert a son ecosysteme, et a rien d'autre.
resolveur du site : le troisieme service prete pendant la jeunesse d un tenant Apres les paquets (serveur_cache_site) et le genome (serveur_forge_site), les NOMS. Meme patron : un installateur, un marqueur. CE QUE CA CORRIGE, mesure sur infra-pki-01, premiere machine a porter l autorite de Chezlepro : DNS BLOQUE un tenant resout chez lui, sa requete ne sort pas 443 sortant OK par adresse apt via le cache OK le mandataire resout a sa place apt s en sortait ; TOUT LE RESTE ETAIT AVEUGLE AUX NOMS. Versionner la cle de signature Smallstep a fait passer une tache et l echec s est deplace d un cran : le depot lui-meme devait etre joint par son nom. POURQUOI PAS L UNBOUND DE LA FRONTIERE : il tourne sur le boitier sans etre un service gere, et le plan du site l avait deja ecrit une fois — un processus n est pas un service. site-dns-01 est deploye par le moteur et verifie par le harnais. OU VIT LA DERIVATION : dans site_inventaire.py, qui connait le plan du site ET le registre des tenants. Le role tourne sur une machine qui n a aucune raison de lire la carte de l hebergeur — la derivation appartient a qui detient les deux sources. PIEGE DE FILTRE, ATTRAPE EN LE BRANCHANT : _supernets_voisins() EXCLUT l instance montee (personne n est son propre voisin). Le site sert TOUS ceux qu il heberge — y compris celui qu on deploie. Reutilise tel quel, il privait de resolution le seul tenant en cours de montage. On reutilise donc la DECOUVERTE partagee sans son filtre : deux recensements divergent, deux filtres non. Le marqueur verifie deux choses : que le resolveur ECOUTE, et que sa liste d autorisation ADMET reellement les tenants. Sans la seconde, la panne chez le locataire ressemblerait a un DNS mort alors que c est une ACL. make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 12:37:33 -04:00
# LE SITE PRETE SON RESOLVEUR PENDANT LA JEUNESSE DE SES LOCATAIRES
# (2026-08-30). L'hote qui porte `serveur_resolveur_site` admet EN PLUS les
# supernets des tenants que ce site heberge.
#
# POURQUOI ICI ET PAS DANS LE ROLE : l'inventaire connait le plan du site ET
# le registre des tenants ; le role, lui, tourne sur une machine qui n'a
# aucune raison de lire la carte de l'hebergeur. La derivation appartient a
# qui detient les deux sources.
#
# CE QUE CA A COUTE DE NE PAS L'AVOIR : `infra-pki-01`, premiere machine a
# porter l'autorite de Chezlepro, ne pouvait resoudre aucun nom. `apt` s'en
# sortait par le mandataire du cache, qui resout a sa place — tout le reste
# (`get_url`, un depot tiers en HTTPS, un `git clone`) restait aveugle.
"serveur_resolveur_reseaux_autorises": (
_zones_du_site + _supernets_tenants
if "serveur_resolveur_site" in _groupes_de(nom) else _zones_du_site),
# Ce que le marqueur verifiera : la liste ci-dessus les admet-elle vraiment ?
"serveur_resolveur_site_supernets_tenants": _supernets_tenants,
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
# Un role partage peut avoir besoin de savoir de quel cote il tourne.
"setops_site": True,
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
# Le plan du site, pour les roles qui lisent des registres.
"setops_plan_dir": str(plan_dir() or ""),
"hosts_statiques_expositions": expositions,
**communes,
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
# En dernier : ce qu'une machine declare d'elle-meme prime sur ce que le
# plan declare pour toutes.
**(srv.get("variables") or {}),
}
groupes.setdefault(GROUPE_SOCLE, []).append(nom)
site : un ecosysteme complet — PKI, DNS, forge, cache, runner Le site portait trois services sur les sept du modele `origine`. Sa forge servait LE GENOME EN CLAIR et aucune de ses machines n'avait de certificat — donc aucun chiffrement est-ouest, ce que la doctrine zero-confiance interdit. site-pki-01 (step-ca) et site-dns-01 (PowerDNS + resolveur colocalises) rejoignent les trois autres. Quatre defauts que le site a fait tomber, chacun invisible chez un tenant : - DNS bloque par notre propre default-deny. Un tenant a son resolveur DANS son reseau et ne traverse jamais la frontiere ; le site interroge la sienne. La regle est derivee de `site.dns_amorcage`, destination declaree, jamais `any`. - serveur_cache_site n'installe rien : il marque un cache et lit les variables de serveur_artefacts. Les defauts d'un role ne sont en portee que dans le play qui l'inclut — une dependance de role regle l'ordre ET la portee. - resoudre_idp partait meme avec OIDC desactive, et exigeait un plan. La resolution suit desormais l'usage. - le plancher /etc/hosts etait VIDE : `hotes_actifs` n'existait pas dans l'inventaire du site. Un role qui reussit en n'ecrivant rien est la pire forme d'echec. Une machine du site peut desormais se configurer (`variables:`), appliquee en dernier : ce qu'une machine declare d'elle-meme prime sur ce que le site declare pour toutes. Verifie et non suppose : systemd disait `active` mais rien n'ecoutait sur 443 — step-ca sert sur 8443. La zone souveraine resout et la recursion marche, mesurees sur la machine. 42 preuves vertes, ansible-lint profil production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 12:00:17 -04:00
groupes.setdefault(GROUPE_FLOTTE, []).append(nom)
site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai, c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan. SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent du registre des applications, les gardes ont suivi le plan (4 controles negatifs). Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout : - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ; - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ; - le certificat, PUBLIC par nature, restait en 0600 ; - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ; - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ; - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ; - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ; - le flux declarait 3000 en dur. Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher s'ecrivait vide. ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec CAP_NET_BIND_SERVICE, et son URL n'a plus de port. Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200. 42 preuves vertes, flux coherents (34 roles, 92 flux). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
for integ in universelles:
groupes.setdefault(integ, []).append(nom)
for integ in (srv.get("integrations") or []):
groupes.setdefault(str(integ), []).append(nom)
# LES GROUPES VIENNENT DU REGISTRE DES APPLICATIONS, comme chez un tenant.
for app in applications.values():
hote, groupe = app.get("hote"), app.get("groupe")
if hote in hostvars and groupe:
groupes.setdefault(str(groupe), []).append(hote)
out: dict = {g: {"hosts": sorted(set(h))} for g, h in groupes.items()}
out["site"] = {"hosts": sorted(hostvars)}
out["_meta"] = {"hostvars": hostvars}
return out
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--list", action="store_true")
ap.add_argument("--host")
a = ap.parse_args()
if a.host:
print(json.dumps(inventaire().get("_meta", {}).get("hostvars", {}).get(a.host, {})))
else:
print(json.dumps(inventaire(), indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())