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.
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
#
|
|
|
|
|
# CE COMMENTAIRE DISAIT VRAI, LE CODE N'EN FAISAIT QUE LA MOITIE (2026-09-02).
|
|
|
|
|
#
|
|
|
|
|
# Seul `serveur_debian` etait pose. `serveur_durci` — auditd, fail2ban, apparmor,
|
|
|
|
|
# sysctl, unattended-upgrades, journald, ssh_hardening, nftables — ne l'etait pas. Un
|
|
|
|
|
# inventaire de tenant met CHAQUE machine dans les deux (`inventory_host.py`) ; celui du
|
|
|
|
|
# site n'en mettait aucune dans le second.
|
|
|
|
|
#
|
|
|
|
|
# Ce que ca laissait sans protection : la racine de l'autorite de certification, la
|
|
|
|
|
# forge qui porte le genome, le cache d'artefacts par ou passe chaque paquet installe
|
|
|
|
|
# sur chaque machine de chaque locataire, et le depot ou tous deposent leur etat. Les
|
|
|
|
|
# machines les plus consequentes de l'installation etaient les moins durcies — et
|
|
|
|
|
# personne ne pouvait le voir, puisque le commentaire affirmait le contraire.
|
|
|
|
|
#
|
|
|
|
|
# TROUVE EN CHERCHANT AUTRE CHOSE : un depot de sauvegarde refusait une cle valide, et
|
|
|
|
|
# le fichier de durcissement SSH fautif datait d'une version abandonnee le 2026-08-09.
|
|
|
|
|
# Rien ne l'avait jamais remplace parce que rien n'appliquait le role qui le remplace.
|
|
|
|
|
GROUPE_SOCLE = ["serveur_debian", "serveur_durci"]
|
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
|
|
|
|
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
|
|
|
|
le site pouvait inseminer un locataire, pas se configurer lui-meme
Deux defauts qui s additionnaient, et un message qui accusait le mauvais.
1. Le site n avait AUCUN moyen d autoriser une cle d administration. Un
locataire declare ssh_baseline_cles_admin ; cote site, rien ne transmettait
cette liste. Ses machines n autorisaient que la cle de cloud-init.
2. Le rebond etait pose sans condition. Depuis le poste c est juste — aucune
route directe. Depuis le runner du site, qui vit DANS le site, c est un
detour qui casse : il doit s authentifier aupres de la frontiere ou sa cle
n est pas autorisee.
Et OpenSSH rend alors Host key verification failed / Connection closed by
UNKNOWN port 65535 — un message qui accuse les cles d HOTE alors que l echec
est une AUTHENTIFICATION, et sur le SAUTEUR, pas sur la cible.
Le critere du rebond est desormais es-tu une MACHINE du site, pas es-tu dans un
RESEAU du site : le poste porte 10.37.0.17, donc il est dans le reseau
management, et il aurait perdu son rebond alors qu il en a besoin.
L amorcage est circulaire et se rompt par le poste : lui seul entrait, il pose
la cle, le runner est ensuite autonome. Verifie : 5/5 machines du site.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-14 10:08:50 -04:00
|
|
|
|
|
|
|
|
def _controleur_est_une_machine_du_site() -> bool:
|
|
|
|
|
"""Le controleur EST-IL l'une des machines que l'underlay declare ?
|
|
|
|
|
|
|
|
|
|
C'EST LA BONNE QUESTION, ET LA PREMIERE VERSION POSAIT LA MAUVAISE. Elle demandait
|
|
|
|
|
« suis-je dans un reseau du site ? » — or le poste de l'exploitant porte `10.37.0.17`,
|
|
|
|
|
donc il est dans le reseau `management` du site, et il aurait perdu son rebond alors
|
|
|
|
|
qu'il en a besoin : ses paquets arrivent sur une autre patte de la frontiere, avec
|
|
|
|
|
d'autres regles.
|
|
|
|
|
|
|
|
|
|
Etre DANS un reseau du site ne dit rien de ce qu'on peut atteindre. Etre une MACHINE
|
|
|
|
|
du site, si : la frontiere les laisse se parler entre elles.
|
|
|
|
|
|
|
|
|
|
En cas de doute — adresses illisibles, underlay muet — on rend False : poser le rebond
|
|
|
|
|
est le comportement d'avant, donc le seul qu'on sache sur. Degrader, jamais deviner.
|
|
|
|
|
"""
|
|
|
|
|
import subprocess
|
|
|
|
|
try:
|
|
|
|
|
sortie = subprocess.run(["ip", "-4", "-o", "addr", "show"],
|
|
|
|
|
capture_output=True, text=True, timeout=5).stdout
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
|
|
|
|
locales = {m.split("/")[0] for ligne in sortie.splitlines() for m in ligne.split()
|
|
|
|
|
if "/" in m and m[0].isdigit()}
|
|
|
|
|
if not locales:
|
|
|
|
|
return False
|
|
|
|
|
try:
|
|
|
|
|
declarees = {str(h.get("ip")) for h in underlay_mod.hotes(None) if h.get("ip")}
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
|
|
|
|
return bool(locales & declarees)
|
|
|
|
|
|
|
|
|
|
|
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 = []
|
|
|
|
|
|
2026-09-01 22:16:20 -04:00
|
|
|
# LES LOCATAIRES DU DEPOT DE SAUVEGARDE — UN COMPTE UNIX CHACUN.
|
|
|
|
|
#
|
|
|
|
|
# Le depot du site recoit l'etat de PLUSIEURS ecosystemes. `serveur_backup`, concu
|
|
|
|
|
# pour le depot d'un tenant, n'a qu'un compte et qu'une cle : la ou il n'y a qu'un
|
|
|
|
|
# locataire, ca suffit. Ici, ca ne suffit plus — une seule cle partagee donnerait a
|
|
|
|
|
# chaque tenant la lecture des instantanes de tous les autres. Une sauvegarde qui
|
|
|
|
|
# expose ce qu'elle protege ne protege rien.
|
|
|
|
|
#
|
|
|
|
|
# D'ou un compte par locataire, chacun avec SON home en 0700 et SA cle. L'isolation
|
|
|
|
|
# est alors celle du noyau, pas une convention de nommage de repertoires.
|
|
|
|
|
#
|
|
|
|
|
# La cle PUBLIQUE d'un tenant n'est pas un secret : elle vit deja en clair dans son
|
|
|
|
|
# `group_vars/serveur_backup.yml`. C'est la MOITIE PRIVEE qui reste chez lui, dans sa
|
|
|
|
|
# voute, que le site ne detient pas et n'a pas a detenir. Le site heberge des
|
|
|
|
|
# octets chiffres par restic cote client : il ne peut pas les lire non plus.
|
|
|
|
|
#
|
|
|
|
|
# Meme decouverte que ci-dessus (`decouvrir_du_site`) : le site sert les tenants
|
|
|
|
|
# qu'il heberge, et cesse de les servir quand ils s'emancipent.
|
|
|
|
|
def _locataires_sauvegarde() -> list:
|
|
|
|
|
out = []
|
|
|
|
|
try:
|
|
|
|
|
import devis_reseau
|
|
|
|
|
racine_instances = devis_reseau.DOSSIER_INSTANCES
|
|
|
|
|
decouverts = devis_reseau.decouvrir_du_site()
|
|
|
|
|
except Exception:
|
|
|
|
|
return out
|
|
|
|
|
for nom_depot, prefixe, _n in decouverts:
|
|
|
|
|
gv = (racine_instances / nom_depot / "inventories" / "principal"
|
|
|
|
|
/ "group_vars" / "serveur_backup.yml")
|
|
|
|
|
try:
|
|
|
|
|
cle = str((yaml.safe_load(gv.read_text(encoding="utf-8")) or {})
|
|
|
|
|
.get("serveur_backup_pubkey") or "").strip()
|
|
|
|
|
except OSError:
|
|
|
|
|
cle = ""
|
|
|
|
|
# Un locataire sans cle publique declaree n'est pas une erreur du site :
|
|
|
|
|
# c'est un tenant qui n'a pas encore de sauvegarde. On ne lui ouvre pas de
|
|
|
|
|
# compte, et surtout on n'en ouvre pas un SANS cle — il accepterait alors
|
|
|
|
|
# tout ce que le compte `restic` accepte deja.
|
|
|
|
|
if cle:
|
|
|
|
|
out.append({"nom": nom_depot,
|
|
|
|
|
"compte": "restic-" + prefixe.lower(),
|
|
|
|
|
"pubkey": cle})
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
_locataires_backup = _locataires_sauvegarde()
|
|
|
|
|
|
2026-09-02 11:21:52 -04:00
|
|
|
# OU LE SITE DEPOSE SON PROPRE ETAT.
|
|
|
|
|
#
|
|
|
|
|
# Le site a de l'etat a lui, et pas des moindres : la racine de son autorite de
|
|
|
|
|
# certification, et la forge qui porte le genome. Tout le reste est reconstructible
|
|
|
|
|
# par le code — le cache se re-remplit, la zone DNS se regenere depuis le plan, les
|
|
|
|
|
# depots du runner vivent dans git.
|
|
|
|
|
#
|
|
|
|
|
# IL DEPOSE SUR SON PROPRE DEPOT, avec SA propre identite (`backup_pubkey` du plan,
|
|
|
|
|
# moitie privee dans `underlay.vault.yml`). Celle des locataires ne lui sert a rien
|
|
|
|
|
# et ne doit surtout pas lui servir : chaque deposant a son compte, le sien est
|
|
|
|
|
# `restic` — celui que `serveur_backup` cree, avec `/srv/restic/site` pour home.
|
|
|
|
|
#
|
|
|
|
|
# LE NOM DU SERVICE, PAS L'ADRESSE. `client_backup_repo` grave cette valeur dans le
|
|
|
|
|
# chemin de CHAQUE instantane : une adresse rendrait le depot indeplacable. On prend
|
|
|
|
|
# donc le `expose` declare par l'application qui porte `serveur_backup_site` —
|
|
|
|
|
# la meme source que celle qu'un locataire utilise.
|
|
|
|
|
_cible_backup = next(
|
|
|
|
|
(str(f) for app in applications.values()
|
|
|
|
|
if str(app.get("groupe") or "") == "serveur_backup_site"
|
|
|
|
|
for f in (app.get("expose") or [])), "")
|
|
|
|
|
|
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 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)}
|
l heure vient de la frontiere : la derniere dependance vivante tombe
Mesure sur les quatorze : aucune sortie TCP vers une adresse publique, apt
par le cache, noms autoritaires en local, unattended-upgrades masked. Et
quatre pairs NTP publics par machine. Le role chrony posait le fuseau et
installait le demon sans jamais toucher a ses sources : le defaut de Debian
tenait depuis le premier jour, herite et jamais choisi.
L autorite est la frontiere, par decision de l exploitant. Elle etait deja
stratum 2 et ecoutait en 123 ; il ne manquait que le passage. 9 anciennes
regles port 123 vers !SETOPS_INTERNES retirees, 9 regles nommees vers
SETOPS_FRONTIERE posees : le changement resserre autant qu il centralise.
Deux chemins parce que la topologie en a deux. Un tenant n atteint pas la
frontiere par sa passerelle de zone — tenue par le SDN — mais par le lien de
transit. Une machine du site a la frontiere pour passerelle directe. Les deux
valeurs sont derivees de l underlay, via reseau_transit() plutot que d un
prefixe d adresse qui aurait menti chez le prochain hebergeur.
La patte face aux tenants manquait a opnsense_if_zones, pour la meme raison
que grappe-controle la veille.
Sonde horloge ecrite en meme temps : synchronisee ET contre la source
DECLAREE. Une machine peut etre parfaitement a l heure contre quatre serveurs
publics — c est exactement l etat d avant.
14 tenant + 7 site, toutes disciplinees, sources publiques = 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-11 12:08:13 -04:00
|
|
|
# L'AUTORITE DE TEMPS, COTE SITE (2026-09-11).
|
|
|
|
|
#
|
|
|
|
|
# Ici la frontiere est la passerelle DIRECTE de chaque zone — pas besoin de passer par
|
|
|
|
|
# le lien de transit comme un tenant. On prend donc la patte de la frontiere posee sur
|
|
|
|
|
# LA ZONE DE LA MACHINE : le chemin le plus court, et celui qu'aucune regle
|
|
|
|
|
# supplementaire n'a besoin d'ouvrir.
|
|
|
|
|
_temps_par_reseau = {str(_h.get("reseau")): str(_h.get("ip"))
|
|
|
|
|
for _h in U.hotes(u)
|
|
|
|
|
if _h.get("role") == "frontiere" and _h.get("ip")
|
|
|
|
|
and str(_h.get("etat", "actif")) != "reserve"}
|
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
|
|
|
|
depot de binaires : le site tient ce que les runners allaient chercher
Le cache du site couvrait apt ; quatre artefacts arrivaient autrement, parce qu ils
ne vivent dans aucun depot apt. Le controleur les tire puis les pousse par SSH.
Mesure du 2026-09-12 : le cache du runner du site est ABSENT. Un second locataire
monte depuis lui sortait chercher 570 Mo sur codeberg.org, github.com et
download.nextcloud.com, alors que le meme ecosysteme ne demandait plus un seul
paquet a Debian. Le poste du mainteneur les a depuis toujours : personne ne l avait vu.
Pas de relais transparent, et la mesure tranche : github.com redirige vers une URL
signee valable une heure, differente a chaque requete. Un cache qui la prend pour
cle ne fait jamais mouche. Le relais marcherait pour deux amonts sur quatre.
Donc un vrai depot, dans le service qui existe deja. LocalDirs d apt-cacher-ng publie
un repertoire du disque sous un prefixe, eprouve AVANT d ecrire le role. Aucun service,
aucun port, aucun certificat, aucun flux nouveaux : l ingress 3142 pair flotte couvre
exactement ce chemin.
Les versions ne sont pas recopiees : le role lit les defauts des quatre consommateurs.
Les quatre roles recoivent une tache AJOUTEE, placee avant leur stat de cache — si le
depot sert, le stat le voit et la tache amont se saute d elle-meme. Aucune tache
existante n a change.
P70 exige que tout dest ecrit sous un cache_local figure au depot. Une liste qui suit
une autre prend du retard ; celle-ci est nee avec sa garde.
Deux marches payees en chemin :
- failed_when: false REECRIT le verdict, donc la premiere garde de signature ne
gardait rien. Elles mesurent le fichier desormais.
- file: state=directory cree les parents en 0750 : apt-cacher-ng, qui ne tourne pas
en root, rendait 403 sur chaque fichier. Un chemin se traverse en entier.
Verifie sur l infrastructure : 6/6 artefacts servis (200/206) depuis le runner du site
ET depuis une machine du locataire a travers la frontiere ; les 6 empreintes SHA-256
sont identiques a celles qui ont construit Chezlepro ; second passage changed=0.
make prouver : 69 OK, 0 echec, 1 saute. ansible-lint : 0 failure, profil production.
Inclut aussi force: true sur cinq telechargements de cles : une reprise conditionnelle
ne reprend rien (304 Not Modified, size 0, attempts 5).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-12 23:45:00 -04:00
|
|
|
# LE DEPOT DES BINAIRES DIRECTS — meme derivation que le resolveur, pour la meme
|
|
|
|
|
# raison : l'adresse se lit la ou elle est vraie, dans le plan qui porte le service.
|
|
|
|
|
#
|
|
|
|
|
# Forgejo, Keycloak, Nextcloud et oauth2-proxy ne vivent dans aucun depot apt : le
|
|
|
|
|
# controleur les tire puis les pousse par SSH. Le cache du site les tient desormais
|
|
|
|
|
# (`serveur_artefacts`, LocalDirs), et c'est ce qui permet a un runner de monter un
|
|
|
|
|
# ecosysteme entier sans sortir. Mesure du 2026-09-12 : sans lui, 570 Mo par runner.
|
|
|
|
|
_cache_site = next((str(s.get("ip")) for nom, s in serveurs.items()
|
|
|
|
|
if any(a.get("hote") == nom and a.get("groupe") == "serveur_artefacts"
|
|
|
|
|
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.
|
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
|
depot de binaires : le site tient ce que les runners allaient chercher
Le cache du site couvrait apt ; quatre artefacts arrivaient autrement, parce qu ils
ne vivent dans aucun depot apt. Le controleur les tire puis les pousse par SSH.
Mesure du 2026-09-12 : le cache du runner du site est ABSENT. Un second locataire
monte depuis lui sortait chercher 570 Mo sur codeberg.org, github.com et
download.nextcloud.com, alors que le meme ecosysteme ne demandait plus un seul
paquet a Debian. Le poste du mainteneur les a depuis toujours : personne ne l avait vu.
Pas de relais transparent, et la mesure tranche : github.com redirige vers une URL
signee valable une heure, differente a chaque requete. Un cache qui la prend pour
cle ne fait jamais mouche. Le relais marcherait pour deux amonts sur quatre.
Donc un vrai depot, dans le service qui existe deja. LocalDirs d apt-cacher-ng publie
un repertoire du disque sous un prefixe, eprouve AVANT d ecrire le role. Aucun service,
aucun port, aucun certificat, aucun flux nouveaux : l ingress 3142 pair flotte couvre
exactement ce chemin.
Les versions ne sont pas recopiees : le role lit les defauts des quatre consommateurs.
Les quatre roles recoivent une tache AJOUTEE, placee avant leur stat de cache — si le
depot sert, le stat le voit et la tache amont se saute d elle-meme. Aucune tache
existante n a change.
P70 exige que tout dest ecrit sous un cache_local figure au depot. Une liste qui suit
une autre prend du retard ; celle-ci est nee avec sa garde.
Deux marches payees en chemin :
- failed_when: false REECRIT le verdict, donc la premiere garde de signature ne
gardait rien. Elles mesurent le fichier desormais.
- file: state=directory cree les parents en 0750 : apt-cacher-ng, qui ne tourne pas
en root, rendait 403 sur chaque fichier. Un chemin se traverse en entier.
Verifie sur l infrastructure : 6/6 artefacts servis (200/206) depuis le runner du site
ET depuis une machine du locataire a travers la frontiere ; les 6 empreintes SHA-256
sont identiques a celles qui ont construit Chezlepro ; second passage changed=0.
make prouver : 69 OK, 0 echec, 1 saute. ansible-lint : 0 failure, profil production.
Inclut aussi force: true sur cinq telechargements de cles : une reprise conditionnelle
ne reprend rien (304 Not Modified, size 0, attempts 5).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-12 23:45:00 -04:00
|
|
|
if _cache_site:
|
|
|
|
|
communes["setops_depot_binaires"] = f"http://{_cache_site}:3142/setops-binaires"
|
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])
|
2026-09-01 22:16:20 -04:00
|
|
|
# L'identite de sauvegarde DU SITE (moitie publique). Le depot du site l'autorise
|
|
|
|
|
# pour ses propres machines ; les locataires ont chacun la leur, ailleurs.
|
|
|
|
|
if intrants.get("backup_pubkey"):
|
|
|
|
|
communes["serveur_backup_pubkey"] = str(intrants["backup_pubkey"])
|
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
|
|
|
if intrants.get("cle_ssh"):
|
|
|
|
|
communes["ansible_ssh_private_key_file"] = intrants["cle_ssh"]
|
le site pouvait inseminer un locataire, pas se configurer lui-meme
Deux defauts qui s additionnaient, et un message qui accusait le mauvais.
1. Le site n avait AUCUN moyen d autoriser une cle d administration. Un
locataire declare ssh_baseline_cles_admin ; cote site, rien ne transmettait
cette liste. Ses machines n autorisaient que la cle de cloud-init.
2. Le rebond etait pose sans condition. Depuis le poste c est juste — aucune
route directe. Depuis le runner du site, qui vit DANS le site, c est un
detour qui casse : il doit s authentifier aupres de la frontiere ou sa cle
n est pas autorisee.
Et OpenSSH rend alors Host key verification failed / Connection closed by
UNKNOWN port 65535 — un message qui accuse les cles d HOTE alors que l echec
est une AUTHENTIFICATION, et sur le SAUTEUR, pas sur la cible.
Le critere du rebond est desormais es-tu une MACHINE du site, pas es-tu dans un
RESEAU du site : le poste porte 10.37.0.17, donc il est dans le reseau
management, et il aurait perdu son rebond alors qu il en a besoin.
L amorcage est circulaire et se rompt par le poste : lui seul entrait, il pose
la cle, le runner est ensuite autonome. Verifie : 5/5 machines du site.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-14 10:08:50 -04:00
|
|
|
# LE SITE N'AVAIT AUCUN MOYEN D'AUTORISER UNE CLE D'ADMINISTRATION (2026-09-14).
|
|
|
|
|
#
|
|
|
|
|
# Un locataire declare `ssh_baseline_cles_admin` dans ses group_vars, et `ssh_baseline`
|
|
|
|
|
# les pose sur toute sa flotte. Cote site, rien ne transmettait cette liste : ses
|
|
|
|
|
# machines n'autorisaient donc que la cle posee par cloud-init a leur naissance.
|
|
|
|
|
#
|
|
|
|
|
# CE QUE CA COUTAIT, ET C'EST STRUCTUREL : le runner du SITE ne pouvait entrer sur
|
|
|
|
|
# AUCUNE machine du site. Il sait inseminer un locataire — c'est prouve — et il ne
|
|
|
|
|
# savait pas configurer l'hebergeur qui le porte. Tout le site avait ete deploye
|
|
|
|
|
# depuis le poste de l'exploitant, et rien ne disait que c'etait la seule voie.
|
|
|
|
|
if intrants.get("cles_admin"):
|
|
|
|
|
communes["ssh_baseline_cles_admin"] = intrants["cles_admin"]
|
|
|
|
|
# LE REBOND NE SERT QU'A CELUI QUI EST DEHORS (2026-09-14).
|
|
|
|
|
#
|
|
|
|
|
# Il etait pose sans condition. Depuis le poste de l'exploitant c'est juste : aucune
|
|
|
|
|
# route directe ne mene aux zones du site. Depuis le runner du site — qui vit DANS une
|
|
|
|
|
# de ces zones — c'est un detour qui casse : il doit s'authentifier aupres de la
|
|
|
|
|
# frontiere, ou sa cle n'est pas autorisee, et OpenSSH rend alors
|
|
|
|
|
#
|
|
|
|
|
# Host key verification failed.
|
|
|
|
|
# Connection closed by UNKNOWN port 65535
|
|
|
|
|
#
|
|
|
|
|
# un message qui accuse les cles d'HOTE alors que l'echec est une authentification, et
|
|
|
|
|
# sur le SAUTEUR, pas sur la cible. Une heure de fausse piste.
|
|
|
|
|
#
|
|
|
|
|
# ON NE DEMANDE PAS UN INTRANT DE PLUS : le controleur sait ou il est. S'il porte deja
|
|
|
|
|
# une adresse dans un reseau du site, il n'a personne a sauter.
|
|
|
|
|
if intrants.get("rebond") and not _controleur_est_une_machine_du_site():
|
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
|
|
|
|
le site surveille enfin sa fabric
La supervision du site voyait ses sept VM et rien d autre. Au depart, depuis
site-mon-01, les trois hyperviseurs, les neuf pattes de la frontiere et sa
PROPRE passerelle par defaut rendaient tous 100 pourcent de perte au ping.
16 hotes UP sur 16 dont 9 pattes de frontiere en controle ACTIF
11 cibles Prometheus dont 3 hyperviseurs, job fabric separe
LE PARTAGE. Les hyperviseurs portent node_exporter - D-48 l autorise - et
exposent 1005 unites systemd avec leur etat, soit ce que la sonde sante
mesurait, plus la charge et le disque. Ils n entrent PAS dans le socle :
leur appliquer serveur_durci reecrirait le pare-feu, le SSH et les sysctl
de la machine qui tient tout le reste. La frontiere, elle, n accueille
aucun agent : controle actif, une entree par PATTE, parce qu une interface
eteinte coupe une zone pendant que les autres vont bien.
ON TIRE, ON NE POUSSE PAS. J avais propose du passif et il avait ete
valide ; la mesure a dit non. Un hyperviseur envoie vers un routeur qui ne
connait pas les reseaux du site, et sa route par defaut est GELEE (D-57).
Le porteur de sante y expirait en 20 s.
LE VRAI DEFAUT ETAIT UNE LISTE QUI N A PAS SUIVI. Le mecanisme de routage
existait deja sur vmbr0, avec un commentaire du 2026-08-26 tenant
exactement le raisonnement qu on venait de refaire. Sa liste s arretait a
10.0.34.0/24 quand le site en declare six : les zones sauvegarde et
supervision sont nees, les routes n ont pas suivi. Le symptome ne
ressemblait pas a une route manquante - il ressemblait a un pare-feu, puis
a un probleme de reseau chez l exploitant.
make routes-fabric-etat compare desormais TROIS choses : zones declarees,
routes declarees dans /etc/network/interfaces, routes vivantes dans le
noyau. Le cas le plus traitre est vivante mais non declaree : tout
fonctionne, la supervision est verte, et la panne attend la prochaine
maintenance. Eprouvee dans les deux sens.
Deux defauts trouves en construisant. bifrost-2 est un nom RESERVE, pas un
boitier - l underlay le disait en prose, illisible par le moteur ; il porte
desormais etat: reserve. Et un service passif n existe que pour un hote qui
peut POUSSER : l appartenance a un groupe sert deux choses qui ne
coincident pas toujours, a qui l on deploie et de qui l on attend un
rapport.
Les commutateurs restent dehors, choix de l exploitant, coherent avec D-48.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 18:50:43 -04:00
|
|
|
# LES ADRESSES DE LA FABRIC, pour que Prometheus puisse la TIRER (2026-09-10).
|
|
|
|
|
#
|
|
|
|
|
# Le job principal de Prometheus vaut `client_metrique ∩ hotes_actifs`, et les
|
|
|
|
|
# hyperviseurs sont volontairement HORS de `hotes_actifs` : ce groupe sert aux
|
|
|
|
|
# operations de flotte (`deployer-tout`), et un hyperviseur n'est pas une VM de la
|
|
|
|
|
# flotte. L'intersection les excluait donc, alors meme qu'ils portent node_exporter.
|
|
|
|
|
#
|
|
|
|
|
# `serveur_prometheus_cibles_supplementaires` est le point d'extension prevu par le
|
|
|
|
|
# role. On y met la fabric sous son PROPRE job : elle n'est pas la flotte, et un
|
|
|
|
|
# tableau qui les melange ferait croire a un parc homogene.
|
|
|
|
|
_fabric_metriques = sorted(
|
|
|
|
|
f"{h['ip']}:9100" for h in U.hotes(u)
|
|
|
|
|
if str(h.get("role")) == "hyperviseur" and str(h.get("ip", "")).startswith("192.168.11."))
|
|
|
|
|
|
|
|
|
|
# CE QUE LA SUPERVISION IRA VOIR : chaque patte de la frontiere, nommee par la zone
|
|
|
|
|
# qu'elle sert. Les commutateurs restent dehors — D-48 les tient hors flotte, et rien
|
|
|
|
|
# ne les rend interrogeables sans leur ouvrir un acces qu'on ne veut pas leur ouvrir.
|
|
|
|
|
_fabric_surveillee = []
|
|
|
|
|
for _h in U.hotes(u):
|
|
|
|
|
if str(_h.get("role")) != "frontiere":
|
|
|
|
|
continue
|
|
|
|
|
# UN NOM RESERVE N'EST PAS UN EQUIPEMENT. `bifrost-2` est declare pour que le lien
|
|
|
|
|
# de transit soit documente et que le nom soit pris — le boitier n'existe pas.
|
|
|
|
|
# Le surveiller donnerait deux CRITICAL permanents, et une alarme toujours rouge
|
|
|
|
|
# est une alarme qu'on cesse de lire.
|
|
|
|
|
if str(_h.get("etat") or "actif") != "actif":
|
|
|
|
|
continue
|
|
|
|
|
_ip = str(_h.get("ip") or "")
|
|
|
|
|
_res = str(_h.get("reseau") or "")
|
|
|
|
|
if not _ip:
|
|
|
|
|
continue
|
|
|
|
|
_fabric_surveillee.append({
|
|
|
|
|
"nom": f"frontiere-{_ip.replace('.', '-')}",
|
|
|
|
|
"adresse": _ip,
|
|
|
|
|
"role": f"frontiere — patte {_res or _ip}",
|
|
|
|
|
})
|
|
|
|
|
_fabric_surveillee.sort(key=lambda x: x["adresse"])
|
|
|
|
|
|
la frontiere journalise a nouveau, et une garde veille cette fois
Doctrine posee par l exploitant : des lors que le site a son Loki, la
journalisation de la frontiere et des hyperviseurs doit y etre dirigee.
CE QUI ETAIT CASSE. La destination syslog d OPNsense pointait
10.17.20.11:3100 - une adresse de TENANT, sur le port de Loki, en UDP, ce
que Loki ne sait pas lire. La VM a ete rasee ; une cible morte a arrete
TOUTE la journalisation pendant dix jours. La destination a ete eteinte
pour reparer et jamais remplacee. Deux fautes en une : plus de journaux, et
une cible chez un locataire, ce que D-87 refuse dans l autre sens.
L intention etait juste - bonnes facilites, info et au-dessus. On l a
REPRISE telle quelle et change uniquement la destination : ces choix
avaient ete faits, ce n etait pas a nous de les redecider.
UN TRADUCTEUR, parce que Loki ne parle pas syslog : loki.source.syslog dans
l Alloy qui tourne DEJA sur le collecteur. Port 1514 et non 514, donc
ecoute sans privilege - un collecteur qui aurait besoin des droits du
systeme pour entendre un equipement serait un mauvais echange.
UNE REGLE DE RELABEL SANS GARDE N IGNORE PAS : ELLE EFFACE. Trois quarts d
heure sur un symptome absurde - la configuration deposee portait
host = "bifrost-1", visible dans le fichier, et le flux arrivait sans
etiquette. Sans regex, la regle correspond TOUJOURS, meme quand sa source n
existe pas, et pose host = "". Loki jette une etiquette vide, et celle de l
ecouteur disparaissait avec elle.
Et la verification finale a corrige une seconde erreur, la mienne :
label/host/values rendait une liste en CACHE. La requete directe rendait
bien un flux. Un index qui ne montre pas une chose ne prouve pas qu elle n
existe pas.
LA GARDE, qui manquait depuis l incident. journaux-frontiere demande a LOKI
ce qu il a RECU, pas a la frontiere ce qu elle croit avoir envoye : la
destination est le seul juge, et un emetteur qui parle a un trou noir se
porte tres bien. La fenetre est DERIVEE du debit observe - environ 1500
lignes par minute - pas choisie.
Trois etats eprouves sur une copie : frontiere muette rc=2, Loki
injoignable rc=3, debit normal rc=0. Le 3 compte autant que le 2 : « je ne
peux rien affirmer » n est pas « c est casse ».
Mesure : 46 681 lignes recues en 30 min, site a 68 services tous OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 19:38:19 -04:00
|
|
|
# Le nom de la frontiere, tel que la carte le donne — pour etiqueter ses journaux.
|
|
|
|
|
_nom_frontiere = next((str(h.get("nom")) for h in U.hotes(u)
|
|
|
|
|
if h.get("role") == "frontiere"
|
|
|
|
|
and str(h.get("etat") or "actif") == "actif"), "frontiere")
|
|
|
|
|
|
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 heure vient de la frontiere : la derniere dependance vivante tombe
Mesure sur les quatorze : aucune sortie TCP vers une adresse publique, apt
par le cache, noms autoritaires en local, unattended-upgrades masked. Et
quatre pairs NTP publics par machine. Le role chrony posait le fuseau et
installait le demon sans jamais toucher a ses sources : le defaut de Debian
tenait depuis le premier jour, herite et jamais choisi.
L autorite est la frontiere, par decision de l exploitant. Elle etait deja
stratum 2 et ecoutait en 123 ; il ne manquait que le passage. 9 anciennes
regles port 123 vers !SETOPS_INTERNES retirees, 9 regles nommees vers
SETOPS_FRONTIERE posees : le changement resserre autant qu il centralise.
Deux chemins parce que la topologie en a deux. Un tenant n atteint pas la
frontiere par sa passerelle de zone — tenue par le SDN — mais par le lien de
transit. Une machine du site a la frontiere pour passerelle directe. Les deux
valeurs sont derivees de l underlay, via reseau_transit() plutot que d un
prefixe d adresse qui aurait menti chez le prochain hebergeur.
La patte face aux tenants manquait a opnsense_if_zones, pour la meme raison
que grappe-controle la veille.
Sonde horloge ecrite en meme temps : synchronisee ET contre la source
DECLAREE. Une machine peut etre parfaitement a l heure contre quatre serveurs
publics — c est exactement l etat d avant.
14 tenant + 7 site, toutes disciplinees, sources publiques = 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-11 12:08:13 -04:00
|
|
|
# La frontiere, autorite de temps — sa patte SUR LA ZONE DE CETTE MACHINE.
|
|
|
|
|
"chrony_serveurs": ([_temps_par_reseau[str(srv.get("reseau"))]]
|
|
|
|
|
if str(srv.get("reseau")) in _temps_par_reseau else []),
|
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
|
|
|
# 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),
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
# LE PARE-FEU DE MACHINE, EN DEUX TEMPS (2026-09-02).
|
|
|
|
|
#
|
|
|
|
|
# `serveur_durci` porte `nftables_baseline`, qui refuse tout ce qui n'est pas
|
|
|
|
|
# declare. Chez un tenant, trois pieces le rendent applicable : un registre de
|
|
|
|
|
# flux resolu POUR SES HOTES, un chemin ou lire le jeu de regles genere, et un
|
|
|
|
|
# `nftables_admin_ssh` qui dit par ou l'administration entre.
|
|
|
|
|
#
|
|
|
|
|
# Le site n'en avait aucune : `resoudre_flux.py nftables` n'ecrit que pour
|
|
|
|
|
# l'instance montee, et le chemin par defaut (`inventory_dir/../../`) sort du
|
|
|
|
|
# depot pour un inventaire DYNAMIQUE, dont le `inventory_dir` est `scripts/`.
|
|
|
|
|
#
|
|
|
|
|
# Poser le pare-feu avant ces pieces, ce serait appliquer une politique
|
|
|
|
|
# `drop` sans aucune regle : les six machines du site tomberaient d'un coup,
|
|
|
|
|
# et le deploiement qui les repare passe justement par SSH. On pose donc les
|
|
|
|
|
# neuf autres roles d'abord — ils ne coupent rien — et le pare-feu ensuite,
|
|
|
|
|
# quand il aura de quoi laisser passer.
|
|
|
|
|
"nftables_baseline_enabled": True,
|
|
|
|
|
# LE CHEMIN DU JEU DE REGLES, DIT ICI PARCE QUE LE DEFAUT NE PEUT PAS LE DIRE.
|
|
|
|
|
#
|
|
|
|
|
# `nftables_baseline` derive son chemin de `inventory_dir` — juste pour un
|
|
|
|
|
# tenant, dont l'inventaire est un FICHIER sous `instance/inventories/...`.
|
|
|
|
|
# Le site a un inventaire DYNAMIQUE : son `inventory_dir` est `scripts/`, et
|
|
|
|
|
# le defaut pointait donc un cran AU-DESSUS du depot, sur un chemin qui
|
|
|
|
|
# n'existe pas. Le role serait tombe sur son repli — une politique `drop`
|
|
|
|
|
# sans les regles resolues.
|
|
|
|
|
#
|
|
|
|
|
# Les regles du site vivent a cote de son plan, comme sa carte et sa voute :
|
|
|
|
|
# `make flux` les y ecrit (`resoudre_flux.py nftables-site`).
|
|
|
|
|
"nftables_baseline_ruleset_genere": str(
|
|
|
|
|
(U.chemin().resolve().parent / "flux-genere" / f"{nom}.nft")
|
|
|
|
|
if U.chemin() else ""),
|
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
|
|
|
# Ce que le marqueur verifiera : la liste ci-dessus les admet-elle vraiment ?
|
|
|
|
|
"serveur_resolveur_site_supernets_tenants": _supernets_tenants,
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
# `MaxStartups` COMPTE LES CONNEXIONS NON AUTHENTIFIEES, et un depot de SITE
|
|
|
|
|
# en voit la somme de tous ses locataires — dont les minuteurs se reveillent
|
|
|
|
|
# a la meme heure. Au-dela du seuil, sshd coupe AVANT la banniere : le client
|
|
|
|
|
# rapporte `kex_exchange_identification: Connection reset by peer`, qui
|
|
|
|
|
# accuse le reseau pour un refus applicatif. Douze sauvegardes simultanees,
|
|
|
|
|
# une perdue (2026-09-01).
|
|
|
|
|
#
|
|
|
|
|
# EN host_var, ET NON EN PARAMETRE DE DEPENDANCE (regression du 2026-09-02).
|
|
|
|
|
# La valeur vivait dans le `meta` de `serveur_backup_site` : elle ne portait
|
|
|
|
|
# donc que dans le play de CE role. Le jour ou `serveur_durci` a apporte
|
|
|
|
|
# `ssh_hardening` a toutes les machines du site, son passage — le dernier — a
|
|
|
|
|
# remis le depot a la valeur par defaut, sans rien signaler. Un reglage qui
|
|
|
|
|
# depend de l'ordre des plays est un reglage qui reviendra en arriere.
|
|
|
|
|
#
|
|
|
|
|
# Ici, tout play qui applique `ssh_hardening` a cet hote la voit.
|
2026-09-02 17:48:19 -04:00
|
|
|
# A qui la supervision parle. Vide, le role le SIGNALE au deploiement.
|
|
|
|
|
"serveur_icinga_destinataire": str(intrants.get("supervision_courriel") or ""),
|
supervision du site, et la panne qui dormait dans un mot
Le site a son temoin : site-mon-01 (VLAN 36) porte PostgreSQL, Icinga et un
relais. Le depot lui rapporte, et son premier verdict fut un vrai defaut —
site-mon-01 n avait jamais depose son propre etat. Les trois sont au vert.
serveur_backup_verification_locale repasse a true sur le depot : le drapeau ne
dit plus on renonce mais verifie ce que tu peux ouvrir . La liste des noeuds
attendus derive de l inventaire ou tourne le role, donc du site seul.
serveur_postfix gagne un mode relais : un site n heberge aucune boite, il
expedie. Directement par sa frontiere — emprunter le MTA d un locataire ferait
dependre l hebergeur d un ecosysteme qu il peut outvivre.
LA PANNE DE FOND : le modele de routes OPNsense lit enabled ; on lui envoyait
disabled=0, un champ ignore. enabled restait a son defaut, ETEINT. Chaque route
creee par Set-OPS depuis l origine l etait desactivee — invisible, parce qu une
route eteinte EXISTE dans le modele et compte posee . Le rechargement lance
pour activer la nouvelle patte a fait reprendre au noyau sa table depuis le
modele : 14 routes sur 15 disparues, 11 machines de Chezlepro injoignables le
lendemain de sa reconstruction, et le devis toujours vert.
Trois corrections empilees : la cause (enabled), la cecite (le plan lit la table
du NOYAU et signale absent ou eteint), l inaction (la reconfiguration ne se
declenchait que sur un changement du modele).
Aussi : un role du site peut enfin en appeler un autre a travers la frontiere —
le devis traitait tout pair nomme comme l exterieur . Et nftables_admin_ssh se
DERIVE de la carte : recopie a la main, il retardait d une zone, et m a
verrouille dehors de la machine que je venais de creer.
Reste ouvert : le destinataire des alertes est encore root@localhost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 17:34:01 -04:00
|
|
|
# LE POSTFIX DU SITE EXPEDIE, IL NE RECOIT PAS.
|
|
|
|
|
#
|
|
|
|
|
# `serveur_postfix` est d'abord le serveur de courriel d'un ecosysteme : il
|
|
|
|
|
# resout ses destinataires dans un annuaire LDAP et remet a Dovecot. Le site
|
|
|
|
|
# n'a ni l'un ni l'autre, et n'heberge aucune boite — il lui faut seulement
|
|
|
|
|
# de quoi faire SORTIR une alerte, en son propre nom.
|
|
|
|
|
"serveur_postfix_mode": "relais",
|
observabilite : le site cesse de ne rien voir de lui-meme
D-87 disait que l hebergeur n a pas le droit de voir les journaux de ses
locataires. La decision avait une face cachee : a force de refuser de voir
ceux des autres, le site s etait prive des siens. Ses sept machines
n expediaient nulle part.
Il prend donc sa propre pile : prometheus, loki et grafana sur site-mon-01,
sans aucun lien avec ceux d un tenant. L exemption qui bloquait portait sa
propre condition de levee, ecrite cinq jours plus tot dans le plan.
Grafana au site n a pas de SSO, et le role l ignorait : il reclamait l IdP
avant de regarder s il en voulait un. L interrupteur existait, il n etait
pas honore. Le role refuse desormais SSO eteint ET formulaire local eteint
- la combinaison deploie un Grafana en sante ou personne ne peut entrer -
et le reglage du formulaire sort du if du SSO, ou il disparaissait en
laissant le defaut amont decider en silence.
Deux manques se cachaient l un l autre dans le devis de la frontiere, et
Prometheus voyait 1 cible sur 7 :
- le devis derivait les groupes d une machine du site de applications.yml
seul, et ne voyait donc aucune integration universelle - alors que
client_metrique ouvre un port d ecoute ;
- une sortie vers un role du site visait !SETOPS_INTERNES, qui exclut
precisement la machine nommee. Le devis autorisait a expedier les
journaux du site a n importe quel Loki du monde, et a nul autre endroit
qu a celui-la. Neuf flux dans ce cas.
Et deux declarations justes ne font qu une regle : appliquer_opnsense pose
tout en direction: in (D-61).
Les deux agents se supervisent enfin eux-memes. La sonde des journaux ne
demande pas si Alloy tourne, elle lit ce qu il a du JETER. Elle a fait ses
preuves le jour meme : Alloy actif, /-/ready a 200, et cinquante lignes
perdues sur six machines.
Mesure : metriques 7/7 vert, journaux 1/7 - les six autres attendent la
regle de frontiere, et le disent. prouver 64 OK, ansible-lint 0 defaut.
Reste a la main de l exploitant : make frontiere-appliquer CONFIRMER=true,
et le secret vault_grafana_admin a deposer dans underlay.vault.yml.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 04:43:01 -04:00
|
|
|
# LE GRAFANA DU SITE N'A PAS DE SSO — pas par oubli, par D-87.
|
|
|
|
|
#
|
|
|
|
|
# Le SSO d'un tenant supposerait que le site emprunte l'identite d'un
|
|
|
|
|
# ecosysteme qu'il doit pouvoir survivre ; un Keycloak DU site ferait
|
|
|
|
|
# remonter l'identite dans le site, exactement ce que D-87 refuse. Reste
|
|
|
|
|
# le compte local, que le site atteint par `sudo` sur l'hote — et il faut
|
|
|
|
|
# alors REOUVRIR le formulaire, que le mode SSO ferme.
|
|
|
|
|
#
|
|
|
|
|
# Les deux lignes vont ensemble : eteindre l'une sans allumer l'autre
|
|
|
|
|
# laisse un Grafana sans aucune porte. Le role refuse desormais cette
|
|
|
|
|
# combinaison, plutot que de la deployer.
|
le site reconstruit depuis zero : 7/7, 0 echec, et seize corrections
SITE-Chezlepro n avait jamais ete rase. La limite qu on repetait partout —
l infrastructure d accueil n a jamais ete reconstruite depuis zero — se
lisait comme de la prudence. C etait seize defauts que rien d autre n aurait
pu reveler.
Un locataire naît dans un monde deja peuple : le site lui fournit paquets,
noms, genome, heure et depot. Un site n a personne au-dessus, sauf sa
frontiere. Onze des seize murs viennent de la.
DEUX CAPACITES QUI N EXISTAIENT PAS. make site-raser — rien ne detruisait les
machines du site, donc la limite etait un trou d outillage. make
forge-amorcer — la forge naît vide et le runner y clone ; l amorcage part du
poste, seul endroit qui detienne alors le genome.
TROIS GARDES QUI VERIFIAIENT LA FORME. La plus couteuse des familles : elles
donnent l apparence d une verification. Le resolveur comparait des adresses
au lieu de mesurer si la resolution aboutit, et protegeait ainsi l etat
casse. L administration etait reconnue a son port. Un flux a deux paires
n obtenait qu une branche.
UN ECART DE SECURITE. Le PostgreSQL du site servait le certificat auto-signe
de Debian, sans reseaux autorises ni hostssl — invisible tant qu aucun client
n exigeait la verification. Le defaut n a pas casse la construction : la
construction a revele le defaut.
UN ACCES ACCIDENTEL. Celui de l exploitant tenait au chevauchement d
adressage que le renumerotage a supprime. Separer les index n a pas cause le
probleme, il a retire le hasard qui le masquait.
La sequence du premier jour est ecrite : runbooks §9, avec les seize murs et
ce que chacun enseigne.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-12 19:08:31 -04:00
|
|
|
# LE POSTGRESQL DU SITE ETAIT HORS DE LA DOCTRINE (2026-09-12).
|
|
|
|
|
#
|
|
|
|
|
# `serveur_postgresql_tls_actif` vaut `false` par defaut, et le tenant le
|
|
|
|
|
# met a `true` dans ses group_vars. Le SITE ne le mettait nulle part : son
|
|
|
|
|
# PostgreSQL servait le certificat AUTO-SIGNE du paquet Debian
|
|
|
|
|
# (`ssl-cert-snakeoil.pem`), pas celui de son AC.
|
|
|
|
|
#
|
|
|
|
|
# PERSONNE NE L'AVAIT VU parce qu'aucun client du site n'exigeait la
|
|
|
|
|
# verification. Le premier a l'exiger — l'import du schema Icinga DB, qui
|
|
|
|
|
# se connecte par le FQDN — a echoue sur `certificate verify failed`, et
|
|
|
|
|
# c'est ainsi que l'ecart s'est montre. Le defaut n'a pas casse la
|
|
|
|
|
# construction : la construction a revele le defaut.
|
|
|
|
|
#
|
|
|
|
|
# Ce n'est pas un blocage d'amorcage, c'est un trou de zero-confiance
|
|
|
|
|
# est-ouest : le site chiffrait sans que personne ne verifie a qui.
|
|
|
|
|
# ET LES CLIENTS DOIVENT ETRE NOMMES. Le tenant declare
|
|
|
|
|
# `serveur_postgresql_reseaux_autorises: [supernet]` ; le site ne declarait
|
|
|
|
|
# rien, donc `pg_hba` n'autorisait que le local :
|
|
|
|
|
#
|
|
|
|
|
# aucune entree dans pg_hba.conf pour l hote 10.37.36.11,
|
|
|
|
|
# utilisateur icingadb, chiffrement SSL
|
|
|
|
|
#
|
|
|
|
|
# Les zones du site, et elles seules — pas un `0.0.0.0/0` qui rendrait le
|
|
|
|
|
# verrou `hostssl` decoratif.
|
|
|
|
|
"serveur_postgresql_reseaux_autorises": _zones_du_site,
|
|
|
|
|
# `tls_force` pose `hostssl` : toute connexion NON chiffree est refusee.
|
|
|
|
|
# On ne l'active qu'apres avoir confirme que le certificat servi vient bien
|
|
|
|
|
# de l'AC — c'est fait, mesure a 17:31 ce jour.
|
|
|
|
|
"serveur_postgresql_tls_force": True,
|
|
|
|
|
"serveur_postgresql_tls_actif": True,
|
observabilite : le site cesse de ne rien voir de lui-meme
D-87 disait que l hebergeur n a pas le droit de voir les journaux de ses
locataires. La decision avait une face cachee : a force de refuser de voir
ceux des autres, le site s etait prive des siens. Ses sept machines
n expediaient nulle part.
Il prend donc sa propre pile : prometheus, loki et grafana sur site-mon-01,
sans aucun lien avec ceux d un tenant. L exemption qui bloquait portait sa
propre condition de levee, ecrite cinq jours plus tot dans le plan.
Grafana au site n a pas de SSO, et le role l ignorait : il reclamait l IdP
avant de regarder s il en voulait un. L interrupteur existait, il n etait
pas honore. Le role refuse desormais SSO eteint ET formulaire local eteint
- la combinaison deploie un Grafana en sante ou personne ne peut entrer -
et le reglage du formulaire sort du if du SSO, ou il disparaissait en
laissant le defaut amont decider en silence.
Deux manques se cachaient l un l autre dans le devis de la frontiere, et
Prometheus voyait 1 cible sur 7 :
- le devis derivait les groupes d une machine du site de applications.yml
seul, et ne voyait donc aucune integration universelle - alors que
client_metrique ouvre un port d ecoute ;
- une sortie vers un role du site visait !SETOPS_INTERNES, qui exclut
precisement la machine nommee. Le devis autorisait a expedier les
journaux du site a n importe quel Loki du monde, et a nul autre endroit
qu a celui-la. Neuf flux dans ce cas.
Et deux declarations justes ne font qu une regle : appliquer_opnsense pose
tout en direction: in (D-61).
Les deux agents se supervisent enfin eux-memes. La sonde des journaux ne
demande pas si Alloy tourne, elle lit ce qu il a du JETER. Elle a fait ses
preuves le jour meme : Alloy actif, /-/ready a 200, et cinquante lignes
perdues sur six machines.
Mesure : metriques 7/7 vert, journaux 1/7 - les six autres attendent la
regle de frontiere, et le disent. prouver 64 OK, ansible-lint 0 defaut.
Reste a la main de l exploitant : make frontiere-appliquer CONFIRMER=true,
et le secret vault_grafana_admin a deposer dans underlay.vault.yml.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 04:43:01 -04:00
|
|
|
"serveur_grafana_oidc_actif": False,
|
|
|
|
|
"serveur_grafana_connexion_locale": True,
|
2026-09-02 11:21:52 -04:00
|
|
|
# Le depot du site, pour les machines du site qui y deposent. Vide s'il n'y
|
|
|
|
|
# en a pas — `client_backup` refuse alors, plutot que de viser un nom qui
|
|
|
|
|
# ne repond pas : mieux vaut un deploiement qui s'arrete qu'une sauvegarde
|
|
|
|
|
# qu'on croit faite.
|
|
|
|
|
**({"client_backup_cible": _cible_backup,
|
|
|
|
|
"client_backup_utilisateur_distant": "restic"} if _cible_backup else {}),
|
2026-09-01 22:16:20 -04:00
|
|
|
# Les ecosystemes qui deposent leur etat ici, un compte Unix chacun.
|
|
|
|
|
"serveur_backup_site_locataires": (
|
|
|
|
|
_locataires_backup
|
|
|
|
|
if "serveur_backup_site" in _groupes_de(nom) else []),
|
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,
|
la frontiere journalise a nouveau, et une garde veille cette fois
Doctrine posee par l exploitant : des lors que le site a son Loki, la
journalisation de la frontiere et des hyperviseurs doit y etre dirigee.
CE QUI ETAIT CASSE. La destination syslog d OPNsense pointait
10.17.20.11:3100 - une adresse de TENANT, sur le port de Loki, en UDP, ce
que Loki ne sait pas lire. La VM a ete rasee ; une cible morte a arrete
TOUTE la journalisation pendant dix jours. La destination a ete eteinte
pour reparer et jamais remplacee. Deux fautes en une : plus de journaux, et
une cible chez un locataire, ce que D-87 refuse dans l autre sens.
L intention etait juste - bonnes facilites, info et au-dessus. On l a
REPRISE telle quelle et change uniquement la destination : ces choix
avaient ete faits, ce n etait pas a nous de les redecider.
UN TRADUCTEUR, parce que Loki ne parle pas syslog : loki.source.syslog dans
l Alloy qui tourne DEJA sur le collecteur. Port 1514 et non 514, donc
ecoute sans privilege - un collecteur qui aurait besoin des droits du
systeme pour entendre un equipement serait un mauvais echange.
UNE REGLE DE RELABEL SANS GARDE N IGNORE PAS : ELLE EFFACE. Trois quarts d
heure sur un symptome absurde - la configuration deposee portait
host = "bifrost-1", visible dans le fichier, et le flux arrivait sans
etiquette. Sans regex, la regle correspond TOUJOURS, meme quand sa source n
existe pas, et pose host = "". Loki jette une etiquette vide, et celle de l
ecouteur disparaissait avec elle.
Et la verification finale a corrige une seconde erreur, la mienne :
label/host/values rendait une liste en CACHE. La requete directe rendait
bien un flux. Un index qui ne montre pas une chose ne prouve pas qu elle n
existe pas.
LA GARDE, qui manquait depuis l incident. journaux-frontiere demande a LOKI
ce qu il a RECU, pas a la frontiere ce qu elle croit avoir envoye : la
destination est le seul juge, et un emetteur qui parle a un trou noir se
porte tres bien. La fenetre est DERIVEE du debit observe - environ 1500
lignes par minute - pas choisie.
Trois etats eprouves sur une copie : frontiere muette rc=2, Loki
injoignable rc=3, debit normal rc=0. Le 3 compte autant que le 2 : « je ne
peux rien affirmer » n est pas « c est casse ».
Mesure : 46 681 lignes recues en 30 min, site a 68 services tous OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 19:38:19 -04:00
|
|
|
# LE COLLECTEUR ECOUTE AUSSI CE QUI NE PEUT PAS POUSSER. La frontiere n'a pas
|
|
|
|
|
# d'agent : elle emet du syslog, et Alloy le traduit vers Loki. Active sur la
|
|
|
|
|
# machine qui PORTE Loki — un second recepteur ailleurs serait une seconde
|
|
|
|
|
# configuration a garder en phase.
|
|
|
|
|
**({"client_journal_syslog_ecoute": f"{srv.get('ip')}:1514",
|
|
|
|
|
"client_journal_syslog_emetteur": _nom_frontiere,
|
|
|
|
|
# La sonde interroge la MEME etiquette que celle posee sur l'ecouteur :
|
|
|
|
|
# une seule derivation, deux consommateurs.
|
|
|
|
|
"serveur_loki_sonde_frontiere_etiquette": _nom_frontiere}
|
|
|
|
|
if "serveur_loki" in _groupes_de(nom) else {}),
|
le site surveille enfin sa fabric
La supervision du site voyait ses sept VM et rien d autre. Au depart, depuis
site-mon-01, les trois hyperviseurs, les neuf pattes de la frontiere et sa
PROPRE passerelle par defaut rendaient tous 100 pourcent de perte au ping.
16 hotes UP sur 16 dont 9 pattes de frontiere en controle ACTIF
11 cibles Prometheus dont 3 hyperviseurs, job fabric separe
LE PARTAGE. Les hyperviseurs portent node_exporter - D-48 l autorise - et
exposent 1005 unites systemd avec leur etat, soit ce que la sonde sante
mesurait, plus la charge et le disque. Ils n entrent PAS dans le socle :
leur appliquer serveur_durci reecrirait le pare-feu, le SSH et les sysctl
de la machine qui tient tout le reste. La frontiere, elle, n accueille
aucun agent : controle actif, une entree par PATTE, parce qu une interface
eteinte coupe une zone pendant que les autres vont bien.
ON TIRE, ON NE POUSSE PAS. J avais propose du passif et il avait ete
valide ; la mesure a dit non. Un hyperviseur envoie vers un routeur qui ne
connait pas les reseaux du site, et sa route par defaut est GELEE (D-57).
Le porteur de sante y expirait en 20 s.
LE VRAI DEFAUT ETAIT UNE LISTE QUI N A PAS SUIVI. Le mecanisme de routage
existait deja sur vmbr0, avec un commentaire du 2026-08-26 tenant
exactement le raisonnement qu on venait de refaire. Sa liste s arretait a
10.0.34.0/24 quand le site en declare six : les zones sauvegarde et
supervision sont nees, les routes n ont pas suivi. Le symptome ne
ressemblait pas a une route manquante - il ressemblait a un pare-feu, puis
a un probleme de reseau chez l exploitant.
make routes-fabric-etat compare desormais TROIS choses : zones declarees,
routes declarees dans /etc/network/interfaces, routes vivantes dans le
noyau. Le cas le plus traitre est vivante mais non declaree : tout
fonctionne, la supervision est verte, et la panne attend la prochaine
maintenance. Eprouvee dans les deux sens.
Deux defauts trouves en construisant. bifrost-2 est un nom RESERVE, pas un
boitier - l underlay le disait en prose, illisible par le moteur ; il porte
desormais etat: reserve. Et un service passif n existe que pour un hote qui
peut POUSSER : l appartenance a un groupe sert deux choses qui ne
coincident pas toujours, a qui l on deploie et de qui l on attend un
rapport.
Les commutateurs restent dehors, choix de l exploitant, coherent avec D-48.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 18:50:43 -04:00
|
|
|
# LA FABRIC SURVEILLEE PAR CONTROLE ACTIF (2026-09-10).
|
|
|
|
|
#
|
|
|
|
|
# Un pare-feu n'accueille aucun agent : Icinga doit aller VOIR. Une entree par
|
|
|
|
|
# PATTE de la frontiere, parce que chaque zone depend de son interface a elle —
|
|
|
|
|
# une interface eteinte coupe une zone pendant que les autres vont bien.
|
|
|
|
|
**({"serveur_icinga_fabric": _fabric_surveillee}
|
|
|
|
|
if (_fabric_surveillee and "serveur_icinga" in _groupes_de(nom)) else {}),
|
|
|
|
|
# La fabric, en job separe — voir `_fabric_metriques` plus haut.
|
|
|
|
|
**({"serveur_prometheus_cibles_supplementaires":
|
|
|
|
|
[{"job": "fabric", "cibles": _fabric_metriques}]}
|
|
|
|
|
if (_fabric_metriques and "serveur_prometheus" in _groupes_de(nom)) else {}),
|
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
|
|
|
}
|
durcissement : le site recoit enfin serveur_durci
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.
Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
generateur refuse desormais de produire des regles sans cet intrant.
Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.
MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.
Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
|
|
|
for _socle in GROUPE_SOCLE:
|
|
|
|
|
groupes.setdefault(_socle, []).append(nom)
|
|
|
|
|
if "serveur_backup_site" in _groupes_de(nom):
|
|
|
|
|
hostvars[nom]["ssh_hardening_max_startups"] = "60:30:200"
|
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)
|
|
|
|
|
|
le site surveille enfin sa fabric
La supervision du site voyait ses sept VM et rien d autre. Au depart, depuis
site-mon-01, les trois hyperviseurs, les neuf pattes de la frontiere et sa
PROPRE passerelle par defaut rendaient tous 100 pourcent de perte au ping.
16 hotes UP sur 16 dont 9 pattes de frontiere en controle ACTIF
11 cibles Prometheus dont 3 hyperviseurs, job fabric separe
LE PARTAGE. Les hyperviseurs portent node_exporter - D-48 l autorise - et
exposent 1005 unites systemd avec leur etat, soit ce que la sonde sante
mesurait, plus la charge et le disque. Ils n entrent PAS dans le socle :
leur appliquer serveur_durci reecrirait le pare-feu, le SSH et les sysctl
de la machine qui tient tout le reste. La frontiere, elle, n accueille
aucun agent : controle actif, une entree par PATTE, parce qu une interface
eteinte coupe une zone pendant que les autres vont bien.
ON TIRE, ON NE POUSSE PAS. J avais propose du passif et il avait ete
valide ; la mesure a dit non. Un hyperviseur envoie vers un routeur qui ne
connait pas les reseaux du site, et sa route par defaut est GELEE (D-57).
Le porteur de sante y expirait en 20 s.
LE VRAI DEFAUT ETAIT UNE LISTE QUI N A PAS SUIVI. Le mecanisme de routage
existait deja sur vmbr0, avec un commentaire du 2026-08-26 tenant
exactement le raisonnement qu on venait de refaire. Sa liste s arretait a
10.0.34.0/24 quand le site en declare six : les zones sauvegarde et
supervision sont nees, les routes n ont pas suivi. Le symptome ne
ressemblait pas a une route manquante - il ressemblait a un pare-feu, puis
a un probleme de reseau chez l exploitant.
make routes-fabric-etat compare desormais TROIS choses : zones declarees,
routes declarees dans /etc/network/interfaces, routes vivantes dans le
noyau. Le cas le plus traitre est vivante mais non declaree : tout
fonctionne, la supervision est verte, et la panne attend la prochaine
maintenance. Eprouvee dans les deux sens.
Deux defauts trouves en construisant. bifrost-2 est un nom RESERVE, pas un
boitier - l underlay le disait en prose, illisible par le moteur ; il porte
desormais etat: reserve. Et un service passif n existe que pour un hote qui
peut POUSSER : l appartenance a un groupe sert deux choses qui ne
coincident pas toujours, a qui l on deploie et de qui l on attend un
rapport.
Les commutateurs restent dehors, choix de l exploitant, coherent avec D-48.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 18:50:43 -04:00
|
|
|
# --- LES HYPERVISEURS ENTRENT, MAIS SEULEMENT POUR ETRE VUS (2026-09-10) ----------
|
|
|
|
|
#
|
|
|
|
|
# L'Icinga du site ne connaissait que ses sept VM. Mesure : depuis `site-mon-01`, les
|
|
|
|
|
# trois hyperviseurs ET sa propre passerelle rendaient 100 % de perte au ping. La
|
|
|
|
|
# machine qui PORTE les VM etait invisible de la supervision qui les surveille.
|
|
|
|
|
#
|
|
|
|
|
# D-48 le permet, et le dit : « les hyperviseurs sont gerables par Ansible ; hors
|
|
|
|
|
# flotte ne vaut que pour les commutateurs et la frontiere ». Ils peuvent donc porter
|
|
|
|
|
# les integrations d'observation, ce qui vaut infiniment mieux qu'un ping : charge,
|
|
|
|
|
# disque, unites systemd en echec, certificat.
|
|
|
|
|
#
|
|
|
|
|
# ILS N'ENTRENT PAS DANS LE SOCLE, ET C'EST LE POINT DELICAT. Un hyperviseur Proxmox
|
|
|
|
|
# n'est pas une VM de la flotte : lui appliquer `serveur_durci` reecrirait son
|
|
|
|
|
# pare-feu, son SSH et ses sysctl — sur la machine qui tient tout le reste. Ils sont
|
|
|
|
|
# donc places dans leur PROPRE groupe, hors de `GROUPE_SOCLE` et hors de
|
|
|
|
|
# `GROUPE_FLOTTE` : `make site-appliquer GROUPE=serveur_durci` ne les touchera jamais,
|
|
|
|
|
# parce qu'ils n'y sont pas.
|
|
|
|
|
#
|
|
|
|
|
# Ce que ca coute a dire : un groupe qui recoit des roles doit etre nomme a chaque
|
|
|
|
|
# fois. C'est le prix d'un perimetre explicite, et il est moins cher qu'un
|
|
|
|
|
# durcissement applique par megarde a un hyperviseur.
|
|
|
|
|
# L'adresse du temoin, derivee comme tout le reste : l'hote qui porte `serveur_icinga`.
|
|
|
|
|
_ip_icinga = ""
|
|
|
|
|
for _n_app, _a_app in applications.items():
|
|
|
|
|
if str(_a_app.get("groupe")) == "serveur_icinga":
|
|
|
|
|
_s = serveurs.get(_a_app.get("hote")) or {}
|
|
|
|
|
_ip_icinga = str(_s.get("ip") or "")
|
|
|
|
|
break
|
|
|
|
|
|
une source vide n est pas « tout le monde »
Les journaux de la fabric, et le defaut de classe que leur mise en place a
revele. 10 hotes dans Loki (7 VM du site + 3 hyperviseurs), site a 67
services tous OK contre 51 avant.
METRIQUES TIREES, JOURNAUX POUSSES. Un scrape part du collecteur et doit
REVENIR : il exige un chemin symetrique, que la route par defaut gelee
interdit. Un push part de la source et n attend qu un accuse.
Trois trous dans les generateurs. Le devis de la frontiere ne connaissait
fabric qu en DESTINATION - un flux entrant tombait dans « rien d autre n
entre » sans rien dire. La regle etait posee sur la patte de la destination
alors que D-61 raisonne en ARRIVEE, et opt7 manquait a la table des zones.
Et le plus grave : fabric etait un mot RECONNU mais NON RESOLU. Source
vide, donc regle sans saddr - tcp dport 3100 accept, ouvert a tous. Un mot
reconnu mais non resolu est pire qu un mot inconnu : celui-ci serait refuse
a la validation, celui-la produit une porte grande ouverte qui a l air d un
flux precis.
LA CLASSE ENTIERE EST REFERMEE. Le defaut n etait pas propre a fabric :
toute paire nommant un ensemble et ne resolvant rien ouvrait le port.
Trouve en lisant les fichiers generes, l API de supervision d un tenant
etait ouverte a tous parce que pair: serveur_backup ne resout rien - cet
ecosysteme depose chez le site. expositions et externe, eux, veulent bien
dire tout le monde : c est une intention. Ailleurs, pas de source, pas de
regle, et le generateur le DIT.
Trois regles se referment, chacune verifiee AVANT d appliquer. Deux etaient
sans effet ; la troisieme portait Grafana, qui ecoute bien sur 3000. Sa
declaration disait pair: edge - juste chez un tenant, faux au site qui n a
pas d edge. Ajout de admin : ce qui etait accidentel devient explicite.
Et la mesure a montre autre chose : Grafana etait DEJA injoignable depuis
le poste, la frontiere ne laissant pas passer le 3000. Le site a une
console deployee et sans chemin d acces. Ce n est pas corrige ici, mais
c est dit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 19:14:23 -04:00
|
|
|
_ip_loki = ""
|
|
|
|
|
for _n_app, _a_app in applications.items():
|
|
|
|
|
if str(_a_app.get("groupe")) == "serveur_loki":
|
|
|
|
|
_ip_loki = str((serveurs.get(_a_app.get("hote")) or {}).get("ip") or "")
|
|
|
|
|
break
|
|
|
|
|
|
le site surveille enfin sa fabric
La supervision du site voyait ses sept VM et rien d autre. Au depart, depuis
site-mon-01, les trois hyperviseurs, les neuf pattes de la frontiere et sa
PROPRE passerelle par defaut rendaient tous 100 pourcent de perte au ping.
16 hotes UP sur 16 dont 9 pattes de frontiere en controle ACTIF
11 cibles Prometheus dont 3 hyperviseurs, job fabric separe
LE PARTAGE. Les hyperviseurs portent node_exporter - D-48 l autorise - et
exposent 1005 unites systemd avec leur etat, soit ce que la sonde sante
mesurait, plus la charge et le disque. Ils n entrent PAS dans le socle :
leur appliquer serveur_durci reecrirait le pare-feu, le SSH et les sysctl
de la machine qui tient tout le reste. La frontiere, elle, n accueille
aucun agent : controle actif, une entree par PATTE, parce qu une interface
eteinte coupe une zone pendant que les autres vont bien.
ON TIRE, ON NE POUSSE PAS. J avais propose du passif et il avait ete
valide ; la mesure a dit non. Un hyperviseur envoie vers un routeur qui ne
connait pas les reseaux du site, et sa route par defaut est GELEE (D-57).
Le porteur de sante y expirait en 20 s.
LE VRAI DEFAUT ETAIT UNE LISTE QUI N A PAS SUIVI. Le mecanisme de routage
existait deja sur vmbr0, avec un commentaire du 2026-08-26 tenant
exactement le raisonnement qu on venait de refaire. Sa liste s arretait a
10.0.34.0/24 quand le site en declare six : les zones sauvegarde et
supervision sont nees, les routes n ont pas suivi. Le symptome ne
ressemblait pas a une route manquante - il ressemblait a un pare-feu, puis
a un probleme de reseau chez l exploitant.
make routes-fabric-etat compare desormais TROIS choses : zones declarees,
routes declarees dans /etc/network/interfaces, routes vivantes dans le
noyau. Le cas le plus traitre est vivante mais non declaree : tout
fonctionne, la supervision est verte, et la panne attend la prochaine
maintenance. Eprouvee dans les deux sens.
Deux defauts trouves en construisant. bifrost-2 est un nom RESERVE, pas un
boitier - l underlay le disait en prose, illisible par le moteur ; il porte
desormais etat: reserve. Et un service passif n existe que pour un hote qui
peut POUSSER : l appartenance a un groupe sert deux choses qui ne
coincident pas toujours, a qui l on deploie et de qui l on attend un
rapport.
Les commutateurs restent dehors, choix de l exploitant, coherent avec D-48.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 18:50:43 -04:00
|
|
|
_hyperviseurs = {}
|
|
|
|
|
for _h in U.hotes(u):
|
|
|
|
|
if str(_h.get("role")) != "hyperviseur":
|
|
|
|
|
continue
|
|
|
|
|
_nom, _ip = str(_h.get("nom") or ""), str(_h.get("ip") or "")
|
|
|
|
|
if not _nom or not _ip:
|
|
|
|
|
continue
|
|
|
|
|
# UNE SEULE ADRESSE PAR HYPERVISEUR : l'underlay en declare trois par machine
|
|
|
|
|
# (administration, transport, fabric). On retient celle du plan d'ADMINISTRATION,
|
|
|
|
|
# la seule que le poste et le runner routent — les autres ne sont pas joignables
|
|
|
|
|
# d'ou l'on parle.
|
|
|
|
|
if _nom in _hyperviseurs:
|
|
|
|
|
continue
|
|
|
|
|
if not _ip.startswith("192.168.11."):
|
|
|
|
|
continue
|
|
|
|
|
_hyperviseurs[_nom] = _ip
|
|
|
|
|
|
|
|
|
|
for _nom, _ip in sorted(_hyperviseurs.items()):
|
|
|
|
|
hostvars[_nom] = {
|
|
|
|
|
"ansible_host": _ip,
|
|
|
|
|
"ansible_user": UTILISATEUR_DEFAUT,
|
|
|
|
|
"setops_site": True,
|
|
|
|
|
"setops_plan_dir": str(plan_dir() or ""),
|
|
|
|
|
# PAS DE PARE-FEU DERIVE : aucun `nftables_baseline_ruleset_genere`. Un
|
|
|
|
|
# hyperviseur porte son propre filtrage, pose par Proxmox et par l'exploitant.
|
|
|
|
|
#
|
|
|
|
|
# LE TEMOIN SE JOINT PAR SON ADRESSE, PAS PAR SON NOM. Le porteur de sante vise
|
|
|
|
|
# `<hote>.<domaine>`, ce qui suppose le plancher `/etc/hosts` du socle — que ces
|
|
|
|
|
# machines n'ont pas, et ne doivent pas avoir : Proxmox s'en sert pour l'identite
|
|
|
|
|
# de noeud du cluster. Mesure : `curl: (6) Could not resolve host` sur les trois.
|
|
|
|
|
**({"client_sante_icinga_url": f"https://{_ip_icinga}:5665"}
|
|
|
|
|
if _ip_icinga else {}),
|
une source vide n est pas « tout le monde »
Les journaux de la fabric, et le defaut de classe que leur mise en place a
revele. 10 hotes dans Loki (7 VM du site + 3 hyperviseurs), site a 67
services tous OK contre 51 avant.
METRIQUES TIREES, JOURNAUX POUSSES. Un scrape part du collecteur et doit
REVENIR : il exige un chemin symetrique, que la route par defaut gelee
interdit. Un push part de la source et n attend qu un accuse.
Trois trous dans les generateurs. Le devis de la frontiere ne connaissait
fabric qu en DESTINATION - un flux entrant tombait dans « rien d autre n
entre » sans rien dire. La regle etait posee sur la patte de la destination
alors que D-61 raisonne en ARRIVEE, et opt7 manquait a la table des zones.
Et le plus grave : fabric etait un mot RECONNU mais NON RESOLU. Source
vide, donc regle sans saddr - tcp dport 3100 accept, ouvert a tous. Un mot
reconnu mais non resolu est pire qu un mot inconnu : celui-ci serait refuse
a la validation, celui-la produit une porte grande ouverte qui a l air d un
flux precis.
LA CLASSE ENTIERE EST REFERMEE. Le defaut n etait pas propre a fabric :
toute paire nommant un ensemble et ne resolvant rien ouvrait le port.
Trouve en lisant les fichiers generes, l API de supervision d un tenant
etait ouverte a tous parce que pair: serveur_backup ne resout rien - cet
ecosysteme depose chez le site. expositions et externe, eux, veulent bien
dire tout le monde : c est une intention. Ailleurs, pas de source, pas de
regle, et le generateur le DIT.
Trois regles se referment, chacune verifiee AVANT d appliquer. Deux etaient
sans effet ; la troisieme portait Grafana, qui ecoute bien sur 3000. Sa
declaration disait pair: edge - juste chez un tenant, faux au site qui n a
pas d edge. Ajout de admin : ce qui etait accidentel devient explicite.
Et la mesure a montre autre chose : Grafana etait DEJA injoignable depuis
le poste, la frontiere ne laissant pas passer le 3000. Le site a une
console deployee et sans chemin d acces. Ce n est pas corrige ici, mais
c est dit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 19:14:23 -04:00
|
|
|
# LES JOURNAUX, EUX, PARTENT D'ICI — et par une ADRESSE, meme raison que
|
|
|
|
|
# ci-dessus : pas de plancher `/etc/hosts` sur un hyperviseur.
|
|
|
|
|
**({"client_journal_loki_url":
|
|
|
|
|
f"http://{_ip_loki}:3100/loki/api/v1/push"} if _ip_loki else {}),
|
le site surveille enfin sa fabric
La supervision du site voyait ses sept VM et rien d autre. Au depart, depuis
site-mon-01, les trois hyperviseurs, les neuf pattes de la frontiere et sa
PROPRE passerelle par defaut rendaient tous 100 pourcent de perte au ping.
16 hotes UP sur 16 dont 9 pattes de frontiere en controle ACTIF
11 cibles Prometheus dont 3 hyperviseurs, job fabric separe
LE PARTAGE. Les hyperviseurs portent node_exporter - D-48 l autorise - et
exposent 1005 unites systemd avec leur etat, soit ce que la sonde sante
mesurait, plus la charge et le disque. Ils n entrent PAS dans le socle :
leur appliquer serveur_durci reecrirait le pare-feu, le SSH et les sysctl
de la machine qui tient tout le reste. La frontiere, elle, n accueille
aucun agent : controle actif, une entree par PATTE, parce qu une interface
eteinte coupe une zone pendant que les autres vont bien.
ON TIRE, ON NE POUSSE PAS. J avais propose du passif et il avait ete
valide ; la mesure a dit non. Un hyperviseur envoie vers un routeur qui ne
connait pas les reseaux du site, et sa route par defaut est GELEE (D-57).
Le porteur de sante y expirait en 20 s.
LE VRAI DEFAUT ETAIT UNE LISTE QUI N A PAS SUIVI. Le mecanisme de routage
existait deja sur vmbr0, avec un commentaire du 2026-08-26 tenant
exactement le raisonnement qu on venait de refaire. Sa liste s arretait a
10.0.34.0/24 quand le site en declare six : les zones sauvegarde et
supervision sont nees, les routes n ont pas suivi. Le symptome ne
ressemblait pas a une route manquante - il ressemblait a un pare-feu, puis
a un probleme de reseau chez l exploitant.
make routes-fabric-etat compare desormais TROIS choses : zones declarees,
routes declarees dans /etc/network/interfaces, routes vivantes dans le
noyau. Le cas le plus traitre est vivante mais non declaree : tout
fonctionne, la supervision est verte, et la panne attend la prochaine
maintenance. Eprouvee dans les deux sens.
Deux defauts trouves en construisant. bifrost-2 est un nom RESERVE, pas un
boitier - l underlay le disait en prose, illisible par le moteur ; il porte
desormais etat: reserve. Et un service passif n existe que pour un hote qui
peut POUSSER : l appartenance a un groupe sert deux choses qui ne
coincident pas toujours, a qui l on deploie et de qui l on attend un
rapport.
Les commutateurs restent dehors, choix de l exploitant, coherent avec D-48.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 18:50:43 -04:00
|
|
|
**communes,
|
|
|
|
|
}
|
|
|
|
|
groupes.setdefault("hyperviseurs", []).append(_nom)
|
|
|
|
|
# ON TIRE, ON NE POUSSE PAS — ET C'EST LA ROUTE GELEE QUI LE DECIDE (2026-09-10).
|
|
|
|
|
#
|
|
|
|
|
# Toute la supervision de Set-OPS est PASSIVE : chaque noeud mesure et pousse. Ici
|
|
|
|
|
# c'est impossible, et la mesure le dit sans ambiguite :
|
|
|
|
|
#
|
|
|
|
|
# asgard -> 10.0.36.11 via 192.168.11.254 (le routeur du site)
|
|
|
|
|
# route par defaut via 192.168.11.254 GELEE (D-57)
|
|
|
|
|
#
|
|
|
|
|
# Un hyperviseur envoie vers un routeur qui ne connait pas les reseaux du site.
|
|
|
|
|
# Le porteur de sante expirait donc en 20 s. Le remede evident — router 10.0.0.0/8
|
|
|
|
|
# par la frontiere — touche la route par defaut d'un hyperviseur EN SERVICE, ce que
|
|
|
|
|
# D-57 interdit : « risquer de perdre l'hyperviseur ET le chemin pour le reparer ».
|
|
|
|
|
#
|
|
|
|
|
# LE SENS INVERSE, LUI, EST OUVRABLE : la frontiere route deja `serveur_ops_site`
|
|
|
|
|
# vers `SETOPS_FABRIC`. Prometheus TIRE donc les metriques, et rien ne pousse.
|
|
|
|
|
#
|
|
|
|
|
# CE QU'ON NE PERD PAS EN ECHANGE. `node_exporter` expose 1005 unites systemd avec
|
|
|
|
|
# leur etat — exactement ce que la sonde `sante` mesurait, plus la charge, le
|
|
|
|
|
# disque et l'horloge. On perd le mecanisme du `ttl` (le SILENCE qui alerte) ;
|
|
|
|
|
# Prometheus le remplace par sa propre cible `down`, que la sonde `collecte`
|
|
|
|
|
# rapporte deja.
|
|
|
|
|
groupes.setdefault("client_metrique", []).append(_nom)
|
une source vide n est pas « tout le monde »
Les journaux de la fabric, et le defaut de classe que leur mise en place a
revele. 10 hotes dans Loki (7 VM du site + 3 hyperviseurs), site a 67
services tous OK contre 51 avant.
METRIQUES TIREES, JOURNAUX POUSSES. Un scrape part du collecteur et doit
REVENIR : il exige un chemin symetrique, que la route par defaut gelee
interdit. Un push part de la source et n attend qu un accuse.
Trois trous dans les generateurs. Le devis de la frontiere ne connaissait
fabric qu en DESTINATION - un flux entrant tombait dans « rien d autre n
entre » sans rien dire. La regle etait posee sur la patte de la destination
alors que D-61 raisonne en ARRIVEE, et opt7 manquait a la table des zones.
Et le plus grave : fabric etait un mot RECONNU mais NON RESOLU. Source
vide, donc regle sans saddr - tcp dport 3100 accept, ouvert a tous. Un mot
reconnu mais non resolu est pire qu un mot inconnu : celui-ci serait refuse
a la validation, celui-la produit une porte grande ouverte qui a l air d un
flux precis.
LA CLASSE ENTIERE EST REFERMEE. Le defaut n etait pas propre a fabric :
toute paire nommant un ensemble et ne resolvant rien ouvrait le port.
Trouve en lisant les fichiers generes, l API de supervision d un tenant
etait ouverte a tous parce que pair: serveur_backup ne resout rien - cet
ecosysteme depose chez le site. expositions et externe, eux, veulent bien
dire tout le monde : c est une intention. Ailleurs, pas de source, pas de
regle, et le generateur le DIT.
Trois regles se referment, chacune verifiee AVANT d appliquer. Deux etaient
sans effet ; la troisieme portait Grafana, qui ecoute bien sur 3000. Sa
declaration disait pair: edge - juste chez un tenant, faux au site qui n a
pas d edge. Ajout de admin : ce qui etait accidentel devient explicite.
Et la mesure a montre autre chose : Grafana etait DEJA injoignable depuis
le poste, la frontiere ne laissant pas passer le 3000. Le site a une
console deployee et sans chemin d acces. Ce n est pas corrige ici, mais
c est dit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 19:14:23 -04:00
|
|
|
# LES JOURNAUX SE POUSSENT, EUX. La route specifique vers les zones du site — celle
|
|
|
|
|
# qui manquait pour 35 et 36 — suffit a un push : il part de la source et n'attend
|
|
|
|
|
# qu'un accuse. Un scrape, lui, exige que la reponse revienne par le meme chemin,
|
|
|
|
|
# ce que la route par defaut gelee empeche. D'ou metriques TIREES, journaux POUSSES.
|
|
|
|
|
groupes.setdefault("client_journal", []).append(_nom)
|
le site surveille enfin sa fabric
La supervision du site voyait ses sept VM et rien d autre. Au depart, depuis
site-mon-01, les trois hyperviseurs, les neuf pattes de la frontiere et sa
PROPRE passerelle par defaut rendaient tous 100 pourcent de perte au ping.
16 hotes UP sur 16 dont 9 pattes de frontiere en controle ACTIF
11 cibles Prometheus dont 3 hyperviseurs, job fabric separe
LE PARTAGE. Les hyperviseurs portent node_exporter - D-48 l autorise - et
exposent 1005 unites systemd avec leur etat, soit ce que la sonde sante
mesurait, plus la charge et le disque. Ils n entrent PAS dans le socle :
leur appliquer serveur_durci reecrirait le pare-feu, le SSH et les sysctl
de la machine qui tient tout le reste. La frontiere, elle, n accueille
aucun agent : controle actif, une entree par PATTE, parce qu une interface
eteinte coupe une zone pendant que les autres vont bien.
ON TIRE, ON NE POUSSE PAS. J avais propose du passif et il avait ete
valide ; la mesure a dit non. Un hyperviseur envoie vers un routeur qui ne
connait pas les reseaux du site, et sa route par defaut est GELEE (D-57).
Le porteur de sante y expirait en 20 s.
LE VRAI DEFAUT ETAIT UNE LISTE QUI N A PAS SUIVI. Le mecanisme de routage
existait deja sur vmbr0, avec un commentaire du 2026-08-26 tenant
exactement le raisonnement qu on venait de refaire. Sa liste s arretait a
10.0.34.0/24 quand le site en declare six : les zones sauvegarde et
supervision sont nees, les routes n ont pas suivi. Le symptome ne
ressemblait pas a une route manquante - il ressemblait a un pare-feu, puis
a un probleme de reseau chez l exploitant.
make routes-fabric-etat compare desormais TROIS choses : zones declarees,
routes declarees dans /etc/network/interfaces, routes vivantes dans le
noyau. Le cas le plus traitre est vivante mais non declaree : tout
fonctionne, la supervision est verte, et la panne attend la prochaine
maintenance. Eprouvee dans les deux sens.
Deux defauts trouves en construisant. bifrost-2 est un nom RESERVE, pas un
boitier - l underlay le disait en prose, illisible par le moteur ; il porte
desormais etat: reserve. Et un service passif n existe que pour un hote qui
peut POUSSER : l appartenance a un groupe sert deux choses qui ne
coincident pas toujours, a qui l on deploie et de qui l on attend un
rapport.
Les commutateurs restent dehors, choix de l exploitant, coherent avec D-48.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 18:50:43 -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
|
|
|
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())
|