Demande de l'exploitant : « il faut vraiment faire une distinction entre l'underlay et les
tenants. Definis bien la frontiere entre les deux mondes. L'underlay et son tenant doivent
avoir chacun sa voute. »
LA DOCTRINE (docs/frontiere-physique-virtuel.md) : qui possede quoi, ou ca vit, qui
l'administre. Et la regle qui la rend operante — UN TENANT NE DETIENT JAMAIS UN SECRET DU
MONDE PHYSIQUE. Aujourd'hui chaque tenant porte le jeton d'API du cluster ; patient 0 a du
le recopier pour exister. C'est la faute des neuf copies, appliquee aux secrets : une
valeur qui vit a N endroits diverge, et on ne peut plus en revoquer une sans les autres.
L'INSTRUMENT (`make underlay-plan`) confronte le fichier au reel : l'API du cluster pour
les adresses REELLEMENT portees, une sonde TCP pour ce qui repond. N'ecrit rien.
CE QU'IL A TROUVE, des le premier passage :
management 10.0.0.0/24, stockage 10.0.1.0/24, ceph 10.0.2-3.0/24 -> PERSONNE
transit 10.0.4.0/24, vxlan 10.0.5.0/24 -> occupes (3 noeuds)
portes par les noeuds et declares NULLE PART : 192.168.11.x (la vraie gestion),
10.11.5-7.x, 192.168.50.x, 10.1.110.254
La frontiere avait deja migre vers 10.17.0.1 ; les hyperviseurs, non. Le fichier decrivait
le monde d'avant — et c'est pour cela que la regle d'admin de patient 0 atterrissait sur
`wan`, ou elle n'aurait jamais laisse passer personne.
DEUX PRECAUTIONS ECRITES DANS L'INSTRUMENT, apprises en l'ecrivant :
- l'AUTORITE DEPEND DU ROLE. L'API de Proxmox connait ses hyperviseurs, et eux seuls.
Declarer un commutateur « porte par personne » parce que le cluster l'ignore, c'est
accuser le monde de ce que l'instrument ne voit pas.
- « pas joignable d'ici » n'est pas « absent ». Les reseaux de CHEMIN (transit, VXLAN,
stockage) ne sont jamais joignables de l'exterieur, par construction (D-78). Le premier
jet les declarait morts.
Le devis a donc corrige DEUX FOIS sa propre facon de mesurer avant de rendre un verdict.
RESTE, dans l'ordre : ecrire dans underlay.yml ce qui EST ; separer les voutes ; et alors
seulement appliquer la frontiere.
make verifier 41/41.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'exploitant : « j'ai une intuition : blockchain ». L'intuition visait le bon probleme —
une memoire partagee, verifiable, sans centre — mais la reponse etait deja dans git.
GIT EST DEJA UNE CHAINE DE HACHAGE : chaque commit porte l'empreinte de son parent, un
arbre de Merkle. Ce qui manquait n'etait pas la chaine mais l'AUTEUR : `user.name` est
declaratif, et toute la soiree du 20 des commits ont porte « Daniel Allaire » sans qu'aucune
preuve ne les lie a une cle (verifie : 8 commits, 0 signature, 0 etiquette).
POSE AUJOURD'HUI :
- signature par cle SSH (celle que la forge connait deja), etiquettes signees par defaut ;
premiere etiquette v2026.08.21, verifiee par `git verify-tag` ;
- `.git-allowed-signers` VERSIONNE : qui clone verifie sans rien demander a la forge, et
sans lui faire confiance. Retirer une ligne revoque pour la suite ; le passe signe reste
verifiable ;
- `scripts/genome.py` + trois cibles make : les QUATRE depots sans lesquels un ecosysteme
ne renait pas (moteur, instance, hebergeur, modeles), DERIVES et non declares ;
- `parente.yml` par ecosysteme : de quel moteur il descend, a quel commit, sous quelle
etiquette. Patient 0 descend de 742bcbf, etiquette v2026.08.21 ;
- P40 : la parente est inscrite, chaque depot se retrouve, chaque commit inscrit EXISTE
encore (une histoire reecrite se voit la), chacun porte un remote. Sautee proprement
quand l'instance n'est pas un depot git — le modele jetable de la CI.
POURQUOI PAS DE BLOCKCHAIN. Elle resout : qui ecrit ensuite, quand personne ne fait
confiance a personne et qu'il y a de l'argent en jeu. Aucun des trois ici. Et la
multiplicite qu'elle achete cher, la lignee la produit comme effet secondaire : chaque
enfant porte une copie du code dont il descend, donc reecrire l'histoire suppose de
convaincre TOUS les descendants. Le jour ou l'Alliance certifiera, ce sera un JOURNAL DE
TRANSPARENCE (Certificate Transparency, Sigstore), pas une chaine.
ENSEIGNE, pas seulement pose : nouvelle unite « Filiation, signatures et temoins » (moule
en quatre temps), onze termes au glossaire, et P39 les exige desormais.
make verifier 40 OK, 0 echec, 0 saute ; make ci idem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Demande de l'exploitant apres une soiree passee a croiser « strophe FRR », VRF, VNet et
nexthop-vrf : cet ecosysteme doit rester pilotable par un humain, idealement un seul ;
que chaque notion sous-jacente soit ENSEIGNEE.
MESURE AVANT D'ECRIRE : 40 termes employes par le depot et absents du glossaire — LDAP
184 fois, playbook 165, underlay 106, EVPN 66, VRF 33, LMTP 25. Le glossaire expliquait le
vocabulaire propre a Set-OPS (plan, index, voute, zone) et laissait dehors tout ce qui
vient du metier. Or c'est le metier qui perd le lecteur.
CE N'EST PAS UN DEFAUT DE REDACTION. La regle fondatrice du depot est qu'un humain pilote
sans IA. Chaque mot obscur retire une personne a la liste de celles qui peuvent reprendre
le systeme : un vocabulaire non explique est un defaut de CONCEPTION.
- Glossaire reecrit : 67 termes groupes par famille (plan, machines, Ansible, reseau,
noms, confiance, identite, courriel, etat et preuve). Chaque entree dit ce que c'est ET
pourquoi ce depot s'en sert, avec renvoi vers l'unite qui developpe.
- Unite d'apprentissage manquante : « Le reseau des tenants ». Dix-sept des quarante
termes y vivaient sans domicile. Elle suit l'ordre ou les problemes se sont poses : deux
clients sur un cable -> VLAN -> ses deux limites -> encapsulation -> pourquoi 1450 ->
EVPN -> le VRF, qui n'est pas une interdiction mais une ignorance structurelle.
- Navigation : la nouvelle unite est au sidebar ; le plan de recette regenere (P22 l'a
exige des l'ajout de la page — le harnais a mordu).
P39 verifie : chaque terme du jargon a une entree ; chaque lien du glossaire mene a une
page existante ; chaque page du wiki est atteignable depuis la navigation.
LA LISTE EST DECLAREE, ET C'EST UN CHOIX MESURE. La derivation automatique a ete essayee :
153 acronymes dans le wiki et le README, dont la moitie sont des mots francais en
capitales (AUCUNE, AVANT, TOUS). Un controle qui exige une entree pour « AUCUNE » finit
desactive, et une preuve desactivee ne garde rien. La preuve dit elle-meme cet angle mort.
EPROUVEE EN NEGATIF contre le glossaire d'avant : 49 termes manquants, nommes un par un.
make verifier 39 OK, 0 echec, 0 saute ; make ci idem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trente-huit preuves, des tests, un lint — et RIEN ne les executait sans qu'un humain tape
`make`. Le meilleur atout du depot dependait de ne pas oublier. Il a desormais une CI
(.forgejo/workflows/verifier.yml) et une cible qui la rejoue a l'identique : `make ci`.
CE QUE LA CI A TROUVE AVANT D'EXISTER. Ecrire le workflow supposait de repondre a une
question jamais posee : est-ce qu'un depot PUBLIC, seul, se tient ? Mesure sur un clone
nu : non, a cinq endroits.
- `make instancier` echouait sur le modele public — le tout premier geste du QUICKSTART.
Le Makefile forcait `principal/hosts.yml` alors que le modele vit en `production/` ; sa
precedence suit desormais celle du code (fichier, puis REPERTOIRE existant, puis defaut).
- P32 parcourait les 54 roles sans regarder ce que l'instance deploie. Elle passait sur
l'ecosysteme de reference PARCE QU'IL PORTE TOUT. Or les modeles sont des OFFRES : toute
offre plus petite que l'ecosysteme complet echouait son propre harnais, pour des services
qu'elle ne vend pas. Le perimetre se lit maintenant du plan (groupes de l'inventaire,
puis roles composes par leur playbook).
- P24 : le modele public ne declarait aucun reseau d'administration — une flotte qu'on
construit et ou l'on n'entre plus. `nftables_admin_ssh` est pose, avec le pourquoi.
- P33 : verifier_ports.py codait `instance/inventories/principal/hosts.yml` en dur.
- P32 et P24 lisaient le symlink `instance/` au lieu de SETOPS_INSTANCE.
Toutes de la MEME FAMILLE que P03 avant-hier : une resolution d'inventaire recopiee, une
variable d'environnement qui deborde de sa portee. Le depot en compte SEPT ; deux de plus
sont corrigees ici, et la septieme le dit en commentaire plutot que de le taire.
`make ci` NE TOUCHE AUCUN SYMLINK : le modele public est monte comme instance jetable,
vise par SETOPS_INSTANCE/SETOPS_UNDERLAY, detruit en sortant. Deux details mesures parce
que devines faux d'abord : l'instance jetable est un DOSSIER FRERE (la federation se
decouvre ainsi ; ailleurs, quatre preuves tombent) ; et SETOPS_UNDERLAY n'est pose QUE
pour la verification, sinon l'inventaire est ecrit avec une fabric et regenere avec une
autre — la commande fabriquait l'ecart qu'elle denonce.
RESULTAT : clone nu sans instance ni frere -> 38 OK, 0 echec, 0 saute. Depot de
l'exploitant avec ses 3 instances -> 38 OK, 0 echec, 0 saute. Aucun residu.
Et le lint du depot a refuse mon propre fichier de CI avant qu'il ne tourne une seule fois
(`on:` lu par YAML comme le booleen vrai). Le harnais mordait deja.
A AJUSTER AU PREMIER PASSAGE, ecrit en tete du workflow : l'etiquette `runs-on` doit
correspondre a un runner Forgejo enregistre, et le runner a besoin du reseau pour pip et
ansible-galaxy. Le vert de cette CI dira que le moteur et son modele public se tiennent —
pas que la flotte va bien : aucune VM jointe, aucune voute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`catalogue-services.md` est le document qu'on lit pour savoir ce que Set-OPS FAIT :
l'hebergeur d'un second site, un futur client, un mainteneur qui arrive. Verifie role par
role contre roles/, voici ce qu'il disait de faux.
- « Capacites futures encore a implementer : collaboration (Nextcloud/Collabora) et couche
web (frontal/dorsal) » — les quatre roles existent, collab-01, web-frontal-01 et
web-dorsal-01 sont ACTIFS, et les deux roles web sont codifies depuis les spikes du
2026-07-05.
- « La federation LDAP n'est pas automatisee dans le role ; Keycloak n'est pas expose » —
serveur_keycloak/tasks/federation-ldap.yml existe, et le plan declare
`expose: auth.<domaine>`.
- `infra-mail-01` : « Sendmail MTA » — c'est Dovecot ; Sendmail est retire depuis le
2026-07-04. La table des hotes datait d'avant la separation edge-mta / mailstore.
- `client_supervision` annonce comme integration — n'a JAMAIS eu ni role ni playbook. La
supervision ne pose rien sur les hotes : controles actifs depuis le coeur, resultats
passifs pousses par l'API (c'est backup-01 qui rapporte l'etat de ses depots).
- Une colonne « Role » decorative inventait des noms (`nextcloud`, `client_metriques`) : le
role porte le nom du GROUPE. Colonne retiree.
Et NEUF roles vivants ne figuraient dans aucune table — le socle, toute la pile courriel,
les sauvegardes, Icinga Web 2, oauth2-proxy, Unbound. Deux (`serveur_backup`,
`client_backup`) n'etaient nommes NULLE PART.
P38 — CE QUE P31 NE POUVAIT PAS VOIR. P31 verifie que tout est nomme et atteignable, pas
qu'un document dise vrai : une carte peut etre complete et perimee. P38 confronte le
catalogue au code dans les deux sens, et c'est la TABLE qui fait foi des deux cotes : tout
role figure dans une ligne de table (la prose ne suffit pas — la pile courriel y etait
racontee et introuvable pour qui lit un index), et tout groupe cite en table existe
reellement (role, ou playbook de groupe pour `serveur_durci`, qui en compose onze).
Deux exemptions nommees : la prose peut citer les roles RETIRES, sinon on ne peut plus
ecrire d'ou l'on vient ; et P38 ne juge pas si une description est JUSTE — cela se revoit
contre le CHANGELOG, le mecaniser serait se mentir.
EPROUVEE EN NEGATIF : rejouee contre la version d'avant, elle echoue en nommant les neuf
roles absents et les trois cases fantomes.
Le catalogue dit aussi desormais ou il s'arrete : la reconstruction prouve qu'une machine
nue atteint l'etat voulu, pas la tenue sous charge ; et l'usage reel de Nextcloud n'est pas
consigne comme preuve. Lacune nommee au passage : la dependance causale
serveur_web_frontal -> serveur_nginx n'est toujours pas declaree.
make prouver : 38 OK, 0 echec, 0 saute. make test inchange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le commit du 2026-08-14 nommait lui-meme ce qui restait : « meme hypothese ailleurs, non
corrigee — devis_sdn et devis_reseau partent du meme decouvrir(). A traiter quand ils
serviront sur un second site. » C'est fait AVANT, pas pendant la visite.
Les trois devis equipent le MATERIEL d'un site : la frontiere (regles, routes), le
commutateur (VLAN, SVI, routes) et le SDN de l'hyperviseur (zones, VNets). Un tenant
d'ailleurs y ajoutait des objets que le materiel accepte, qui ne correspondent jamais a
rien, et que rien ne signale.
UNE SEULE FONCTION AU LIEU D'UN FILTRE RECOPIE TROIS FOIS :
`devis_reseau.decouvrir_du_site()` = decouvrir() restreint par `underlay.tenants`, la
doctrine ecrite une fois. Le filtre inline de devis_opnsense est retire au profit d'elle.
`admin_tous_tenants()` la suit : le routeur d'un site n'a pas a savoir revenir vers le
plan de gestion d'un tenant qu'il ne porte pas.
EPROUVE dans les trois situations : underlay sans la cle -> les deux tenants, comme avant ;
underlay du second site -> OPS-Technolibre seul ; nom declare qu'aucun dossier ne fournit
-> ATTENTION et le reste est retenu ; filtre qui ne retient rien -> refus, code 1.
SANS EFFET SUR LE SITE ACTUEL : l'underlay de Chezlepro ne declare pas `tenants`, et cle
absente = toute la federation (verifie : decouvrir() et decouvrir_du_site() rendent la
meme liste ici).
ET LA CLE EST ENFIN DOCUMENTEE — c'etait le vrai trou. `underlay.tenants` existait depuis
le 14 sans figurer ni dans underlay.yml.example ni dans l'annexe du runbook
d'implantation : indecouvrable pour qui monte un second site.
prouver 37 OK, 0 echec, 0 saute ; make test inchange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trouve en validant une mise a jour du CHANGELOG. Deux invocations de la meme preuve,
deux verdicts : `make prouver` -> NON CONFORME (« lab : 17 hotes avec ecart »),
`python3 scripts/prouver.py` -> CONFORME 37/37. Le lab n'avait aucun ecart.
DEUX VARIABLES DESIGNENT LA CIBLE, ET LA SECONDE GAGNE. Le Makefile exporte
SETOPS_INVENTAIRE (ligne 13), derive de l'instance ACTIVE ; instancier.py:68 lui fait
FORCER la cible par-dessus SETOPS_INSTANCE. P03 (prouver.py:505) ne redirigeait que
SETOPS_INSTANCE : elle generait le plan de CHAQUE instance federee et le comparait a
l'inventaire applique de la SEULE instance active.
LE ROUGE N'ETAIT PAS LE PROBLEME, LE VERT L'ETAIT. Sous `make`, l'inventaire applique
de lab et de Technolibre n'etait JAMAIS lu — l'angle meme pour lequel P03 a ete ecrite
le 2026-08-12 (un tenant qu'on ne regarde pas imposant ses vieilles adresses au pare-feu
partage). La preuve etait aveugle a son propre cas, par l'invocation documentee. Les
rapports du 13 et du 14 sortent de cette invocation-la. Signature visible sans lire le
code : les hosts.genere.yml de lab et de Technolibre ne bougeaient pas.
CORRECTIF, cinq sites : env.pop("SETOPS_INVENTAIRE") partout ou l'on redirige
SETOPS_INSTANCE — P03 et P15, plus les trois applicateurs (opnsense, proxmox_fw, sdn) qui
pointent vers l'HEBERGEUR. Ces trois sont sans effet tant qu'hebergeur et tenant actif
coincident, c'est-a-dire jusqu'au second site. Le geste existait deja (modeles.py:96).
GARDE, pour que la classe cesse d'etre silencieuse : inventory_rules.inventaire_force()
REFUSE une cible hors de l'instance visee, en nommant les deux valeurs. Eprouvee dans les
deux sens (contradiction -> code 1 ; cible legitime dans l'instance -> passe). Branchee
sur les quatre resolutions de _inventaire (instancier, serveurs, applications,
config_proxmox). Le GUI garde la sienne : il ne redirige jamais SETOPS_INSTANCE pour un
fils et resout par symlink a chaque requete.
make prouver : 37 OK, 0 echec, 0 saute — et les hosts.genere.yml des TROIS instances
portent l'horodatage du passage. make test 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mesure du 2026-08-14, sur le second site. `make frontiere-plan` voulait poser sur la
frontiere de Technolibre les regles ET les routes de CHEZLEPRO : 30 objets de plus,
dont six routes vers des sous-reseaux 10.17.x qui n'existent pas la-bas.
LE DEFAUT EST DE PORTEE, ET IL EST SILENCIEUX. Les devis partaient de
`devis_reseau.decouvrir()`, qui rend TOUTE la federation — tout dossier frere portant
une nomenclature avec un index. C'etait juste tant qu'il n'y avait qu'un site :
l'hebergeur unique portait bien tous les tenants. Des le second, le boitier aurait
accepte ces trente objets, aucun n'aurait jamais correspondu a un paquet, et rien ne
l'aurait signale — une politique qui a l'air complete et ne protege rien. Encore le
chèque vert sur un perimetre vide.
CORRECTIF : l'hebergeur declare dans SON underlay les tenants qu'il porte
(`underlay.tenants`), et `devis_opnsense` s'y limite. Cle ABSENTE = ancien
comportement (toute la federation) : un site unique n'a rien a declarer, c'est le
second qui doit se nommer. Un nom declare qu'aucun dossier frere ne fournit est
signale, pas ignore.
MEME HYPOTHESE AILLEURS, non corrigee ici : `devis_sdn` et `devis_reseau` partent du
meme `decouvrir()`. A traiter quand ils serviront sur un second site.
AU PASSAGE, un defaut du document ecrit la veille : le squelette d'`underlay.yml` de
`implanter-un-tenant-sur-un-site.md` omettait `index`. Sans cette cle, `make underlay`
refuse le reseau de gestion en le prenant pour le supernet d'un AUTRE site — message
deroutant, cause triviale. Trouve en s'en servant, moins de 24 h apres l'avoir ecrit.
prouver 37/37, make test 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mesure du 2026-08-13 sur un OPNsense 24.7 (version ANCIENNE, en cours de mise a
jour depuis) : toute ecriture du moteur y echouait. Alias, regles, NAT, routes —
`make frontiere-appliquer` n'aurait rien pose sur ce boitier.
Ce n'est donc PAS un defaut universel du moteur : il ecrit correctement sur la
frontiere de Chezlepro, plus recente. C'est un probleme de COMPATIBILITE, et le
correctif vaut surtout comme garantie de portabilite — le jour ou l'on arrive sur
un site dont on ne choisit pas le firmware, ce qui est exactement le cas ici.
LE SYMPTOME MERITE D'ETRE RETENU, lui, quelle que soit la version. Le controleur
d'OPNsense lit ses champs avec `hasPost(<racine>)`. Si le corps arrive en
`application/json` et que le boitier ne le decompose pas en variables de POST, ce
test est FAUX : reponse `{"result":"failed"}` NUE — HTTP 200, aucune redirection,
et surtout AUCUNE validation. Le controleur ne dit pas quel champ manque, parce que
de son point de vue il n'y avait aucun champ.
Trois fausses pistes avant la bonne : valeur invalide (un corps VIDE echouait
pareil), racine de payload erronee (elle etait juste), privileges de la cle (la
lecture passait). Ce qui a tranche : un corps vide aurait DU produire des
validations. Leur absence disait que le controleur n'avait rien recu.
CORRECTIF : `Frontiere` poste desormais `racine[champ]=valeur`. C'est la forme que
poste l'interface web elle-meme — aucune version d'OPNsense ne la refuse, alors que
le JSON depend du boitier. Tous les corps du moteur sont des dicts plats de chaines
(alias, rule, route) : un seul niveau d'imbrication suffit.
EPROUVE SUR LE BOITIER, dans les deux sens : addItem d'un alias sonde -> `saved`,
delItem -> `deleted`, aucune trace laissee, lecture intacte (11 alias). A re-eprouver
apres la mise a jour, pour verifier que le formulaire reste bon sur la version
recente — c'est le seul point qui reste ouvert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le depot couvrait deja « l'hebergeur prepare son materiel » et « un tenant VIVANT
change d'hebergeur, sans coupure ». Il manquait le troisieme cas, qui est celui
qu'on s'apprete a faire : un tenant dont le plan existe deja prend corps sur un
site qui n'a jamais rien porte. Rien a migrer, rien a interrompre — donc ni la
sequence de gel/bascule de migration-tenant.md, ni le bootstrap de QUICKSTART.
SIX PHASES, chacune fermee par une commande qui INTERROGE le systeme :
0 bureau (index du site = index du tenant, depot hebergeur, gabarit, voute)
1 reconnaissance du cluster -> `make underlay`, `make placement-plan`
2 frontiere -> `make frontiere-plan`
3 gabarit (convertir en template ; un clone de VM vivante en ferait 14 copies)
4 composer les DEUX symlinks (D-80) + les 4 valeurs de placement
5 materialiser -> `make sdn-appliquer` puis `make reconstruire`
6 recette : devis, restauration eprouvee, supervision
CE QUE LE RUNBOOK PORTE ET QU'AUCUN AUTRE DOCUMENT NE DIT
- Un site NEUF se batit d'emblee dans l'adressage cible (D-77/D-78). Le site
historique est encore en 10.0.x et migrera par runbooks §6 ; sur un site vierge
la cible ne coute rien. Deux sites en 10.0.0.0/24 rendraient la reprise mutuelle
IMPOSSIBLE : deux plans de gestion identiques ne peuvent pas s'atteindre.
- L'ordre de `reconstruire` est explique ligne a ligne, dont le piege `make flux` :
sans lui nftables tombe en `policy drop` SANS AUCUNE REGLE, la flotte monte, SSH
repond, et tout le reste est mur (trouve le 2026-08-10).
- Un site a UN SEUL noeud : il est aussi noeud de sortie, aucun pont ne peut etre
partiel, aucune haute disponibilite — a dire, pas a laisser supposer.
- PVE 9 n'a jamais ete eprouve ici : tout ecart est inconnu jusqu'a mesure.
- « Ce qui n'est PAS fait en repartant » : resserrer le compte d'API, le lien
inter-sites, le gabarit des deux cotes, la voute hors de son propre site.
ANNEXE : les squelettes d'`underlay.yml` et `proxmox-hebergeur.yml` pour un site a
un noeud, a remplir depuis la RECONNAISSANCE et jamais de memoire — c'est en
recopiant des listes chez chaque tenant qu'elles avaient diverge.
Sixieme porte dans la table de routage du README. Les 21 cibles make citees ont
ete verifiees comme existantes. prouver 37/37 (P34 : 40 documents declarent leur
lecteur), make test 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'exploitant a decouvert par hasard, en creusant l'intrant « pont reseau », qu'on
peut fabriquer une VM HORS DU PLAN. Verification : `cloner-vm` n'etait mentionne
qu'UNE fois dans tout le depot, comme note de plomberie. L'usage n'etait nulle
part.
Or le depot porte deja la regle « eprouver l'outil avant d'ecrire le role qui
l'enveloppe » — elle a evite les bugs de premier deploiement de rspamd et tranche
le pivot Stalwart -> Postfix/Dovecot. L'instrument de cette regle n'etait pas
nomme.
DOCUMENTER LA DISCIPLINE, PAS SEULEMENT LA CAPACITE. Une telle VM est NUE :
l'inventaire ne la contient pas, `make raser` ne la detruira JAMAIS (il derive du
plan), aucun DNS, certificat, sauvegarde, pare-feu ni nftables, et son VMID n'est
garde par aucune preuve. Elle ne disparait que si on la detruit soi-meme — un VMID
oublie squatte le cluster sans que rien ne le signale, exactement comme un pont
disparu a survecu dix jours dans une declaration ce matin.
Ecrit pour trois lecteurs : le GESTE dans vm-lifecycle.md §4bis, la CAPACITE dans
pouvoirs-set-ops.md (qui evalue le moteur), le REFLEXE dans la discipline de
carte-set-ops.md (qui modifie le moteur).
Decouvrir une capacite de son propre outil par accident est le signe qu'elle
manquait a la documentation, pas au code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Question de l'exploitant apres la decouverte de vmbr3 : a quoi sert cet intrant ?
Mesure : a presque rien.
proxmox_pont 14 occurrences dans l'inventaire -> DERIVE par hote (VNet de zone)
proxmox_noeud 0 -> proxmox_clone_noeud est la vraie valeur
proxmox_stockage 0 -> proxmox_clone_stockage est la vraie valeur
instancier pose le VNet de chaque zone dans proxmox_pont, et l'hote l'emporte sur
le defaut. proxmox_clone_pont n'est donc consulte que par un `make cloner-vm`
manuel, hors flotte. C'est exactement pourquoi vmbr3 a pu y etre faux dix jours.
C'est le pire genre d'intrant : visible dans le GUI, on le corrige, on redeploie,
rien ne change. L'intitule dit desormais sa portee.
D-80 CORRIGEE : j'y avais ecrit « trois cles : noeud, stockage, pont ». Faux pour
le pont. La liaison de placement reelle est noeud, stockage et gabarit ; le pont
se derive comme le reste.
Verifier avant d'enumerer : j'avais liste les cles en lisant le fichier du tenant,
sans regarder lesquelles sont reellement consultees. Deux le sont, une ne l'est
pas — et c'est celle qui etait fausse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
J'avais ecarte le gabarit de P37 (« objet du cluster, pas une liste declaree »).
L'exploitant a releve que ce n'etait pas une raison de ne pas le verifier : ca
deplace la question du STATIQUE vers le DEVIS.
devis_placement.py interroge le cluster et confronte les quatre valeurs : le noeud
existe ; le stockage existe ET accepte `images` ; le pont existe SUR LE NOEUD
retenu ; le gabarit existe ET porte template=1 — cloner une VM ordinaire
fonctionnerait, et produirait quatorze copies d'une machine vivante.
AU PREMIER PASSAGE il a trouve une declaration perimee : vmbr3 n'existe plus sur
aucun noeud (disparu au passage du transport VXLAN sur les interfaces VLAN), mais
proxmox-hebergeur.yml le declarait encore depuis le 3 aout — et P37 le validait,
puisqu'elle valide la DECLARATION, pas le cluster.
Invisible jusqu'ici : flotte-creer surcharge le pont avec le VNet derive de chaque
zone, donc le defaut n'aurait morde que sur un `make cloner-vm` manuel. Corrige
des deux cotes ; le defaut des trois tenants passe a vmbr1, que le cluster nomme
lui-meme « VM (5Gig) ».
P37 et ce devis ne se remplacent pas : l'un garde la coherence entre deux
declarations, l'autre confronte la declaration au reel. Il fallait les deux pour
voir un pont disparu depuis dix jours.
P31 a refuse le script tant qu'aucune cible make ne l'atteignait.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Formulation de l'exploitant, meilleure que celle du depot. Le commentaire disait
« la fabric reste celle de l'hebergeur, quel que soit le tenant actif » — vrai,
mais centre sur l'HEBERGEUR. Le cadrage juste est centre sur le TENANT : les deux
symlinks composent deux axes INDEPENDANTS (instance = quel tenant, underlay.yml =
sur quelle fabric).
Tout l'adressage derive du seed index : le plan se deplace d'une fabric a l'autre
sans y toucher. Ce qui ne se deplace pas tient en TROIS CLES —
proxmox_clone_noeud, _stockage, _pont. Elles vivent cote tenant (c'est lui qui
choisit ou se poser) mais nomment des objets de l'hebergeur. Trois, pas trente :
c'est ce qui separe « portable » de « theoriquement portable ».
P37 confronte ces trois noms aux listes de proxmox-hebergeur.yml, trouve par
derivation du symlink underlay.yml. Un nom absent est un ecart STATIQUE (D-75) —
sans quoi une faute de frappe ne se decouvre qu'au premier clone, apres quarante
minutes. Eprouvee en negatif sur les trois cles.
Non verifie et dit comme tel : le gabarit (_vmid_modele) est un objet du cluster,
pas une liste declaree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Chezlepro reconstruit depuis zero en 10.17.x.x. Sept defauts sont tombes, et
AUCUN n'etait detectable autrement — c'est tout l'argument de la reconstruction
comme preuve.
1. aucune route vers le tenant sur le poste (posee a la main, jamais persistee)
2. backup-01 exigeait l'AC d'Icinga — dependance non declaree
3. ma correction inversait les couches : REFUSEE PAR P08 avant d'entrer
4. base Forgejo a moitie initialisee (sequelle de l'arret du n°2)
5. /etc/hosts : nom court avant le FQDN -> hostname -f faux sur les 14 machines
6. API Icinga jamais activee (garde `creates:` d'api setup)
7. restic refuse tout le lot si un chemin declare manque
LE CINQUIEME DEPASSAIT ICINGA. hostname -f rend le PREMIER nom : toute la flotte
se croyait appelee « mon-01 ». Visible sur le certificat d'Icinga, mais le meme
piege attendait Postfix (myhostname), les journaux, les certificats. FQDN d'abord.
CE QU'ON NE CORRIGE PAS : icinga2 api setup REECRIT NodeName d'apres le nom court
et nomme ses certificats d'apres lui. Aligne avant, il est ecrase ; aligne apres,
les certificats portent le mauvais nom. On adopte sa convention : le depot appelle
l'API par le nom court, celui que le certificat porte.
serveur_icinga_node_name derive desormais de l'INVENTAIRE, pas d'ansible_fqdn —
un fait qui depend du resolveur et rendait « mon-01 » alors que le FQDN etait bon.
Le commentaire ecrit pour avertir qu'une sequence accolade-diese casse les
gabarits Jinja contenait cette sequence, et cassait le gabarit. Neuf hotes en
echec pour un avertissement mal redige.
Sept devis CONFORME, prouver 36/36, make test 0. Neuf sauvegardes reussies.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Question de l'exploitant : pourquoi les regles au pare-feu global alors que chaque
VNet peut en porter ? La reponse honnete a demande trois corrections.
CE QUE J'AVAIS TORT D'AFFIRMER :
- « une regle de VNet serait trop grossiere » : faux, elle porte source, dest,
dport, proto. Eprouve (regle creee, relue, retiree).
- « c'etait un arbitrage » : faux. Zero mention du pare-feu SDN dans le depot.
Pas un choix, une omission presentee comme un raisonnement.
- « sur Debian 12 iptables c'est iptables-nft » : faux ici. Mesure sur asgard :
iptables v1.8.9 (legacy). Proxmox force l'alternative sur legacy. LE DEFAUT
D'UNE DISTRIBUTION N'EST PAS UNE MESURE.
MESURE : proxmox-firewall 0.7.1 installe mais ABSENT des services ; seul
pve-firewall 5.1.3 tourne ; node firewall enable 0. Le pare-feu de VNet est
entierement expressif (source/dest/dport/proto, policy_forward ACCEPT|DROP) et
implemente par le SEUL moteur nftables : une regle posee aujourd'hui serait
acceptee, stockee, visible, et n'appliquerait rien. Quatrieme occurrence du motif
de la journee.
TROIS GAINS DANS LE MEME GESTE : moteur xtables -> nftables natif (la doctrine que
les invites respectent deja), pare-feu de VNet reel, et regles qui SURVIVENT a la
reconstruction la ou 38 groupes par VM doivent etre re-attaches a chaque cycle.
Differe apres la reconstruction, sur un noeud d'abord, avec controle negatif.
Reserves : jeu de regles charge non inspecte (nft exige root), aucun blocage reel
eprouve (aucune VM en service).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le retrait du decalage de +10 a deplace les tenants vers 10.<index>. Ces plages
n'etaient pas vierges : les hyperviseurs portent des interfaces VLAN heritees que
le plan ne connait pas — vlan5/6/7 = 10.11.5-7.41 (Technolibre) et vlan1110 =
10.1.110.254 (lab).
Changer l'index plutot que deloger le materiel. 10.23 et 10.13 sont libres
partout, VLAN derives 1231-1236 et 1131-1136 sans croisement.
LES DEVIS NE SUPPRIMENT JAMAIS CE QU'ILS NE POSSEDENT PAS — garde juste, mais elle
laisse des orphelins quand un tenant change de nom derive. Retires a la main :
zone SDN t11 (6 VNets, 6 sous-reseaux) et 18 groupes de securite t11-* (31
regles). Ordre impose : contenu d'abord, contenant ensuite.
Verifie sur le REEL et non sur les devis : SDN t17/t23 seulement, pare-feu t17/t23
seulement, frontiere 10.0/10.17/10.23. Les trois devis disent « rien a faire ».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D-77 sortait deja le stockage de l'espace derive, mais par une EXCEPTION NOMMEE
plutot que par une regle. La question de l'exploitant — « le VLAN 40 n'a aucune
VM, pourquoi ne pas lui donner une adresse non routee ? » — a fait apparaitre le
critere juste.
Ce n'est pas « y a-t-il des machines dedans » (le VLAN de gestion n'en a pas plus
qu'un autre), c'est : ce reseau est-il jamais une DESTINATION, ou seulement un
CHEMIN ?
gestion (10) atteinte depuis ailleurs -> 10.<index>.0.0/24, UNIQUE
transit (40) prochains sauts seulement -> 192.168.40.0/24
transport VXLAN(50) VTEP <-> VTEP -> 192.168.50.0/24
stockage (20/30/31) baie <-> hyperviseurs -> 192.168.20/30/31.0/24
Sur six reseaux, CINQ cessent d'exiger la moindre coordination entre deux
hebergeurs, et « unique » redevient signifiant.
L'OS, trouve par l'exploitant avant ecriture : le VLAN 11 aurait produit
192.168.11.0/24, DEJA occupe par la gestion des hyperviseurs sur vmbr0
(passerelle .254). Le 11 se liberera quand cette gestion rejoindra 10.<index>.0.x
— mais faire dependre un plan d'adressage de l'ORDRE d'une migration est le genre
de dette qui se paie un an plus tard. Le transport passe au VLAN 50 : libre,
192.168.50.0/24 libre, et ne depend de rien. Verifie : rien ne code le 11 en dur.
underlay.yml n'est PAS modifie : il decrit le materiel tel qu'il est, et y ecrire
les nouvelles plages avant le deplacement physique le ferait mentir. Les adresses
changeront avec le materiel, dans l'ordre du runbook §6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
P03 ne verifiait la fraicheur de l'inventaire que pour l'instance ACTIVE. Or la
frontiere nord/sud est PARTAGEE : ses alias d'hotes sont construits depuis le
hosts.yml de CHAQUE tenant. Un tenant qu'on ne regarde pas — parce qu'il n'a
aucune VM, precisement — impose donc ses adresses au pare-feu de tout le monde.
Elle boucle desormais sur les instances decouvertes, chacune verifiee via
SETOPS_INSTANCE (que instancier.py honore deja).
AU PREMIER PASSAGE, elle a trouve une troisieme instance perimee et une collision
franche :
OPS-Chezlepro-lab (index 1) applique : 10.11.18.21 <- ancienne derivation
OPS-Technolibre (index 11) derive : 10.11.x.x <- nouvelle derivation
Le lab occupait EXACTEMENT la plage desormais attribuee a Technolibre. P21 garde
les index ; rien ne gardait les inventaires APPLIQUES. La collision serait apparue
le jour ou les deux auraient tourne ensemble.
Les trois instances sont alignees : 10.1 (lab), 10.11 (Technolibre), 10.17
(Chezlepro).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
10.17.0.1/24 posee par l'API a cote de 10.0.0.1/24, qui reste active. Ajouter
AVANT de retirer : un point de routage qui change d'adresse d'un coup coupe
simultanement l'exploitant, les commutateurs qui l'ont en passerelle par defaut,
et l'outil qui devait faire la bascule.
Documente l'ordre du reste (poste, commutateurs, hyperviseurs, transit, VXLAN,
stockage, puis retrait) et surtout ce qui n'en depend PAS : le renumerotage du
TENANT est independant — la frontiere route son supernet vers le meme prochain
saut quelle que soit sa propre adresse de gestion.
prouver reste a 1 : P03 signale l'ecart plan/applique, qui est reel jusqu'au
renumerotage du tenant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
supernet_de rendait 10.(10+index).0.0/16. Personne ne savait plus pourquoi : ni le
commentaire de la constante, ni le wiki, ni le commit fondateur 36a882b ne le
justifiaient. Trois endroits consultes, zero raison ecrite.
Ses deux effets constates :
- il reservait 10.0-10.9 sous la plage tenant. Utile tant que l'underlay vivait
la — mais D-77 l'a fait entrer dans la bande basse de son propre /16, ce qui a
vide cette reserve de son role la veille ;
- il eloignait le premier tenant de 10.0.0.0/16, la plage la plus repandue en
reseau domestique. Ce risque revient donc aux index bas, et c'est ASSUME.
En echange l'index se lit directement dans l'adresse (17 -> 10.17.x.x) et le
plafond passe de 245 a 255 ecosystemes federes.
Chezlepro (17) 10.27.0.0/16 -> 10.17.0.0/16
Technolibre (11) 10.21.0.0/16 -> 10.11.0.0/16
lab (1) 10.11.0.0/16 -> 10.1.0.0/16
Doc alignee : les trois pages du wiki, multi-instances.md (plafond et exemple
devenus faux arithmetiquement), sdn-evpn.md, le libelle de la GUI, la docstring
d'underlay.py, D-77, et le document de preparation d'un site hebergeur. Les
CONSTATS DE TERRAIN dates sont laisses tels quels : ce sont des mesures.
CE COMMIT NE RENUMEROTE RIEN. Il change ce que le plan DERIVE ; l'inventaire
applique porte toujours 10.27.x.x et les quatorze VM tournent dessus. Appliquer
sans reconstruire rendrait la flotte injoignable — le renumerotage est une
operation a part, a mener a froid.
Au passage, retire un debris : une copie de conflit Nextcloud de
serveur_powerdns/defaults/main.yml, IDENTIQUE a l'original, commitee par accident
dans 1295eea et jamais chargee par Ansible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D-77 disait ou l'underlay doit vivre ; rien ne le verifiait. Une convention qu'on
n'outille pas pourrit en silence (D-70) — c'est ce qui a laisse la sauvegarde vide
pendant un mois.
Le controle disait l'INVERSE de la decision : underlay.py refusait tout
chevauchement avec un supernet tenant. Rendu plus FIN, pas plus strict :
son propre supernet, bande basse -> conforme (la regle)
son propre supernet, bande haute -> REFUS (collision avec ses zones)
supernet d'un AUTRE site -> REFUS (jamais reliables)
hors de tout supernet -> conforme (heritage 10.0.x, stockage 192.168.x)
La frontiere derive d'OCTET_ZONE, jamais ecrite en dur. Le site declare son `index`
dans underlay.yml ; sans lui on retombe sur la regle stricte d'avant D-77, sure —
Chezlepro reste donc conforme en 10.0.x tant qu'il n'a pas migre.
LE PIEGE, attrape par le test : un prefixe peut COMMENCER dans la bande basse et
deborder — 10.21.0.0/19 couvre les octets 0 a 31. La borne est evaluee sur toute
l'etendue du prefixe, pas sur son premier octet.
Deux de mes propres cas d'epreuve etaient mal choisis (10.21.14.0/23 et
10.21.12.0/21 se normalisent dans la bande basse) : il a fallu un prefixe qui
franchit reellement la frontiere pour eprouver la garde.
test_underlay_bande_basse.py : 7 cas, cable dans make test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mon « numero de site » etait un SECOND SEED a tenir et a synchroniser, alors que
tout derive deja d'index (D-17). Rejete au profit de la proposition de l'exploitant,
meilleure : l'underlay occupe la BANDE BASSE du supernet du tenant,
10.(10+index).0-15.x.
La bande basse est sure PAR LA REGLE, pas par chance : les zones valent
10.(10+index).(15+categorie).0/24, donc 3e octet >= 16. Les octets 0 a 15 ne sont
JAMAIS alloues. Consequence heureuse : les 3e et derniers octets de l'underlay
actuel sont preserves — seul le 2e change, et l'invariant .1 (D-04) survit.
Verifie sur les quatre cas qui comptent : Technolibre sur l'underlay de Chezlepro,
Technolibre chez Mathieu (meme /16, bandes disjointes), Chezlepro restaure chez
Mathieu, et le lien entre les deux sites. Aucun recouvrement.
Le STOCKAGE en sort : jumbo, non route, il ne quitte jamais son site, donc aucun
besoin d'etre unique. Identique partout en 192.168.<vlan>.0/24 — l'adresse dit son
VLAN. Contrepartie assumee : ces reseaux ne traverseront jamais un lien
inter-sites ; sans consequence, puisque ce qui voyage entre deux sites est l'ETAT,
par le depot de sauvegarde, pas le stockage bloc.
Calendrier : applique tout de suite la ou c'est gratuit (site neuf), differe a
froid pour Chezlepro — 10.0.x.x et 10.21.x.x ne se chevauchent pas entre-temps.
Reste a outiller : borner le 3e octet de l'underlay sous OCTET_ZONE + 1 (P23).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le document prescrivait 10.0.x.x a tout hebergeur — c'est-a-dire exactement les
plages de Chezlepro. Deux sites qui portent les memes sous-reseaux NE PEUVENT PAS
etre relies : chaque routeur croit que le reseau est chez lui. Le defaut etait
invisible tant qu'il n'existait qu'un seul site.
Il devient bloquant des qu'on veut une reprise apres sinistre MUTUELLE : la
replication doit tourner machine a machine, en continu. D'ou `10.<site>.x.0/24`.
Les VLAN ne changent pas — ils sont locaux et ne traversent jamais le lien ;
une seule table mentale.
Nouveau §8 : relier deux sites. Il separe deux besoins qu'on confond facilement.
Les services publies n'ont besoin de RIEN (edge + TLS + SSO). Le plan de controle,
si : l'inventaire adresse les VM par leurs IP privees DERIVEES — aucun chemin
publie ne peut y mener sans casser la derivation — et les deux API d'admin
tournent avec la verification du certificat DESACTIVEE.
Acces nomade pour exploiter ponctuellement ; site-a-site pour repliquer, parce
qu'il doit tenir sans qu'aucun poste soit allume. Dans les deux cas la politique
est explicite : un lien qui joint deux reseaux defait en silence l'isolation
inter-tenant.
Ce que la reprise mutuelle exige en plus : capacite chez le survivant pour les
DEUX ecosystemes, gabarit present des deux cotes, voute conservee hors de son
propre site. Les sauvegardes sont chiffrees cote client : le site d'accueil
heberge du chiffre qu'il ne peut pas lire — la confiance porte sur la
DISPONIBILITE, pas la confidentialite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tout derive d'un plan et se prouve. Le gabarit est la SEULE piece faite a la
main — 853 lignes de procedure manuelle — et il est en amont des quatorze VM.
Le 2026-08-09 l'a demontre : l'ancien portait une cle privee d'hote SSH et un
resolv.conf fige, recopies dans chaque clone.
La preuve de reconstruction s'arrete donc un cran trop tot : on reconstruit la
flotte, pas ce dont elle est clonee.
Cible : image cloud Debian officielle, signature verifiee contre une empreinte
epinglee (verifier_signature.py existe deja), import, reglages, conversion.
Cout assume : la recette doit etre COMPAREE au gabarit courant avant bascule —
un reglage oublie serait herite par les quatorze VM et decouvert loin de sa
cause. En attendant, vzdump/qmrestore transporte un gabarit entre clusters.
Ce que ca retourne : un artefact qu'on ne sait pas refaire est un artefact qu'on
ne peut pas se permettre de perdre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le gabarit portait le nom du mauvais proprietaire. Les TROIS instances
(Chezlepro, Technolibre, lab) le declaraient et pointaient deja toutes sur le
MEME VMID 99998 — alors que le commentaire affirmait « chaque tenant a SON
golden template ». Faux depuis longtemps, et invisible en lisant un seul fichier.
Renomme sur le cluster et dans les trois instances. Le gabarit est un artefact du
MOTEUR, pas d'un tenant ; le nom d'un tenant sur le gabarit d'un autre etait un
piege qui n'attendait qu'un troisieme hebergeur. model_creer.py et
config_proxmox.py proposaient encore modele-debian13 : alignes.
Sans risque : le clonage se fait par VMID depuis le 2026-08-10, le nom ne sert
plus qu'a l'affichage.
NOUVELLE PORTE dans l'aiguillage : docs/preparer-un-site-hebergeur.md, pour
quelqu'un qui prete son materiel sans rien connaitre de Set-OPS. Ecrit a partir
du depot — VLAN et MTU d'underlay.yml, bloc par tenant de
inventory_rules.supernet_de(), privileges du jeton de config-proxmox.md,
dimensionnement (~460 Go, ~37 Go de RAM) mesure sur la flotte vivante.
Deux avertissements y figurent parce qu'ils ont deja coute cher ici : un gabarit
personnalise recopie son identite dans chaque clone, et un blocage contourne en
silence se paie en heures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On savait que la donnee partait et arrivait. Pas qu'elle revenait.
Trois charges critiques eprouvees :
- cles de l'AC (infra-pki-01) : restauration + comparaison OCTET POUR OCTET,
12 fichiers, 11 identiques, root_ca_key et intermediate_ca_key compris. Seul
ecart : db/000000.vlog, le journal badger de step-ca, qui avance a chaque
emission. Attendu.
- annuaire (idm-01) : slapadd -u (essai a blanc) sur le LDIF restaure —
REJOUABLE, 7 entrees dont uid=sysadmin.
- bases (data-sql-01) : section forgejo REJOUEE dans une base d'epreuve —
0 erreur, 130 tables, comptes reels. Production verifiee intacte apres.
Controles negatifs : un LDIF corrompu fait sortir slapadd en 1 ; la garde SQL a
refuse une section mal decoupee.
LE PIEGE pg_dumpall : il ecrit CREATE DATABASE <suivante> AVANT le \connect
correspondant. Decouper « du \connect X au \connect suivant » emporte un ordre
visant une AUTRE base. Ma premiere decoupe l'a fait ; la garde a refuse de
rejouer. Consigne dans runbooks-exploitation.md §5.
valider.yml ne ment plus : il exigeait une restauration de TOUS les noeuds
client_backup, donc echouait sur ceux qui ne detiennent rien et sur les depots
vides. Quatre verdicts desormais (OK / A CONFIRMER / SANS OBJET / ECHEC),
ALIGNES sur ceux de la supervision. Et il prouve que l'annuaire est rejouable,
pas seulement present.
make valider : 0 echec sur toute la flotte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Objectif : supprimer le `curl -k` du rapporteur. Fait, et la maniere a ete
imposee par la mesure.
SERVIR UN CERTIFICAT STEP-CA SUR L'API EST IMPOSSIBLE. Icinga le dit lui-meme :
« Our certificate will expire soon, but we own the CA. Renewing. » Il renouvelle
tout certificat expirant sous 30 jours ; les notres vivent 24 h ; possedant une
AC, il re-emet avec la sienne et ecrase le notre a chaque demarrage. Collision
entre deux politiques de PKI, et la notre n'est pas negociable.
Deux decouvertes en chemin, toutes deux par le garde-fou `icinga2 daemon -C`
ajoute au role, qui a ARRETE le deploiement avant de redemarrer la supervision :
- cert_path/key_path/ca_path sont DEPRECIES depuis 2.8 ; les poser reveille un
chemin de code herite qui exige en plus un objet Endpoint ;
- l'identite de l'API est le CN du certificat. NodeName valait « mon-01 », donc
le SAN aussi, alors qu'on appelle par le FQDN : aucune verification n'aurait
pu reussir. NodeName est desormais aligne sur le FQDN.
A LA PLACE : Icinga garde son AC (domaine de confiance FERME, legitime) et
backup-01 verifie le pair contre CETTE AC, recuperee depuis mon-01 au
deploiement. Le pair est authentifie ; seule la racine differe.
Controle negatif — une verification qu'on ne teste pas est un ornement : avec la
mauvaise AC (step-ca), curl refuse (« unable to get local issuer certificate ») ;
avec la bonne, les neuf rapports passent.
Sous-AC step-ca ecartee : elle poserait sur l'hote de supervision une cle capable
d'EMETTRE pour n'importe quel nom, alors qu'on a choisi le sens du flux pour que
compromettre mon-01 ne donne rien.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Icinga ne surveillait rien : aucun objet Host ni Service de Set-OPS, seulement
la config Debian d'origine sur localhost.
Superviser setops-sauvegarde.service aurait reproduit le defaut du jour meme :
l'unite etait VERTE sur onze noeuds pendant qu'elle n'emportait rien. Le noeud
sait qu'il a LANCE sa sauvegarde, pas qu'elle est ARRIVEE. backup-01 evalue donc
ses depots et pousse un resultat passif par noeud vers l'API Icinga.
Trois criteres, parce qu'un seul suffit a mentir : l'instantane existe, il est
recent (26 h / 50 h), il contient au moins un fichier.
Le sens du flux est delibere : le depot parle a la supervision, jamais l'inverse
— compromettre mon-01 ne donne aucun acces aux sauvegardes.
Le ttl de 6 h fait la fraicheur : si le rapporteur se tait, Icinga perime les
services tout seul. C'est le silence qui a laisse le defaut vivre un mois.
Deux erreurs corrigees par la mesure :
- --data-urlencode refuse en Bad Request (l'API veut du JSON) ; le flux, le TLS
et l'auth marchaient, seule la charge etait perdue.
- le seuil « vide » en octets signalait a tort idm-01 (2363 o) : un export LDIF
d'un annuaire a un compte pese cela. « Vide » se mesure en FICHIERS. Et le
verdict est un AVERTISSEMENT : la machine ne distingue pas « les donnees ont
disparu » de « il n'y en a pas encore ».
Reserve assumee : curl -k — l'API presente le cert de sa propre AC, pas step-ca.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Correction : l'entree precedente attribuait le defaut a la reconstruction
from-zero. C'est faux — le commit fondateur 7476a54 (2026-07-03) disait lui-meme
« Reste : jobs Tier 1 (pg_dump/slapcat/vmail/forgejo) ». Ils n'ont jamais ete
ecrits, et infra-pki-01 a ensuite perdu son integration client_backup.
Le role qui POSSEDE la donnee dit comment la sortir : client_backup_jobs est
l'intersection du catalogue et des group_names du noeud. Un tenant qui deplace un
service emporte sa sauvegarde avec lui. On sauvegarde l'etat NON REGENERABLE :
ni zones PowerDNS ni tableaux Grafana, ils se redeploient.
L'unite qui ment est RETIREE, pas rendue bloquante : refuser le deploiement aurait
casse infra-edge-01, infra-dns-01 et mon-01, qui ne detiennent legitimement rien.
Le defaut etait le timer qui echouait chaque nuit en donnant l'apparence d'une
sauvegarde.
P36 (D-75) lit les groupes detenteurs dans le catalogue : ajouter un role au
catalogue etend la preuve du meme geste. Elle a attrape infra-pki-01 — les cles
de l'AC — corrige au plan.
Mesure hors-noeud : collab-01 64,0 MiB/272, edge-mta-01 4,4 MiB/139,
data-sql-01 1,0 MiB (pg_dumpall complet), forge-01 26,4 KiB/68,
infra-pki-01 20,1 KiB/21, idm-01 2,3 KiB/5 (slapcat). 9 hotes, 9 success.
Reste : rien ne surveille l'unite — c'est ce silence qui a laisse le defaut
vivre un mois.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Consigner une consequence connue (raser l'hote openldap detruit l'annuaire, donc
le compte sysadmin est recree depuis la voute et le changement force est rearme —
mesure : pwdReset TRUE) a fait apparaitre la perte reelle : par le §2 les
appartenances ne sont JAMAIS reconciliees, donc tout ce que l'exploitant a
construit est detruit et ne sera pas recree.
En cherchant ou pointer pour la restauration, mesure sur les 14 hotes :
- 11 hotes : setops-sauvegarde.service en echec chaque nuit, nothing to backup
- infra-pki-01, obs-01, backup-01 : aucune sauvegarde deployee
(et infra-pki-01 porte les cles de l'AC)
client_backup_jobs vaut [] par defaut et rien ne le surcharge dans l'instance.
Aucune donnee de cet ecosysteme n'est sauvegardee.
Le defaut n'est PAS corrige ici : il est rendu visible, avec la sortie manuelle
de l'annuaire en attendant. Une unite en echec qui n'alerte personne est le
second defaut a traiter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les roles installent, ils ne mettent pas a jour. Relever une version ne change
rien tant qu'une machine neuve ne la rencontre pas.
Trois VM rasees (raser HOTE= ne peut que restreindre), leurs trois bases
supprimees puis recreees vides depuis le registre. Resultat mesure sur la
machine et livre par le frontal : Keycloak 26.7.1, Forgejo 16.0.2,
Nextcloud 34.0.2.1. 33 couches, 0 echec, ~15 min contre 54.
Les deux verifications de signature PGP ont tourne en conditions reelles, sans
ignore_errors : un refus aurait casse le play avant le depot de l'archive.
L'epinglage sur la cle PRIMAIRE de Forgejo tient malgre une sous-cle differente.
Supprimer les bases n'etait pas une commodite : occ maintenance:install refuse
une base peuplee.
Sept devis CONFORME, prouver.py 35 OK / 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Question de l'exploitant : « ne devrait-on pas prendre les versions les plus
recentes ? » Non — resoudre « la derniere » au deploiement detruirait la
reproductibilite qu'on vient de prouver en rasant et remontant deux
ecosystemes, et ferait de chaque reconstruction une loterie. Six majeures de
Forgejo ne s'avalent pas en effet de bord d'un `make`.
Mais il avait raison de tirer le fil : L'ECART ETAIT INVISIBLE. Il a fallu
quatre requetes a la main pour decouvrir Forgejo 10.0.0 contre v16.0.2 publie,
Keycloak 26.0.7 contre 26.7.1, Nextcloud 34.0.1 contre 34.0.2, oauth2-proxy a
jour.
Le devis ne juge pas, il RENSEIGNE : un retard n'est pas une faute, il sort
donc en 0. Monter de version reste un acte explicite.
Ce qui le distingue d'une liste ecrite a la main : les versions epinglees sont
DERIVEES du depot. La source amont, elle, ne peut pas se deriver — elle depend
de l'editeur — et vit dans une table. TOUTE VERSION EPINGLEE SANS ENTREE DANS
CETTE TABLE FAIT SORTIR EN ERREUR : sans cette garde, un epinglage ajoute
demain vieillirait sans que personne ne le voie. Une exemption reste possible,
mais ecrite et motivee (deux le sont : la serie PHP de Debian, la version
PostgreSQL vide).
Eprouve dans les deux sens : un serveur_redis_version ajoute temporairement
fait sortir en 1 en le nommant ; retire, retour a 0.
Et il rappelle ce qu'on n'epingle PAS. Grafana n'a aucune version dans le
depot — le 13.1.1 vu dans l'historique apt etait ce que son depot servait ce
jour-la. Debian, Grafana, smallstep et Icinga prennent ce qu'on leur donne.
Deux regimes coexistent dans la meme flotte, et les taire donnerait l'illusion
que tout est maitrise.
Verifie : versions-mesurer code 0, garde eprouvee, prouver.py 35 OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Question de l'exploitant : « je ne vois nulle part un MTU a 1450 ». Fondee.
Le 1450 existait bien — MTU_OVERLAY_DEFAUT dans underlay.py, dont P23 derive
sa garde (transport >= overlay + 50), pose par le devis SDN sur chaque zone,
et verifie sur le cluster. Mais il ne se propage pas a la carte de l'invite :
les 14 VM tournaient a 1500, et cloner_vm_debian.yml n'avait aucune occurrence
de `mtu`. La VM emettait donc des trames que son propre chemin ne pouvait pas
encapsuler — la panne que le registre des flux appelle « la plus couteuse a
diagnostiquer ».
Invisible parce que les 14 VM vivent sur asgard : deux VM du meme hyperviseur
communiquent par le pont local, sans encapsulation. Le defaut serait apparu au
premier eclatement de la flotte — c'est-a-dire quand il faudra heberger les
deux tenants ensemble.
Corrige en DEUX endroits, avec `mtu=1` (« herite du pont ») plutot que 1450 en
dur : juste en SDN comme hors SDN, et encore juste le jour des trames jumbo.
Le GABARIT le porte — proposition de l'exploitant, meilleure que la mienne :
elle couvre les clonages qui ne passent pas par le playbook. Et le playbook le
repose a chaque clone, parce qu'un gabarit se RECAPTURE et que ce qui n'est pas
versionne se perd en silence.
make mtu-mesurer (7e devis) rattache chaque hote a sa zone par son pont derive
et lit le MTU attendu dans devis_sdn.py. Ecart trouve sur 14 hotes du premier
coup, puis confirme corrige.
ET J'AI CASSE LA FLOTTE EN CORRIGEANT : applique aux cartes de VM EN MARCHE,
le changement detache et rebranche la carte sans que l'invite reconfigure son
interface. 0/14 joignables pendant trois minutes. L'agent qemu, independant du
reseau invite, a permis de constater et de redemarrer par l'API. Dans le
playbook la meme tache s'execute AVANT le demarrage du clone : aucun risque.
Sur une VM en service : poser la config, puis redemarrer — une seule d'abord.
Verifie : mtu-mesurer CONFORME 14/14 a 1450, prouver.py 35 OK, ansible-lint
production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'epreuve de portabilite est passee. Un second ecosysteme souverain complet,
monte depuis zero par le meme moteur : 14 hotes, 2583 taches ok, 331 changed,
0 failed. Plan distinct, voute separee, realm technolibre, sa propre AC — et
une topologie differente : LDAP et SSO sur des machines separees la ou
Chezlepro les co-localise.
Cinq devis CONFORME (identite, certificats, PostgreSQL, courriel, frontiere —
55 lignes 0 ecart). Le sixieme dit exactement la bonne chose : les 6 services
repondent depuis l'edge, aucun depuis le poste, qui ne resout pas encore
technolibre.internal (6 entrees /etc/hosts absentes — le plancher).
SIXIEME DEFAUT MOTEUR. Le devis d'identite interrogeait LDAP en `ldapi:///`
— un socket UNIX LOCAL — depuis l'hote serveur_keycloak. Cela ne marchait que
par CO-LOCATION ACCIDENTELLE. Un tenant qui separe l'annuaire du SSO echouait
sur « Failed to import python-ldap » : l'hote SSO n'a pas de client LDAP. Les
deux lectures sont deleguees a l'hote DERIVE par resoudre_annuaire.
P35 (D-75) : toute application dont le role exige une base en a une au plan.
La garde de resoudre_base existait deja, mais s'est declenchee a la 92e tache
de collab-01, apres quarante minutes, pour un ecart entierement lisible dans
le plan.
Rien n'y est code en dur : les roles concernes sont ceux qui INCLUENT
resoudre_base, et le groupe reclame est lu dans le DEFAUT de la variable
passee — jamais deduit du nom. serveur_icingaweb2 reclame la base de
serveur_icinga ; une preuve supposant « role = groupe » aurait crie sur un cas
sain.
Eprouvee dans les deux sens ET sur les deux tenants, dont les registres n'ont
pas la meme portee : base retiree -> ECHEC la nommant ; restauree -> OK.
Verifie : prouver.py 35 OK sur les deux instances, ansible-lint production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La refonte de ce matin posait une convention. Une convention qu'on n'outille
pas tient tant que quelqu'un y pense : c'est le raisonnement de D-70, applique
au corpus documentaire.
Etat de depart mesure : 2 documents sur 34 declaraient leur lecteur. Les 32
autres disaient leur SUJET — ce qui avait enfoui le runbook de reprise le plus
utile du depot au §6 de autorisation.md.
Les 38 le declarent desormais, lecteur determine document par document et non
colle au gabarit : l'exploitant (devis, migration de tenant, cycle de vie,
gabarit d'or), le mainteneur (conceptions, registres, carte), le lecteur
externe (ecosysteme-chezlepro), l'agent IA (MISE-A-JOUR-CODEX-CLAUDE).
Deux exemptions DERIVEES, pas listees — un chemin en dur aurait vieilli a la
premiere page ajoutee : un document qui s'annonce genere, et un fragment sans
titre. Les 13 exemptes verifies un par un ; aucun document ecrit a la main
n'est exempte par accident. La preuve ne lit que l'EN-TETE, ce qui empeche
frontiere-opnsense.md et plan-et-generation.md — qui parlent de generation
dans leur corps — d'etre exemptes a tort.
Eprouvee dans les deux sens. Elle a echoue seule des sa premiere execution en
nommant deux documents que mon inventaire avait manques (docs/audit/). Puis
test negatif delibere : declaration retiree de meta-classe.md -> ECHEC la
nommant ; restauree -> OK.
Ce qu'elle ne teste pas : que le lecteur declare soit le BON. Ca se juge en
revue ; elle garantit qu'on a du y penser.
P01–P34. Comptes perimes corriges au passage (AGENTS.md et devis-services.md
annoncaient encore 30 preuves).
Verifie : prouver.py 0 (34 OK), plan-recette inchange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README.md ouvre desormais sur « par ou entrer, selon ce que tu viens faire » :
monter (QUICKSTART), heriter (wiki Reprendre l'ecosysteme), modifier
(carte-set-ops), apprendre (wiki Home). On n'arrive pas avec un sujet, on
arrive avec une situation.
carte-set-ods.md et wiki/Home.md declarent leur lecteur — le mainteneur et
l'apprenant — et renvoient aux deux autres portes. C'est la convention qui
empeche la rechute : un document qui declare son lecteur se range tout seul.
La regle du miroir est ecrite aux trois endroits ou elle se lit : le wiki est
publie DEPUIS le depot, une page modifiee dans l'interface de la forge est
detruite a la publication suivante.
Corrige au passage les comptes perimes de la carte (26 docs + 7 audits + 21
unites -> 34 + 15 + 23, et un README pour chacun des 54 roles).
Le lien vers la page accentuee est percent-encode : aucun precedent de lien
accentue hors du wiki dans ce depot, et le rendu du depot n'est pas celui du
wiki.
Verifie : les cinq liens relatifs du README resolvent, chaque porte declare
son lecteur, prouver.py 0 (33 OK), plan-recette inchange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trois des cinq pieges restants avaient deja une page depuis que la semaine a
avance : le connect() vers le vide (frontiere-opnsense.md + le controle porte
par frontiere-mesurer), le `make prouver` vert (wiki « Verifier le deploye »),
et le banner exchange. Les deux orphelins — kcadm -s sur une map, grafana-cli
dans une base fantome — s'adressent a qui ECRIT du code, pas a qui reprend
l'exploitation : ils n'ont rien a faire dans un chemin de reprise.
Une page separee aurait redit ce que trois autres disent deja, et aurait donc
vieilli mal — le reproche exact qu'on faisait a sa version longue.
La section ⑤ de « Reprendre l'ecosysteme » porte desormais la REGLE qui les
relie (verifier l'instrument avant d'accuser le composant) et un tableau de
trois lignes qui renvoie chacune a son domicile. Plus de promesse en attente.
Et le banner exchange n'avait AUCUN domicile : le savoir vivait dans un
commentaire du Makefile et trois entrees du CHANGELOG — nulle part ou on le
cherche, exactement le mal que la refonte traite. Ecrit en runbook §3, avec
ses trois causes par frequence et ce qui tranche dans l'ordre.
runbooks-exploitation.md declare aussi son lecteur, comme le veut la
convention validee.
Verifie : chaque renvoi controle un a un, prouver.py 0 (33 OK), plan-recette
inchange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'exploitant a retire la derniere regle heritee. `make frontiere-mesurer`
rend CONFORME (code 0) : tout ce qui est declare est livre, tout le reste est
refuse — y compris collab-01:9980, le seul qui livrait vraiment un HTTP 200
depuis le poste.
Et mon instrument avait tort, pas la frontiere. Il comptait 38 ecarts en
concluant depuis le client : « connexion etablie => la bordure a relaye ».
Faux, verifie A LA DESTINATION : pendant que le poste tenait une connexion
« etablie » vers idm-01:389, idm-01 n'en voyait aucune ; collab-01 n'en
voyait aucune sur 9980. La frontiere repond a la poignee TCP sans relayer.
Le devis raisonne desormais sur la LIVRAISON seule, et le controle ne rend le
releve NUL que s'il LIVRE des donnees — qu'il ressorte AMBIGU est attendu ici
et le rapport le dit a chaque execution. Cette relaxation rend aussi le sens
sortant mesurable : il etait declare NUL en permanence.
Nouveau mot-cle `poste: false` dans meta/flux.yml : un service publie n'est
pas forcement fait pour un poste de travail. Le 25 entrant de Postfix est un
flux serveur a serveur ; la frontiere l'etendait au VLAN d'administration ou
le nftables de l'hote le refusait. Deux regles retirees. Le mot-cle vit avec
le role qui sait ce que son port veut dire ; le generateur ne connait
toujours aucun numero de port.
Verifie : frontiere-plan sans ecart (41 regles, 12 routes), flotte 14/14,
frontiere-mesurer CONFORME, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
devis_expositions pose la question positive (une exposition declaree
repond-elle). Celle-ci manquait, et ce n'est pas la meme : un pare-feu peut
servir tout ce qu'on lui demande ET laisser passer tout le reste.
Rien n'y est saisi. Les cibles sont les ports REELLEMENT en ecoute (sonder un
port ferme ne prouverait rien du pare-feu) ; la politique attendue est lue
dans devis_opnsense.py, la source qui configure la frontiere.
scripts/sonde_tcp.py refuse de conclure : un CONTROLE (adresse ou personne
n'ecoute) avant tout verdict — s'il repond, le releve est declare NUL. Et
etablir n'est pas livrer : banniere, sinon requete HTTP, sinon poignee TLS ;
sans reponse le verdict est AMBIGU, jamais « ouvert ».
Deux pieges rencontres en le construisant, corriges et commentes sur place :
la sonde « tenant » s'executait sur le poste (dans un play connection: local,
delegate_to herite de cette connexion) ; et 2 s de lecture faisaient ressortir
un Postfix sain en AMBIGU (postscreen retarde sa banniere expres) — porte a
8 s, compense par du parallelisme.
Premier verdict, frontiere en l'etat : 38 ecarts, dont collab-01:9980 qui
livre vraiment un HTTP 200 depuis le poste. Releve sortant declare NUL.
Verifie : ansible-lint Passed, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Validation a l'instrument de l'exigence « aucun trafic impertinent », par de
vraies requetes applicatives contre des destinations interdites.
Le transit tenant etait deja correct : hyperviseur, frontiere, poste et
Proxmox tous muets ; le 25 sortant passe depuis edge-mta-01 (banniere
220 mx.google.com) et est refuse depuis infra-dns-01.
Trou 1, a nous : les flux sortants visaient `any`, donc n'excluaient ni le
plan de gestion ni le voisin — https://10.0.0.1/ repondait depuis une VM.
Ils visent desormais !SETOPS_INTERNES, destination NIEE valant les trois
blocs prives RFC 1918. Pas la liste de nos reseaux : elle laissait dehors
192.168.11.0/24, le plan de gestion herite.
Trou 2, pas a nous : la regle d'usine « Default allow LAN to any » privait
notre defaut-deny de tout effet (mesure : le 443 d'un nginx repondait depuis
le poste alors que seul le 22 est declare). Aucune API ne l'expose. Set-OPS
declare donc les flux d'administration legitimes vers les services publies,
pour que la desactiver ne coupe pas l'exploitant de ses consoles web.
Verifie apres application : 10.0.0.1:443 bloque depuis le tenant, sortie web
+ DNS + SMTP public toujours passants, curl vers le nginx du tenant -> 302,
flotte 14/14, frontiere-plan sans ecart, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Retire les deux routes /16 (les douze /24 attribues suffisent) et retrecit les
alias SETOPS_TENANT_* du supernet aux memes /24 : nos propres regles
autorisaient jusqu'ici « admin -> tout le /16:22 ».
Ajoute la garde qui l'aurait attrape : verifier() exige que l'ensemble des
reseaux routes et l'ensemble des reseaux autorises coincident exactement.
Attachee a P24, qui ne verifiait que la traduction NAT.
Fait entrer les routes dans appliquer_opnsense.py — elles etaient posees a la
main, donc reconciliees par rien : identite portee par la description
(setopsroute:<tenant>:<reseau>-><saut>), creation avant retrait, perimetre
strict. Le nom de passerelle est resolu depuis l'adresse du prochain saut.
Le symptome du connect() qui aboutit toujours subsiste et n'est pas de notre
fait : l'etat pf porte la regle d'usine « Default allow LAN to any rule ».
Mesure qui tranche : depuis une VM du tenant, 172.31.99.99 « s'etablit » en
1 ms sans rendre de banniere.
Verifie : frontiere-plan sans ecart (12 routes), flotte 14/14, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Router 10.27.0.0/16 entier faisait porter a la frontiere des destinations qui
n'existent nulle part. Ces paquets atteignaient le noeud de sortie, y
arrivaient dans la table PRINCIPALE — pas dans le VRF, qui n'est atteint que
par les /24 annonces en BGP — et repartaient vers la passerelle du reseau
d'ADMINISTRATION. Mesure : ip route get 10.27.99.99 rendait via 192.168.11.254.
C'est aussi ce qui faisait reussir tout connect() depuis le VLAN
d'administration, y compris vers des adresses inexistantes — symptome attribue
pendant deux jours a une fonction d'anti-usurpation de la frontiere, alors que
c'etait un routage trop large.
Le devis emet desormais une route par sous-reseau attribue (12 au lieu de 2).
Le NAT reste sur le supernet : il porte sur la SOURCE, qui contient tous les
sous-reseaux — la garde P24 itere sur les alias et reste satisfaite.
Verifie avant de livrer : les 14 hotes du plan sont tous dans les six /24,
aucun ne serait coupe. L'applicateur ne gere pas les routes (0 mention) : le
devis prescrit, l'exploitant applique — D-23/D-24, et ce chemin est celui de
son administration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
P22 garde la synchronisation entre le wiki et docs/audit/plan-de-recette.md.
En ajoutant l'unite « Verifier le deploye », j'ai rendu le plan perime et la
preuve l'a vu aussitot — une garde que le depot avait deja et que j'avais
oubliee.
Et j'ai POUSSE le commit precedent avec cette preuve en echec, pour la
DEUXIEME fois, avec la meme cause : mon garde-fou etait
grep -E '^CONFORME|^NON CONFORME', qui reconnait « NON CONFORME » comme une
correspondance et laisse passer la chaine &&. Je l'avais documente hier sans
changer l'habitude. Desormais : le CODE DE SORTIE de prouver.py.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le wiki datait du 3 aout. Verifie avant d'y toucher : toutes les cibles make
citees existent, aucune commande morte.
La-preuve.md annoncait P01-P21 ; le harnais est a P33. Les trois nouvelles
ajoutees, et surtout la page dit maintenant ce que ces preuves NE FONT PAS :
elles sont statiques, elles lisent le depot, et c'est dans cet angle mort
qu'une AC est restee expiree huit heures sous un harnais vert.
Infra-as-Code-et-idempotence.md enseignait l'idempotence comme acquise. Vrai
role par role, faux a l'echelle de la flotte. La page enseigne desormais
depuis les chiffres (924 -> 17 -> 0), raconte ce que les 17 cachaient, et
explique pourquoi il faut verifier le zero lui-meme.
Nouvelle unite « Verifier le deploye » : la difference entre valider du code
et verifier un systeme, avec les trois regles qui separent un devis utile d'un
devis decoratif.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
924 (rejeu depuis zero) -> 17 (2e passage) -> 0 (3e). Zero tache changed,
zero echec sur les quatorze hotes. Le depot n'avait jamais fait ce test a
l'echelle de la flotte.
Verification du zero, parce qu'un zero peut signifier que les roles ne font
plus rien : PLUS de taches se sont executees au passage a vide qu'au rejeu
(2266 contre 2152). Elles ont toutes tourne et toutes trouve le systeme
conforme. Un zero obtenu avec MOINS de taches aurait dit l'inverse.
Ce que le test a rapporte : trois defauts, dont deux n'etaient pas des defauts
d'idempotence mais des PANNES SILENCIEUSES — node_exporter mourait a chaque
renouvellement de certificat sur les 14 hotes, et chaque deploiement
invalidait les jetons OAuth2 de la forge.
Le chiffre devient la ligne de base : un deploiement futur qui rapporte
changed sur une flotte non modifiee signale desormais quelque chose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le groupe etait TOUJOURS vide et rien ne le signalait. instancier l'emet comme
squelette, les etats d'un serveur ne connaissent que actif et planifie — aucun
chemin ne permettait d'y faire entrer une machine. Les trois cibles recevaient
« skipping: no hosts matched », qui n'est pas une erreur.
Le gabarit ne PEUT PAS venir du plan : sa config Proxmox le place sur le
reseau de fabrication (192.168.12.99/24), pas dans le supernet. Ce n'est pas
un hote de l'ecosysteme, c'est la matrice dont il est tire. Le forcer dans
plan/serveurs.yml aurait ete le mettre dans un registre qui n'est pas le sien.
MODELE_HOTE=<ip> le designe ; l'inventaire d'un seul hote sert les trois
playbooks, prerequis d'acces et de privileges compris. Sans lui, elles
REFUSENT en expliquant au lieu de ne rien faire.
Meme famille que le reste de la journee : une capacite declaree dont personne
ne verifiait qu'elle est branchee — a ceci pres qu'elle ne se manifestait par
aucun symptome. Elle ne faisait rien, poliment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le test unitaire a rejete mon ajout d'export — c'est exactement son role : il
epingle le contrat de parametres-proxmox, et une variable de plus est un
changement de contrat qui doit etre declare, pas subi.
J'ai pousse le commit precedent AVEC cette preuve en echec. Cause : mon
garde-fou etait « grep -E 'CONFORME|ECART' », qui correspond aussi a « NON
CONFORME ». Un motif qui ne sait pas distinguer le succes de l'echec ne garde
rien — meme famille que les criteres creux de P31.
Harnais de nouveau a 33 preuves, 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Question « on peut l'optimiser ? » — mesure avant de repondre. Rien a gagner
cote performance (UEFI/q35, virtio-scsi-single + iothread, discard+ssd,
x86-64-v2-AES, agent, balloon 0) ni cote paquets : le socle est deja cuit dans
l'image.
Ce que le gabarit transporte, c'est son lieu de naissance. searchdomain
chezlepro.ca — le domaine PUBLIC — etait herite par les 14 VM, faute de
proxmox_clone_domaines_recherche defini. Desormais derive de domaine_interne
via SETOPS_DOMAINE, par le mecanisme qui existait deja pour le DNS.
Honnetement : ca ne reparait pas de panne. serveur_debian reecrit resolv.conf
au deploiement sans ligne search — verifie sur la flotte. C'etait faux et ca
ne tenait que par chance.
template_cleanup vide desormais /etc/resolv.conf a la capture : un gabarit ne
transporte aucune identite de reseau.
Notes : mtu 9000 est un reglage MORT (les clones tournent en 1500) ; et le
groupe modeles_vm est VIDE, donc preparer/verifier/nettoyer-modele n'ont
aucune cible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deuxieme des trois chantiers ouverts par la reconstruction. Retrouve son
defaut n6 a froid, sans machine.
Le SASL de Dovecot et l'interface d'Alloy se disputaient le 12345 sur
infra-mail-01 depuis le premier jour, et c'est Dovecot qui perdait EN SILENCE.
Il a fallu inverser l'ordre de demarrage — ce que fait un rejeu depuis zero —
pour que ca devienne audible.
Le controle n'etait possible qu'apres avoir DECLARE le port d'Alloy : un port
SUBI (defaut amont d'un logiciel qu'on n'a pas choisi) n'existe pour aucun
registre, donc aucune preuve ne peut le voir. Il faut l'imposer pour le
verifier.
partage: true — nouveau mot du registre — distingue « j'ouvre cette ecoute »
de « je decris celle d'un autre » (serveur_backup empruntant le sshd de
serveur_debian). Sans lui, la seule co-location legitime de la flotte serait
signalee a tort, et une preuve qui crie sur un cas sain finit par etre ignoree.
Verifie dans les deux sens : 32 revendications sans collision sur le reel ;
en remettant Alloy a 12345, le defaut n6 est nomme, code 1.
Harnais : 33 preuves, 0 echec, 0 sautee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Premier des trois chantiers rendus evidents par la reconstruction. Il aurait
trouve son defaut n1 — amorcage_acces_courriel — SANS RIEN DETRUIRE.
Un assert de role declare un contrat ; rien ne verifiait que l'instance
l'honore, et le manque ne se voit qu'au moment ou la garde s'execute — donc,
pour un intrant d'amorcage, seulement en repartant de rien.
Satisfait par : defaut non vide (vault_* compris, gardes par P18), set_fact de
resolveur, ou declaration de l'inventaire. Aucune voute dechiffree : la preuve
reste statique.
Deux fois mon instrument a accuse le composant a sa place, avant meme sa
premiere execution utile : il criait au manque sur
serveur_postfix_mailstore_hote, pourtant fourni — je ne lisais pas le fichier
d'inventaire, puis je n'y cherchais que les blocs vars: alors qu'instancier
ecrit sous le nom d'hote.
Verifie dans les deux sens : 30 exigences satisfaites sur le reel ; sur un
double sans la declaration d'hier, le defaut n1 est nomme, code 1.
Harnais : 32 preuves, 0 echec, 0 sautee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ecosysteme chezlepro detruit (14 VM, disques compris) puis rejoue depuis le
plan seul, sans restaurer aucune sauvegarde. Les cinq devis rendent le meme
verdict qu'avant la destruction.
Premiere fois que le depot peut affirmer que le SYSTEME RECONSTRUIT est celui
que le plan decrit, au lieu d'affirmer que le depot est coherent avec lui-meme.
Six defauts trouves, tous invisibles autrement : trois d'ordre, deux courses de
premier demarrage, un conflit de port. Ils dormaient tous derriere un etat
preexistant — un compte deja la, des roles crees par un passage anterieur, des
clients existants, un service qui tournait depuis toujours, un port deja tenu.
Le rejeu n'a rien casse : il a retire l'etat qui masquait.
Refait a la main : la seule base de Grafana, dont le schema etait reste a
moitie migre apres l'interruption du defaut n5. Rien d'autre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sixieme et dernier arret de la reconstruction from-zero, et le seul qui ne soit
ni un ordre ni une course : un vrai conflit de port.
infra-mail-01 : dovecot ecoute 0.0.0.0:12345 (serveur_dovecot_sasl_port,
choix delibere de Set-OPS pour la soumission :587)
alloy : defaut amont 12345 -> bind: address already in use
Le premier demarre gagne. En exploitation courante le conflit DORMAIT : Alloy
tenait le port depuis toujours et c'est l'ecoute SASL de Dovecot qui echouait,
en silence. L'ordre des couches d'une reconstruction inverse les roles et le
rend visible.
Le vrai defaut n'est pas le numero : c'est qu'un port SUBI ne se declare nulle
part, donc aucun controle ne peut voir la collision. Le port est desormais
IMPOSE (--server.http.listen-addr) et DECLARE dans meta/flux.yml avec
pair: localhost — une revendication de port, pas un flux entre hotes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contrainte de l'exploitant apres avoir vu backup-01 et collab-01 crees avant
l'AC et le DNS. Ma premiere reponse etait incomplete : un clone est bien
inerte, mais un echec a la 12e VM coute 40 minutes sans rien deployer, et le
journal donne l'impression que le moteur ignore ses couches.
Mesure avant de coder :
- enregistrements A : DEJA derives du plan (zone generee depuis hotes_actifs)
- zone inverse / PTR : n'existe NULLE PART, aucun role ne touche in-addr.arpa
- ordre d'amorcage : aucun, deployer-tout est par couches
Le premier point a reduit le travail de moitie — j'allais ecrire un enrolement
DNS par hote alors que la zone directe etait deja correcte.
Zone inverse derivee du supernet (27.10.in-addr.arpa), PTR issus de la MEME
source que les A : pas d'endroit ou elles puissent diverger. Vide si le
supernet n'est pas un /16.
_amorcer-socle monte l'AC puis le DNS completement avant deployer-tout ; les
deux derives de applications.<app>.hote, dans un ordre causal et non
alphabetique. Deux exceptions assumees : l'AC s'auto-signe, le DNS pose son
propre enregistrement.
P10 a attrape un handler que je venais d'inventer (Recharger PowerDNS au lieu
de Validate and reload PowerDNS) avant tout deploiement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
« Pourquoi pas make myDay ? » — la cible existe (alias strict de reconstruire)
mais n'avait aucun texte d'aide, et P31 la declarait conforme. Le motif etait
^[a-z][a-z0-9_-]*: — toute majuscule echappait au controle. myDay est citee
dans l'aide du Makefile et dans la GUI, et n'apparaissait dans aucun
recensement.
Une preuve ne vaut que ce que vaut son motif. Celle-ci a ete ecrite avec la
conviction d'etre rigoureuse et testee dans les deux sens le jour meme. Le
trou a ete trouve par une question, pas par un test.
Troisieme fois sur la meme preuve en une journee, apres le rapport genere qui
se citait lui-meme et l'inventaire genere qui l'aurait satisfaite par
construction. La difficulte n'est pas d'ecrire un test, c'est de delimiter
honnetement ce qu'il regarde.
87 cibles documentees, 36 scripts, 54 roles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ajoutee pour rendre la reconstruction from-zero REPETABLE — un test qu'on ne
peut jouer qu'une fois n'est pas une recette.
Verrous, tous eprouves avant usage :
- VMID derives du plan uniquement (le gabarit dore est structurellement exclu)
- le NOM doit correspondre : un VMID du plan sous un autre nom fait refuser
l'operation ENTIERE. Pas theorique — le 2026-08-07 une VM heritee portait un
VMID du plan sous le nom web-frontal-01 et proxmox_kvm rapportait ok.
- il faut NOMMER l'ecosysteme (INSTANCE=) : le symlink instance/ peut pointer
n'importe ou ; taper le nom distingue le POC de la production.
- CONFIRMER=true ; sans lui, inventaire et rien d'autre.
Le verrou du nom est le seul qu'on ne peut pas eprouver sur le vrai cluster
sans y fabriquer une collision : scripts/tests/test_raser.py l'isole derriere
un faux cluster, rattache a P02. Harnais : 31 preuves, 0 sautee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les 14 VM sont un POC, pas de la prod (recadrage de l'exploitant). Et
reconstruire Chezlepro est un MEILLEUR test que construire Technolibre : on a
un etat de reference — les 5 devis y sont CONFORME. Toute divergence apres
rejeu sera un defaut reel, mesurable. Sur Technolibre, qui n'a jamais tourne,
un echec serait ambigu.
Fige : les 5 verdicts, les 14 hotes (adresse + services), et ce qui sera perdu
et devra etre refait a la main (cle racine de l'AC, donc la racine installee
dans le navigateur ; mot de passe sysadmin). Pour que « identique » soit
prouvable plutot que ressenti.
Verifie que rien de necessaire au rejeu ne vit dans les 14 VM : les voutes et
le mot de passe de voute sont hors cluster, et origin est sur eregion
(192.168.12.201), machine distincte du tenant.
Constat : le moteur n'a AUCUN chemin de destruction. make reconstruire cree et
deploie, il ne rase rien.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Question de l'exploitant : « Set-OPS trichait ? ». Non, et c'est le depot qui
le prouve : onze de ses propres promesses publiques marquees FAUSSES, un
perimetre declare (« aucune VM / Proxmox / reseau touche »), et D-25 qui en
fait une regle. Un systeme qui triche n'ecrit aucune de ces trois choses.
L'angle mort etait ailleurs, et il est ferme depuis ce matin : les 30 preuves
sont statiques. « CONFORME : 30 preuves » se lit comme « le systeme
fonctionne » alors que ca veut dire « le depot est coherent avec lui-meme ».
C'est ainsi que le certificat de l'AC a pu expirer 8 h sous un harnais vert.
Les 11 rejugees, chacune reconfrontee au depot : toutes resolues. Preuve
consignee ligne par ligne.
Le rejugement a trouve mieux qu'un registre oublie : les resolutions etaient
DEJA documentees en Phase 3, mais le tableau de synthese annoncait encore
« fausse : 8 ». Deux representations du meme fait, une corrigee et l'autre
non, rien qui verifie qu'elles se rejoignent — le defaut que ce registre
existe pour traquer, applique a lui-meme. Il penchait du bon cote, ce qui l'a
rendu invisible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Directive de l'exploitant : la doc dit et explique tout ce que Set-OPS fait.
Une exigence seulement enoncee pourrit en silence — trois exemples le jour
meme dans la carte.
Ecart mesure : 66 cibles make sur 85 sans texte d'aide (make aide en montrait
19), 11 scripts sur 35 cites nulle part. Les 66 cibles ont recu leur aide :
85 commandes documentees.
P31 garde le couvert. Le chemin pour l'ecrire a ete instructif : deux fois mon
critere s'est revele creux. D'abord « le nom apparait dans un document » — le
rapport d'audit GENERE recopiait les noms manquants dans son message d'echec.
Puis j'ai failli refaire le trou en plus grand : generer un inventaire de
l'outillage aurait satisfait le critere par construction. Un critere qu'on
peut satisfaire en generant du texte ne prouve rien.
P31 teste donc que chaque script porte une docstring qui l'explique et reste
ATTEIGNABLE (cible make ou autre outil), que chaque cible porte son aide (sauf
les internes prefixees _, exemption nommee), que chaque role a son README.
Verifiee dans les deux sens.
Ce qu'elle ne garde pas, et c'est dit dans son code : que l'explication soit
bonne. Le pourquoi se juge en revue.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pas de refonte : 54 roles / 54 README, 34 documents, une carte, un registre de
decisions. Le retard etait ailleurs — le travail du jour vivait dans son coin,
les cinq devis n'existant que dans deux fichiers. Donc decouvrables seulement
par qui connait le Makefile, ce qui contredit « exploitable sans IA ».
Tisses dans les quatre points d'entree : ligne « Conformite du deploye » dans
la carte, section « Ecrire, puis relire (D-68) » dans AGENTS.md, §6.0 du
runbook (le premier reflexe), vue Reconstruction de la GUI.
Le tissage a fait tomber trois affirmations perimees :
- la carte annoncait 28 decisions, il y en a 66 en vigueur (D-01 -> D-69) ;
- elle disait les acces « decides, non construits, ou=people et ou=groups
restent vides » — mesure : un compte, un groupe, chaine exercee de bout en
bout sur Icinga Web 2 le jour meme ;
- la GUI parlait des « deux » devis d'infrastructure ; il y en a quatre.
Formation et wiki differes : la reconstruction from-zero est le test de cette
documentation, et enseigner une procedure que personne n'a executee serait
enseigner une hypothese.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Question de l'exploitant apres deux pannes causees par kcadm. La reponse est
non, et elle se fonde sur la mesure : sur six familles de defauts du jour,
deux seulement viennent d'un CLI ; un module Ansible (ldap_entry) a commis la
meme faute, et trois autres viennent d'un grep de fichier, de la precedence
Ansible et de mon propre comparateur.
D-68 — ecrire, puis relire et comparer, quelle que soit l'interface ; choisir
celle dont le chemin de lecture parle le meme langage que celui d'ecriture.
Une API est souvent preferable parce qu'elle rend la ressource ENTIERE, ce qui
permet le patron de chaque devis. Mais la plupart de la flotte n'a pas d'API,
et postconf -h / -e sont parfaitement symetriques.
D-69 — sur Keycloak : l'API pour toute map ou collection, kcadm ailleurs
(vocabulaire de la doc du produit, donc lisible sans IA).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Arbitrage rendu : le courrier local est route par uid, plus par l'attribut
mail. Dovecot le faisait DEJA (mail_home = /var/vmail/%{user | username}) ;
les deux moities ne s'accordaient que par coincidence, tant que mail valait
uid@<domaine_interne>. Cout assume : l'adresse interne est derivee et ne se
choisit plus ; en echange mail redevient libre de porter la vraie adresse de
la personne.
Puis la preuve de bout en bout a revele bien pire : toute livraison interne
etait DIFFEREE.
SSL_connect error to infra-mail-01:24: Connection timed out
status=deferred (Cannot start TLS: handshake failure)
Postfix etait durci (lmtp_tls_security_level = verify), le port LMTP de
Dovecot ecoutait en clair — son ssl = required global ne concerne que les
services de connexion. Les deux cotes d'un meme flux avaient ete traites
separement. Corrige et accorde : ssl = yes sur l'inet_listener (TLS implicite)
et lmtp_tls_wrappermode = yes cote client ; l'un sans l'autre ne marche pas.
Livraison prouvee : status=sent (250 ... Saved), message dans
/var/vmail/sysadmin/Maildir/.INBOX/new/.
Le devis disait CONFORME pendant ce temps : il verifiait la resolution et les
dialectes, jamais si le courrier BOUGE. Un devis qui ne regarde que les
reglages ne dit pas si le service rend son service. Il releve desormais la
file d'attente et ses raisons ; test negatif : la panne est nommee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PostgreSQL — rend visibles deux defauts deja vecus ici : un reseau ECRIT dans
pg_hba au lieu d'etre derive, et une ligne host en clair la ou il faut hostssl
(verrou qui saute sans bruit, les clients verify-full continuant de marcher).
Interroge pg_settings, jamais le fichier : un grep de postgresql.conf annonce
ssl_cert_file=snakeoil alors que le serveur sert le certificat de l'AC — la
valeur vient d'un conf.d que le grep ne voyait pas. L'instrument etait
incomplet, pas la configuration.
Courriel — chaque maillon interroge la ou il dit la verite : postmap -q pour
la resolution LDAP de Postfix, doveadm user pour celle de Dovecot (le maillon
exact ou la livraison avait bloque), vraies conversations SMTP/IMAP. Verifie
aussi qu'une adresse INEXISTANTE ne resout pas — sinon la boite est un
fourre-tout et le devis ne mesure plus rien.
Trouve immediatement une divergence reelle : Dovecot connait la boite de
sysadmin@chezlepro.internal, Postfix ne sait pas y router (query_filter
(mail=%s), et l'attribut mail porte sysadmin@chezlepro.ca). Collision de
roles : une identite ne porte qu'une adresse mail et on lui en demande deux —
notification joignable hors du systeme, et cle de routage local. Arbitrage a
rendre avant correction.
Tests negatifs : PostgreSQL 3 ecarts nommes, code 1. Une variable morte
trouvee dans une branche que le cas nominal n'emprunte jamais.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Troisieme devis de service. Chaque expose: du plan repond-il, et si non, ou
ca casse.
- requete HTTPS complete avec la racine de l'AC, jamais un connect() : a
travers l'OPNsense (anti-spoofing) toute connexion TCP reussit, et en TLS
le silence apres connect() ne distingue pas un service sain d'un trou.
- deux points de vue : depuis l'edge (edge + dorsal) et depuis le poste
(DNS + frontiere + edge + dorsal). Leur difference diagnostique.
- un code n'est pas un verdict : mon premier comparateur laissait passer un
502 des deux cotes. Trouve par le test negatif, pas par la relecture.
Etat : les 6 expositions repondent des deux cotes. Test negatif (frontiere
qui bloque + dorsal tombe) : 2 ecarts nommes distinctement, code 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deuxieme application du patron devis/applicateur aux services. Trouve a la
premiere execution : sur infra-pki-01 — l'autorite elle-meme — le certificat
etait expire depuis plus de 8 h et le renouvellement echouait toutes les 14
minutes sur « 'step ca renew' requires the '--ca-url' flag ». Rien ne le
signalait.
Cause : sur l'hote de l'AC, /etc/step est le STEPPATH du SERVEUR, pas un
amorcage client — pas de defaults.json, et l'unite de renouvellement en
dependait. La lecon etait deja ecrite dans le commentaire de la tache
d'emission (« l'autorite ne bootstrape pas »), jamais reportee sur l'unite.
Le role ne pouvait pas non plus se soigner : la re-emission ne regardait que
la FORME (cert absent ou SAN manquant), jamais la validite. client_pki
verifie desormais l'echeance (client_pki_marge_renouvellement).
Ce qu'il a fallu desapprendre : les certificats vivent 24 h et se renouvellent
toutes les ~14 min ; « empreinte servie != empreinte disque » est l'etat
NORMAL. Comparer les empreintes aurait donne un verificateur qui crie en
permanence. Le signal est l'echeance de ce qui est SERVI, plus l'absence de
client_pki_reload_services.
Le devis a d'abord menti, du defaut meme qu'il traque : include_vars au niveau
du play prime sur les group_vars. Et le premier correctif a PARU marcher —
set_fact accepte un dictionnaire entier en argument libre sans erreur et n'en
fait rien. Il faut reimposer cle par cle. Les deux devis sont corriges et le
piege est consigne dans docs/devis-services.md avant d'ecrire le prochain.
Verifie dans les deux sens : CONFORME sur 14 hotes ; sur un releve ou l'on
rejoue une copie perimee en memoire, 2 ecarts et code de sortie 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Constat de l'exploitant : « ca fait beaucoup de trucs incoherents qu'on
debusque ensemble ». Il y a une raison mesurable — les 30 preuves de
prouver.py sont STATIQUES (0 appel reseau, 0 ssh, 0 ansible). Elles montrent
que le depot est coherent avec lui-meme ; aucune ne demande au systeme
deploye s'il ressemble a ce que le depot annonce. Les quatre defauts du jour
vivaient tous la.
La classe statique est presque epuisee : recensement des motifs « cree mais
ne reconcilie jamais » -> amorcage_acces (delibere, D-67), serveur_openldap
(corrige le matin), et un seul reste reel (rbac-oidc.yml). Une preuve
statique de plus aurait rapporte une ligne.
Le patron devis/applicateur (D-23/D-24) existait deja pour les quatre
pare-feu, jamais pour les services. make identite-plan l'y porte :
- playbooks/maintenance/devis-identite.yml RELEVE le declare et le reel
- scripts/devis_identite.py COMPARE (le raisonnement n'a rien a faire en
Jinja ; le depot a deja cette forme pour les devis reseau)
- le declare n'est jamais recopie : defauts du role + resolveurs. Un devis
qui redeclare ce qu'il verifie ne verifie rien.
Verifie dans les deux sens : CONFORME sur le systeme reel ; sur un releve ou
les quatre defauts du jour sont rejoues plus deux regressions, 6 divergences
listees et code de sortie 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Quatre defauts mesures dans l'integration Keycloak/LDAP, meme famille : une
valeur declaree d'un cote, consommee de l'autre, rien qui verifie la jonction.
1. Aucune regle ne s'appliquait sur le chemin d'un vrai utilisateur. Sonde :
« abcd » refuse par l'operation etendue LDAP, accepte par Keycloak (204),
puis actif pour l'authentification. Keycloak ecrivait userPassword en
direct (ppolicy aveugle) et le realm n'avait aucune passwordPolicy.
2. ldap_entry ne fait que CREER : la politique etait figee a sa creation. Le
depot disait pwdMustChange TRUE, le serveur FALSE — une reconstruction
from-zero aurait ressuscite la boucle du 2026-08-07. ldap_attrs state=exact
reconcilie la politique et l'overlay (DN lu, pas devine).
3. syncRegistrations absent : un compte cree dans Keycloak n'atteignait jamais
ou=people — acces web, aucune boite, invisible du modele de groupes.
4. Le prenom pointait sur cn (nom complet) : « Administrateur systeme systeme ».
Ajoute roles/resoudre_politique_mdp : LA declaration, traduite en pwdPolicy,
passwordPolicy et anti-force-brute. Les deux roles la consomment sans la
redeclarer.
usePasswordModifyExtendedOp ET validatePasswordPolicy : la seconde est
porteuse, Keycloak se liant en rootDN et slapd n'appliquant pas ses controles
de qualite au rootDN. La premiere seule aurait paru juste sans tenir.
Verification : abcd -> 400 « minimum length 12 » et absent de LDAP ; mot de
passe conforme -> 204 puis ldapwhoami accepte ; POST users -> 201 ET present
dans ou=people. Second passage des deux playbooks : changed=0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le realm portait une politique d'acces complete et aucun moyen d'ecrire a qui
que ce soit. Tout oubli remontait donc a l'exploitant, qui n'avait d'autre
choix que de manipuler le mot de passe d'autrui.
- serveur_keycloak/tasks/courriel-realm.yml : reconcilie smtpServer et
resetPasswordAllowed ; hote du relais DERIVE de applications.postfix.hote,
et refus explicite si le plan ne declare pas de MTA.
- passe par l'API d'administration : kcadm.sh accepte les deux formes -s sur
une map, sort en succes et n'ecrit rien (smtpServer reste vide).
- amorcage_acces_courriel redevient a declarer : cette adresse designe une
personne, hors du systeme qu'on amorce ; une boite interne serait illisible
tant qu'on n'a pas l'acces qu'on cherche justement a recuperer.
- autorisation.md §6.6 : le mecanisme, ses deux conditions, et l'ecart
d'adresse laisse par l'ancien mode READ_ONLY de la federation.
Preuve : banniere SMTP lue depuis idm-01, RCPT TO accepte, execute-actions-email
declenche, MTA en starttls -> relay=mx.chezlepro.ca status=sent (250). Second
deploiement changed=0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ldappasswd` ne peut pas le changer : `cn=admin` n'est pas une entree de la base
mais le rootDN declare dans cn=config. Le mot de passe vit dans `olcRootPW` et se
modifie par un bind EXTERNAL. L'echec etait sans degat — l'ancien fonctionnait
toujours, verifie avant de continuer.
Keycloak stocke le mot de passe de LIAISON dans sa base et le masque : la
reconciliation d'hier couvrait l'URL, les DN et le mode, pas `bindCredential`.
Tourner le secret aurait coupe Keycloak de l'annuaire. Comme la valeur est
masquee, la reconciliation passe par une empreinte.
Verifie consommateur par consommateur : synchro LDAP de Keycloak (qui prouve la
liaison), carte LDAP de Postfix, Dovecot actif. Les trois rejouent a changed=0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ce compte est le moyen de se changer lui-meme : regenerer la voute d'abord
l'aurait rendu inapplicable — plus rien n'aurait pu s'authentifier pour poser la
nouvelle valeur. L'ordre est inverse : s'authentifier avec l'actuelle, poser la
nouvelle, verifier, PUIS ecrire la voute.
C'est une procedure, pas un redeploiement. Meme contrainte pour
`vault_openldap_admin`, qui reste a faire. Consigne au runbook §6.7, avec le
rappel qu'une verification n'est pas un message de succes.
Verifie : admin (voute) OK, groupe sysadmin et role grafana-admin intacts,
rejeu a changed=0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`realm-admin` (role du client `realm-management`) est attache au GROUPE sysadmin.
Administrer le realm ne passe plus par le compte local. Portee : CE realm, jamais
`master` — le compte de secours reste hors d'atteinte du groupe, et c'est le sens
meme d'un acces de secours (D-40).
Les roles de CLIENT sont un espace de noms distinct : la declaration gagne
`roles_client`. L'API attend l'UUID du client, pas son clientId — interroger par
le nom rendait une erreur, la verification echouait toujours et la tache se
declarait `changed` a chaque passage. Deux passages consecutifs a changed=0.
La boucle de changement de mot de passe : `pwdMustChange: TRUE` signifie « quand
un ADMINISTRATEUR pose un mot de passe, l'utilisateur doit le changer ». Keycloak
ecrit en tant qu'administrateur — chaque changement relaye etait vu comme une
reinitialisation. Incompatible par construction avec un IdP qui relaie.
La contrainte est deplacee la ou l'utilisateur la voit : pwdMustChange FALSE cote
annuaire, action `UPDATE_PASSWORD` posee par Keycloak. J'avais eprouve pwdReset
au niveau LDAP, ou il marche, sans parcourir le chemin complet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La premiere connexion du sysadmin echouait sur « invalid username or password ».
Ni le jeton ni le compte : `https://auth.<domaine>/` redirige vers `/admin/`, la
console du realm `master`, ou `sysadmin` n'existe pas — il vit dans le realm
applicatif.
Le runbook disait « se connecter a Keycloak » SANS donner d'URL, et l'URL
evidente est la mauvaise. Il nomme desormais les deux consoles :
/realms/<realm>/account/ ton compte sysadmin + jeton
/admin/ Keycloak lui-meme admin + vault_keycloak_admin
L'absence de `pwdFailureTime` cote LDAP etait le vrai indice : aucune tentative
n'atteignait l'annuaire, donc le probleme etait en amont de la validation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le sysadmin ne pouvait atteindre AUCUNE interface web de la flotte qu'il
administre : le pare-feu de l'hyperviseur n'autorisait le 443 de l'edge que
depuis `+t17-flotte`. Le reseau d'administration est dans `t17-admin`.
L'exploitant arrive par un TROISIEME chemin que rien ne declarait : ni `externe`
(Internet, affaire de la frontiere), ni `flotte` (le tenant). Le runbook de
reprise supposait pourtant qu'on ouvre Keycloak dans un navigateur.
`admin` devient un pair declarable — les reseaux de `nftables_admin_ssh`, deja
source unique de la garde anti-lockout. `serveur_nginx` le declare pour son 443.
L'edge SEUL : ouvrir les services en direct elargirait la surface pour rien.
Erreur de methode de ma part : mon premier test utilisait `/dev/tcp` et concluait
« atteignable ». Faux — la frontiere repond au SYN a la place de la cible. Je
l'avais consigne le matin meme.
`make ca-racine` / `make ca-empreinte` : l'hote de l'AC est derive du groupe
`serveur_step_ca`, et la sortie insiste sur la comparaison d'empreinte. La racine
est un certificat PUBLIC — hors voute, dans le magasin de confiance.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le module etait sur disque mais jamais charge (seul back_mdb l'etait). Depuis
OpenLDAP 2.5 son schema est INTEGRE au module : aucun .ldif a charger.
La contrainte mord, mesuree sur un compte fraichement amorce :
Insufficient access (50)
Operations are restricted to bind/unbind/abandon/StartTLS/modify password
Le sysadmin peut se connecter et RIEN d'autre que changer son mot de passe. Ce
que la doctrine promettait est garanti techniquement, plus seulement demande.
`pwdMustChange` est ce qui donne son effet a `pwdReset` : sans lui, marquer une
entree n'oblige a rien. La politique apporte aussi longueur minimale 12,
verrouillage apres 5 echecs, historique. `olcPPolicyUseLockout` reste FALSE :
annoncer « compte verrouille » renseignerait un attaquant sur son existence.
Le DN de la base est LU, pas suppose : olcDatabase={1}mdb est l'usage mais
l'index n'est pas garanti.
La detection du role d'amorcage s'est verifiee d'elle-meme : rejoue apres le
chargement, il annonce « Changement FORCE » la ou il disait l'inverse une heure
plus tot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cree uid=sysadmin et cn=sysadmin dans LDAP. Prouve sur idm-01 : le compte
s'authentifie, et le second passage ne touche a rien (changed=0, 7 taches
sautees) — idempotence par EXISTENCE, pas par conformite (D-67).
Il suit l'annuaire au lieu de se declarer au plan : il ecrit par `ldapi:///` et
doit tourner sur cet hote. Le declarer comme groupe obligerait chaque instance a
le poser sur le bon hote, et elles ne le nomment pas pareil (idm-01 ici,
id-ldap-01 chez Technolibre) — je l'ai d'abord pose sur infra-pki-01 par erreur.
Trois defauts trouves en le construisant :
1. `pwdReset` n'existe pas dans ce schema (overlay ppolicy non charge) :
l'entree entiere etait rejetee et `no_log` masquait la cause. J'avais suppose
un mecanisme sans verifier. Le role le DETECTE maintenant, et la doctrine ne
promet plus un changement force qui n'a pas lieu.
2. Le mot de passe aurait ete stocke EN CLAIR : `ldap_entry` ecrit userPassword
litteralement. Hache par `slappasswd -h {SSHA}` desormais.
3. `voute.py` ne scannait que roles/<groupe> : un role applique par un playbook
sans etre un groupe echappait au recensement, ce que D-20 interdit. Il suit
maintenant les listes `roles:` des playbooks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'authentification etait resolue et gardee ; l'autorisation n'existait nulle
part. `ou=groups` est cree depuis le debut et AUCUN des 29 roles ne le lit ;
`ou=people` est vide.
D-67 contredit DELIBEREMENT la doctrine du depot : partout ailleurs un ecart est
un defaut a corriger, ici il est legitime — c'est le sysadmin qui travaille.
Set-OPS cree UN acces puis se retire. Idempotence par EXISTENCE, pas par
conformite : compte present, aucune action quel que soit son etat. Reconcilier
effacerait le compte cree la veille pour un nouvel employe.
D-65 : les groupes LDAP portent l'autorisation, Keycloak les projette. Dovecot
et Postfix ne savent pas lire un role Keycloak — un seul endroit a administrer.
D-66 : un service nomme un groupe, jamais une personne : revoquer quelqu'un ne
demande pas un deploiement.
Le §6 est un runbook de reprise. Il dit aussi ce qu'il faut regenerer pour que
la livraison soit un vrai transfert : les comptes de secours ont ete generes
pendant le deploiement, et leur auteur y a eu acces.
Sans registre de personnes, aucune donnee personnelle n'entre dans git.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'authentification etait resolue et gardee ; l'autorisation n'existait nulle
part. `ou=groups` est cree depuis le debut et AUCUN des 29 roles ne le lit —
toute personne authentifiee obtient le defaut du service.
D-65 : les groupes LDAP portent l'autorisation, Keycloak les projette.
L'argument est mecanique : Dovecot et Postfix ne savent pas lire un role
Keycloak. L'y loger rendrait la moitie courriel aveugle et imposerait deux
modeles de permissions.
D-66 : un service nomme un GROUPE, jamais une personne. Un depart devient une
ligne au plan, sans toucher un service.
Le vocabulaire d'habilitation reste celui du service : une echelle commune
devrait etre traduite partout, et la traduction est ou l'habilitation se perd.
Rien n'est construit : registre, role, meta/acces.yml et P31 restent a ecrire.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le generateur emettait `{protocole} dport {port}` pour tout flux. L'ICMP a un
type et un code, pas un port : `icmp dport frag-needed` produit un jeu que `nft`
rejette, et un jeu rejete ne se charge PAS — l'hote perd sa barriere au lieu
d'en gagner une.
Le defaut touchait les 14 hotes. La seconde barriere de D-31 n'avait jamais pu
demarrer nulle part ; personne ne l'avait vu parce qu'aucune VM tenant n'avait
encore ete deployee.
`_selecteur_nft()` traduit : `icmp frag-needed` devient
`icmp type destination-unreachable icmp code frag-needed`. Un code inconnu est
refuse a la generation, avec le nom du role fautif. La garde tourne aussi a la
verification, pour attraper un flux declare mais pas encore porte.
Mesure : nftables actif sur infra-pki-01 et infra-dns-01, 16 et 17 regles, dont
la garde anti-lockout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Une VM ne peut pas s'installer sans resoudre des noms. PowerDNS repond
UNIQUEMENT pour la zone souveraine et ne recurse pour personne : il manquait un
resolveur recursif. `client_unbound` est l'outil ecrit pour ca — il rejoint
`client_journal` et `client_metrique` parmi les integrations universelles.
`infra-dns-01` est exempte (PowerDNS occupe son port 53), et
`serveur_powerdns_listen_addresses` passe de 0.0.0.0 a l'adresse de l'hote pour
laisser 127.0.0.1:53 libre. Mais l'appartenance au groupe est AUSSI ce qui ouvre
le port 53 a la frontiere : en exemptant la machine, je lui retirais le droit de
resoudre. `serveur_powerdns` declare donc son propre flux sortant — il ne
recurse pour personne, mais doit resoudre pour lui-meme.
Nouvel intrant `dns_amorcage`, derive jusqu'a `make creer-vm`. Cloud-init
l'ecrit bien mais sans effet : `dns-nameservers` exige `resolvconf`, absent du
gabarit, et installer resolvconf demande apt, qui demande la resolution.
`serveur_debian` pose donc le resolveur en pre_tasks, avant le premier apt, avec
une garde qui respecte la bascule ulterieure de client_unbound.
Defaut corrige en chemin : `_intrants_communs()` lisait `instance/` en dur ; le
chemin derive maintenant de l'inventaire recu, et un test le prouve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le devis enseignait « politique d'entree DROP au datacenter ». Or policy_in y
est la politique par defaut de TOUTE VM dont le pare-feu s'active — 37 machines
heritees sans regles, sur ce cluster. `policy_in` existe aussi par VM : le devis
et l'applicateur le posent la. Meme isolation, sans falaise, et l'applicateur
n'a plus a refuser une partie de son devis.
Trois verrous, pas un : datacenter enable=1, `enable` de la VM (defaut 0), et
`firewall=1` sur la carte. C'est le verrou du milieu que j'avais manque en
annoncant que huit VM en production tomberaient.
`enable=1` au datacenter bascule et verifie : pve-firewall running, 12 chaines
cadres, AUCUNE chaine par VM, 0 regle visant roxanne, 15 VM toujours en marche,
hyperviseurs et frontiere joignables, sortie tenant 2/2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deux obstacles, aucun n'etait celui qu'on croyait.
Proxmox n'installe AUCUN defaut dans le VRF du tenant : `default-originate`
annonce une route aux autres noeuds, il n'en pose pas chez lui. Les deux zones
etaient dans cet etat. La sortie vient d'une strophe frr.conf.local, que Proxmox
fusionne a chaque regeneration (verifie : survit a `pvesh set /cluster/sdn` et a
un redemarrage de FRR).
`nexthop-vrf default` emprunte UNE adresse au lieu d'importer la table
principale : la route par defaut des hyperviseurs ne gouverne pas la sortie des
tenants. `import vrf default` l'aurait fait contourner la frontiere et aurait
fuite le transport VXLAN, la gestion et les VLAN herites dans le VRF.
Le NAT sortant en mode automatique ne couvre que les reseaux directement
attaches ; un supernet joint par route statique en sort en silence. L'etat
montrait `nat_addr` absent : le filtre passait, la traduction manquait.
`devis_opnsense` emet le NAT (section 2bis), le reconciliateur l'applique et le
retire, et P24 refuse tout supernet route mais non traduit.
Mesure : tenant -> frontiere 3/3, -> passerelle FAI 3/3, -> Internet 2/2 pour
les deux tenants.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le devis rangeait tout flux entrant sur le WAN, en supposant que
l'administration revenait par l'adresse publique. Vrai pour 192.168.255.0/24,
faux depuis que c'est 10.0.0.0/24 — directement attache sur `lan`. Les deux
regles SSH etaient mortes deux fois : mauvaise interface, et « Block private
networks » les aurait filtrees. Le devis conseillait meme de decocher ce filtre
sur le WAN, ce qui aurait affaibli l'interface publique pour rien.
`reseaux_locaux_frontiere()` derive de l'underlay les sous-reseaux ou la
frontiere porte une adresse, hors transit. Un alias par interface : Technolibre
a les deux cotes, Chezlepro seulement la gestion. Sans underlay, tout retombe
sur le WAN — comportement inchange.
P24 confronte desormais chaque regle d'administration a l'attachement de sa
source, et refuse l'ancien comportement.
Nouvel intrant `opnsense_if_gestion`, au catalogue du GUI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`nftables_admin_ssh` est la source unique du « d'où administre-t-on » : il
alimente les alias de la frontière, le devis du commutateur et le jeu nftables
de chaque VM. Chezlepro déclarait encore l'ancien 192.168.255.0/24.
`make flux` échouait par ailleurs sur un tri mêlant ports numériques et
symboliques — défaut latent réveillé par les deux flux ICMP frag-needed, qui
sont dans les deux sens. `_cle_port()` rend la clé homogène.
Aperçus régénérés : les 14 hôtes actifs de Chezlepro portent 10.0.0.0/24.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les deux commutateurs deviennent bifrost-3 et bifrost-4, avec 10.0.0.3 et
10.0.0.4. Tout l'ensemble de bordure porte un seul nom, et son numéro est son
adresse : bifrost-1 = .1, bifrost-4 = .4. Plus de table de correspondance.
D-12 disait « bifrost aux frontières, sleipnir à la fabric » — le nom portait le
type de la machine. C'est le champ `role` qui le fait, et lui seul pilote le
devis : aucune logique ne dépendait du nom, seulement des données et un
commentaire. Le devis a suivi seul, jusqu'aux marqueurs de ports.
Inconvénient assumé : bifrost-3 ne dit plus « commutateur », il faut lire `role`.
La partie B du devis s'en charge à l'affichage.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le pont n'était pas seulement non portable, il était faux. proxmox_clone_pont
faisait naître les VM sur vmbr1 avec une étiquette VLAN — l'ancien monde. En SDN
une VM appartient à son VNet ; c'est ce qu'il a fallu corriger à la main sur
infra-pki-01, et les treize suivantes auraient suivi.
deriver_nomenclature() expose désormais la zone de sécurité, instancier en dérive
proxmox_pont et une étiquette VIDE — le VNet porte déjà le tag, en poser un
second donnerait un double étiquetage. La chaîne va jusqu'à make creer-vm :
SETOPS_PONT='t11appl', SETOPS_VLAN=''.
Trois pièges. Un doublon dans le Makefile passait PONT_PROXMOX deux fois dans la
même cible, la seconde vide aurait écrasé la valeur dérivée. Un repli naïf sur
proxmox_vlan aurait fait revenir l'étiquette en SDN : le repli ne s'applique que
si la clé est ABSENTE, jamais si elle est présente et vide. Et le test unitaire
est tombé, à raison — il couvre maintenant cette distinction.
D-55 : le dépôt réseau porte le contrat entre l'Alliance et ses hébergeurs, et
abstrait le matériel en encapsulant chaque tenant dans sa zone EVPN. Mesuré : un
tenant est à deux valeurs de la portabilité complète (noeud, stockage).
D-57 : l'interface sysadmin d'un hyperviseur (vmbr0, 10.0.0.41/.43/.47) n'a pas
de route par défaut ; celle-ci vit sur vlan40, vers la frontière. On n'atteint
l'administration que depuis son propre domaine de diffusion. Ça tranche la
question de la sortie des nœuds laissée ouverte ce matin — option A, mais sur une
interface dédiée, ce qui lève l'objection qui la bloquait.
D-58 : un hôte déclare par quelle interface (`via`) chaque réseau lui arrive ; le
devis en dérive un port par interface et son type — trunk 11,40 sur bond3, accès
VLAN 10 sur vmbr0. Sans ça, ajouter le VLAN 10 le remettait sur le trunk du
transport, soit le domaine qu'on venait d'en sortir.
D-59 : un VLAN qui ne porte que des adresses d'hôte n'a pas besoin de pont.
Régression créée puis corrigée : le modèle public, qui ne déclare aucun
hyperviseur, n'émettait plus rien pour ce port. Il émet maintenant tout
l'underlay en disant que c'est un repli.
30 preuves OK, 4 tests unitaires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il valait partout ; il ne vaut que dans l'adressage dérivé des tenants, où
passerelle_de(index, zone) produit le même .1 dans les treize sous-réseaux d'un
tenant. C'est une propriété de la dérivation, pas une loi universelle.
Dans l'underlay il produisait deux effets pervers. Un seuil arbitraire : les
sous-réseaux plus étroits qu'un /24 étaient exemptés, donc élargir un /29
changeait la validité du fichier sans que rien d'autre bouge. Et une couture
entre propriétaires : l'octet attendu venait de la nomenclature d'un tenant,
appliquée à la fabric de l'hébergeur — la validité de l'underlay aurait dépendu
du tenant actif.
Ce qui reste est plus fort et suffit (D-52) : une passerelle doit être l'adresse
d'un hôte déclaré sur ce réseau. Elle attrape les passerelles fantômes, ce que le
comptage d'octets ne faisait pas. Vérifié : la garde mord toujours.
À noter, parce que l'ordre était mauvais : le ré-adressage de l'OPNsense en .1 a
été demandé au nom de cette règle, deux messages avant qu'elle soit recadrée. Pas
perdu — .1 est la position conventionnelle d'une passerelle — mais la portée
aurait dû être questionnée avant de faire changer une adresse en service.
D-53 : le réseau et l'underlay de l'hébergeur méritent leur propre dépôt.
underlay.yml décrit une infrastructure, le dépôt de tenant une organisation ; un
tenant peut déménager, une fabric non. Consigné, non fait.
D-54 : 10.0.0.0/24 est réservé à l'IPAM, la gestion des équipements et l'OOB,
accès sysadmin. Aucun hyperviseur, aucune VM, aucun trafic tenant. C'est la
raison d'être des VLAN 11 et 40.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le port du commutateur vers la frontière était figé sur le seul VLAN de transit.
Depuis que les bifrost ont une patte sur le VLAN de sortie tenant, ce port serait
resté muet : le devis aurait eu l'air juste et le trafic ne serait jamais arrivé.
Il dérive maintenant des rattachements déclarés des hôtes `role: frontiere`.
Contexte, côté underlay (dépôt de l'hébergeur) : le VTEP vivait sur le VLAN 10,
donc le transport tenant partageait son domaine de diffusion avec l'administration
des équipements. Plus grave, un nœud de sortie décapsule le trafic tenant et le
remet dans sa table principale — dont la route par défaut sort par vmbr0,
l'interface de gestion des nœuds. D'où deux VLAN dédiés, 11 (transport VXLAN) et
41 (trafic décapsulé). Le second n'a volontairement pas de passerelle : le
commutateur le transporte sans le router, et le devis n'émet donc pas
d'interface Vlan41.
Services de l'hébergeur — décision consignée, rien n'est construit :
Aucun équipement de l'hébergeur n'est dans un inventaire Ansible, et rien ne
sauvegarde leurs configurations. Ni hyperviseurs, ni commutateurs, ni frontière.
D-46 : un hébergeur porte trois catégories — son tenant (un client comme les
autres), ses opérations (supervision de la fabric, journaux, sauvegarde des
configs, DNS d'underlay), et le plan de contrôle (déjà dehors).
D-47 : les opérations vivent dans le dépôt de l'hébergeur, et leurs VM se
rattachent à un pont VLAN, jamais un VNet. Un service qui observe la fabric ne
peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison
de la panne.
D-48 : les hyperviseurs sont gérables par Ansible ; « hors flotte » ne vaut que
pour les commutateurs (aucun agent) et la frontière (API seulement).
Question laissée ouverte : l'index 0 réservé au tenant propre de l'hébergeur. Il
produit les VNI 1001-1006, que le parc hérité utilise déjà (1001 TechnoLibre
historique, 1003 KBR), et P21 déclencherait une fausse collision entre deux
dépôts d'hébergeurs.
Construction parallèle vérifiée : aucune collision entre les 38 VM héritées et
les VNI/VLAN projetés.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ajouter un tenant implique 1 zone EVPN + 6 VNets + 6 sous-réseaux sur le cluster.
Aucun générateur ne les produisait : dernière lacune dans un dépôt où tout dérive.
26 objets pour les deux tenants, VNI = 1000 + index×10 + zone, sous-réseau et
passerelle par les mêmes fonctions que l'inventaire.
Nommage, en deux temps. D'abord VRF0017 / v1174, alignés sur ce que le cluster
portait — réflexe inverse du bon : cette convention venait d'une création à la
main et ne disait pas de quel tenant il s'agissait. Forme retenue : t<index> pour
la zone, t<index><zone abrégée> pour le VNet (t17, t17serv). C'est le préfixe que
le pare-feu Proxmox utilisait déjà (t17-cli-metrique), donc un seul schéma dans
tout le dépôt. Abréviation = 4 premières lettres du libellé, accents retirés.
Pas de tiret entre index et zone, contrairement aux IPSets : t245-serv ferait 9
caractères, t245serv en fait 8 — la forme reste uniforme jusqu'au dernier index.
Contrainte cadrante : zones ET VNets sont limités à 8 caractères par Proxmox.
sdn-evpn.md annonçait chez17-services-infra (21) : il aurait été refusé à
l'application. P30 refuse tout dépassement et toute collision de nom, de VNI ou
de sous-réseau. Éprouvé aux bornes et par sabotage.
Vérification la plus forte : avant renommage, la dérivation reproduisait à
l'identique les deux zones créées à la main — nom, VNI de VRF, MTU, contrôleur.
voute.py saisir : le pendant de la génération. On génère un secret dont le dépôt
est la source, on saisit celui dont un tiers est la source — inventer une clé
d'API OPNsense donnerait une valeur refusée à la première requête, avec P18 au
vert sur une voûte inutilisable. Sans écho, double confirmation, rien sur la
ligne de commande.
Reste ouvert : aucun nœud de sortie déclaré. Le devis émet un marqueur, pas une
valeur plausible. Deux points à trancher — le nœud de sortie route selon sa
propre table (défaut actuel : 192.168.11.254, pas la frontière), et l'entrée
n'est pas redondante puisqu'elle dépend d'une route statique d'OPNsense vers un
seul nœud.
D-45 : l'affinité de VM attend Proxmox 9 (cluster en 8.4.19), tenue à la main.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Une règle qu'aucune garde ne vérifie finit par ne plus être vraie — c'est ce qui
était arrivé aux 28 lignes d'intégration recopiées. Chaque rôle serveur_* porte
un meta/authentification.yml, confronté à son code par P29.
web-sso 5, socle-identite 2 (keycloak/openldap : ils SONT la chaîne d'identité),
ldap-direct 2, interne-sans-auth 2, sans-auth-humaine 12.
La preuve refuse l'oubli ET le mensonge. Éprouvée par sabotage sur sept cas :
déclaration supprimée, portée inventée, secours retiré, posture de formulaire
retirée, raison retirée, ldap-direct mensonger, réglage retiré des defaults.
Les deux derniers passaient dans la première version :
- le mensonge passait à cause d'un commentaire. Je cherchais le mot « ldap » dans
le rôle, et serveur_grafana/defaults/main.yml contient « désactiver quelqu'un
dans LDAP » : de la prose validait une déclaration fausse. La preuve exige
maintenant un indice nommé — variable <rôle>_oidc / <rôle>_ldap, ou URI ldap://
- le réglage retiré passait parce que le gabarit citait encore la variable alors
que plus rien ne lui donnait de valeur. La preuve lit defaults/main.yml en YAML
et exige que la clé y soit définie, pas mentionnée.
Elle a aussi forcé une valeur : oauth2-proxy était déclaré « formulaire local
fermé » alors qu'il n'a aucun compte local. D'où formulaire_local: aucun, qui
distingue « il n'y en a jamais eu » de « il y en a un, il est fermé ».
Correction d'une note de la veille : Prometheus et Loki ne sont PAS exposés
publiquement (aucun expose au plan). Seuls six groupes le sont. Le risque est
intra-tenant, pas frontalier. Les deux lacunes sont comptées à chaque exécution,
pas masquées.
AFF-111, D-42. 29 preuves OK, ansible-lint (production) sur 375 fichiers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Directive : toute authentification web passe par Keycloak, LDAP est la source
unique des comptes, chaque service garde un accès de secours par sudo sur
l'hôte. Les trois sont indissociables — la chaîne service → Keycloak → LDAP est
en série, donc sans secours une panne exclut tout le monde, y compris pour
réparer. Portée : le web seulement ; IMAP/SMTP se lient à LDAP directement et
SSH est en clé seule.
Posture <rôle>_connexion_locale, false par défaut. Le compte local existe — il
ne peut pas dépendre de Keycloak — mais son formulaire n'est plus proposé au
repos : ouvert en permanence, il contourne la politique de mot de passe, le MFA
et surtout la révocation centrale.
Vérifié auprès de l'amont, puis par rendu réel des gabarits dans les deux
postures :
- Grafana GF_AUTH_DISABLE_LOGIN_FORM → ferme ;
- Forgejo ENABLE_INTERNAL_SIGNIN + ENABLE_BASIC_AUTHENTICATION → ferme, API
Basic comprise. N'existe que depuis la v10 (ticket amont 7476) ; le rôle
épingle 10.0.0 et un assert refuse la fermeture en deçà, car le réglage serait
ignoré sans erreur ;
- Nextcloud hide_login_form → MASQUE seulement : ?direct=1 reste le chemin de
secours documenté par l'amont. Écrit comme tel, sans prétendre à l'équivalence.
Défaut attrapé par le rendu : la condition Forgejo sans `| bool` n'émettait rien
dans aucune posture — une valeur en chaîne est vraie au sens Jinja, la connexion
locale serait restée ouverte en silence.
docs/authentification.md, décisions D-38 à D-41. ansible-lint (production) sans
échec, 28 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reconnaissance en lecture seule de l'API du cluster, avec le jeton de la voûte.
Trois valeurs devinées étaient fausses, et deux défauts bloquants sont apparus.
Corrigé d'après le cluster
- stockages : truenas-dbsql manquait ; le catalogue ne garde que ceux qui
portent `images` (PBS, cephFS, local et truenas iSCSI n'accueillent pas de
disque de VM) ;
- ponts : vmbr0 avait été omis, et l'uniformité sur les trois nœuds n'avait pas
été vérifiée — un pont partiel empêche la VM de démarrer sur certains nœuds.
Pools
Chezlepro-17 et Technolibre-11 créés, dérivés comme le reste. Les pools
Env.Tenant antérieurs (Prod.Chezlepro, Lab.KBR...) sont l'ancien monde : on n'y
touche pas et on n'y verse pas la flotte générée. Diff réel : 2 pools ajoutés,
0 retiré, 1 VM sur 38 déplacée — infra-pki-01, qui n'avait aucun pool.
Défaut bloquant : make creer-vm aurait échoué en 401
proxmoxer recompose `utilisateur!nom` à partir d'api_user et d'api_token_id. La
voûte stocke la forme complète, que les playbooks passaient telle quelle, d'où
un 401 muet — alors que le même jeton fonctionne en curl. Mesuré des deux côtés
avec un module en lecture seule : forme complète = 401, forme courte = OK.
Normalisation par split('!') | last, qui accepte les deux écritures.
Reliquat proxmox.vault.yml supprimé
Toléré « en compatibilité », il restait le seul porteur du jeton chez
Technolibre — et comme *.vault.yml est gitignoré, ce jeton ne voyageait avec
aucun dépôt : une voûte unique (D-19) qui ne l'était pas. Migration faite en
mémoire, avec relecture et aller-retour de chiffrement vérifiés avant écriture ;
fichier supprimé, listes de chargement des playbooks nettoyées, validé par un
appel API réel ne chargeant que all/vault.yml.
La garde qui manquait
voute.py verifier ne comparait que le gabarit — il disait « complet » pendant
qu'un secret vivait ailleurs. Il contrôle maintenant aussi la voûte réelle quand
ANSIBLE_VAULT_PASSWORD_FILE la rend déchiffrable : noms de clés seulement,
jamais de valeur, et vérification sautée sans mot de passe.
Elle a trouvé un second trou dès son premier passage : la voûte réelle de
Technolibre n'a ni vault_nextcloud_admin ni vault_nextcloud_oidc, que le plan
exige. Non corrigé — générer ces secrets est une décision, et celui d'OIDC doit
correspondre à ce que Keycloak connaîtra.
27 preuves OK. --syntax-check et ansible-lint (production) sur les playbooks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Onze des quatorze serveurs portent le même nom court chez Chezlepro et chez
Technolibre. Vérifié un par un, ce n'est pas un problème technique : tout le
reste dérive du seed et diverge (10.27.19.21 contre 10.21.19.21, VMID 117402101
contre 111402101, VLAN 1174 contre 1114, deux domaines internes), et rien n'est
indexé sur le nom court — les opérations Proxmox portent toutes un vmid, les
certificats un FQDN, et client_backup_repo vise backup-01.{{ domaine_interne }}.
Le coût est humain : la console Proxmox affiche le nom, et deux infra-pki-01 y
sont indiscernables à l'œil. Le VMID porte le tenant, encore faut-il connaître le
codage.
Un pool par tenant, dérivé du dossier d'instance et de l'index — déjà unique par
P21, donc aucun registre de plus : Chezlepro-17, Technolibre-11.
make devis-proxmox-pools rattrape la flotte existante (création du pool, puis
affectation des VM actives). Les VM créées ensuite entrent d'elles-mêmes :
make creer-vm dérive le pool par la même fonction et le passe à la création. Le
playbook crée le pool au préalable — proxmox_kvm échoue sur un pool inconnu, et
l'API ne sait pas changer le pool d'une VM existante ; c'est aussi pourquoi le
rattrapage passe par les membres.
P28 garde deux collisions : même nom de pool entre tenants, et surtout même VMID
— une machine appartenant à deux tenants serait pire qu'une homonymie.
Rien n'est renommé : les homonymes sont la preuve que la nomenclature est un vrai
gabarit. Le devis ne lit pas le cluster, il dit l'état cible et non l'écart.
27 preuves OK, 0 échec. --syntax-check du playbook de clonage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Je sautais le flux entier dès qu'un de ses pairs valait `externe`. Or le SSH
du socle est déclaré `[flotte, externe]` : la moitié `externe` relève de la
frontière, mais la moitié `flotte` — le SSH entre hôtes, celui d'Ansible —
était perdue. Sous une politique DROP, plus aucun hôte n'aurait été joignable
en SSH depuis l'intérieur. Même piège pour le SMTP interne de Postfix,
déclaré `[externe, client_smtp]`.
`externe` est sauté pair par pair, jamais le flux entier. 36 groupes,
56 règles.
Ajouté : la liste des rôles sans règle entrante, avec leur motif. Onze rôles
sont injoignables sous DROP, et c'est voulu dans les onze cas — boucle locale
pour Prometheus, Redis, rspamd, Icinga et Unbound ; frontière seule pour
nginx ; aucun service pour `serveur_durci` et les clients. Un douzième motif
existe, marqué d'un avertissement : « flux entrants déclarés mais aucune
source résolue ici » — celui-là serait un vrai trou.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un flux dont le pair nomme quatre rôles donne maintenant quatre règles,
chacune renvoyant à l'IPSet de son rôle. 52 règles, toutes par IPSet, zéro
littérale.
Le gain n'est pas cosmétique : une règle porte qui elle autorise.
`-source +t17-srv-keycloak` se lit ; une liste de quatre adresses demande de
retrouver à qui chacune appartient.
La raison appartient au flux, pas à chacune de ses règles : elle est écrite
une fois au-dessus du paquet qu'elle explique plutôt que répétée quatre fois.
Vérifié : aucun renvoi orphelin, aucun IPSet inutilisé.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>