2026-08-24 23:11:32 -04:00
|
|
|
#!/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
|
2026-08-24 23:11:32 -04:00
|
|
|
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
|
2026-08-24 23:11:32 -04:00
|
|
|
|
|
|
|
|
# L'inventaire des tenants passe par ce compte ; le site n'a aucune raison d'en differer.
|
|
|
|
|
UTILISATEUR_DEFAUT = "ansible"
|
|
|
|
|
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
# 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"
|
|
|
|
|
|
2026-08-24 23:11:32 -04:00
|
|
|
|
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 {}
|
|
|
|
|
|
|
|
|
|
|
2026-08-24 23:11:32 -04:00
|
|
|
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"), "")
|
2026-08-24 23:11:32 -04:00
|
|
|
|
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.
|
2026-08-25 09:34:02 -04:00
|
|
|
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"):
|
2026-08-25 09:34:02 -04:00
|
|
|
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))
|
2026-08-25 09:34:02 -04:00
|
|
|
|
2026-08-24 23:11:32 -04:00
|
|
|
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 {}
|
2026-08-24 23:11:32 -04:00
|
|
|
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"),
|
2026-08-24 23:11:32 -04:00
|
|
|
"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.
|
2026-08-24 23:11:32 -04:00
|
|
|
"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,
|
2026-08-25 09:34:02 -04:00
|
|
|
**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 {}),
|
2026-08-24 23:11:32 -04:00
|
|
|
}
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
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()}
|
2026-08-24 23:11:32 -04:00
|
|
|
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())
|