Commit graph

345 commits

Author SHA1 Message Date
bebdb84212 cles : sortir du poste ce qui n existe qu au poste
Some checks are pending
verifier / verifier (push) Waiting to run
Le code est replique trois fois (eregion, forge du site, patient 0) et les
voutes chiffrees y sont aussi — le coffre est solide. Les CLES qui l ouvrent
vivaient dans neuf fichiers, 1644 octets, sans copie ailleurs.

Poste seul : les mots de passe restic restent lisibles sur les machines vivantes,
donc recuperable mais douloureux. Poste + une machine : l etat de cette machine
devient illisible. Poste + site : terminal.

make cles-recenser montre ce qui sortirait sans rien ecrire — nom, taille,
empreinte, JAMAIS le contenu. make cles-exporter chiffre en AES256 puis
REDECHIFFRE ce qu il vient d ecrire et compare les empreintes une a une : une
sauvegarde de cles qu on n a pas rouverte n est pas une sauvegarde.

A lancer par l exploitant lui-meme : gpg demande une phrase de passe, elle ne
doit passer ni par un journal ni par le contexte d un assistant.

Trois refus, eprouves en les faisant echouer : destination dans l infrastructure
(un coffre dont la cle est dedans), archive existante (elle est peut-etre la
seule), archive illisible (supprimee). Le premier essai du premier refus etait
faux — le shell developpait HOME avant que je le remplace, l instrument mesurait
ailleurs que la cible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-05 09:42:01 -04:00
42becd0c02 paquets tiers : passer par le cache du controleur, plus par Internet
Some checks are pending
verifier / verifier (push) Waiting to run
Trois depots tiers etaient en HTTPS, et client_artefacts pose
Acquire::https::Proxy DIRECT — sans quoi le cache du site refuse les tunnels et
aucun depot tiers n est joignable. La ligne est juste ; sa consequence ne l avait
pas ete vue : ces trois depots CONTOURNENT le cache, et chaque VM neuve allait
les chercher sur Internet a sa naissance.

step-cli et alloy sont poses par des integrations UNIVERSELLES. Sans lien, une
machine neuve n obtenait ni son client d autorite ni ses metriques — elle n
entrait dans aucun flux chiffre. Dernier obstacle a une reconstruction hors
ligne, et il tenait dans un mot.

make cacher-paquets tire les 21 deb aux versions epinglees, empreinte SHA256
verifiee depuis l index du depot. Le role partage paquets_tiers les depose et les
installe EN UN SEUL appel a apt — il sait resoudre un ensemble de fichiers locaux
qui se dependent, la ou paquet par paquet echouerait sur l ordre (Icinga en
apporte dix-sept). Six roles branches ; chacun retombe sur le depot distant pour
ce que le cache n a PAS fourni, et rien d autre.

Eprouve pour de vrai : packages.smallstep.com renvoye vers 127.0.0.1, step-cli
desinstalle, cache local efface. Le role rejoue installe 0.30.6-1 depuis le cache
et la machine obtient ses certificats. Zero echec.

Deux defauts de mon propre outil, trouves en le construisant : Smallstep sert son
index NON COMPRESSE (404 sur .gz) donc step-cli n etait jamais mis en cache ; et
un depot injoignable faisait continue AVANT d incrementer le total, si bien que
le script rapportait 20 sur 20 alors qu il en manquait un. Une garde qui ne peut
pas echouer ne garde rien — troisieme fois cette semaine. Corrige puis eprouve en
le faisant echouer.

Reste hors ligne : NTP externe, et l expedition des alertes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-05 08:22:14 -04:00
cccb4e5b43 supervision : declarer a qui elle parle
Some checks are pending
verifier / verifier (push) Waiting to run
Icinga livre son exemple avec root@localhost — une adresse que personne ne lit.
Une supervision qui voit tout et n en parle a personne a le meme effet qu aucune
supervision, en plus couteux : elle rassure.

serveur_icinga_destinataire cree un utilisateur dans le groupe auquel les
notifications sont deja assignees (Icinga refuse deux objets de meme nom, on ne
peut pas redefinir l exemple ; on en ajoute un autre, l exemple reste muet).

Vide, aucun destinataire n est pose ET LE DEPLOIEMENT LE DIT — plutot que de
laisser croire qu une alerte partira.

Eprouve : message expedie par le relais du site, status=sent (250 2.0.0 Ok),
livre directement a mx.chezlepro.ca sans intermediaire ni dependance envers un
locataire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 17:48:19 -04:00
4e12c3a802 supervision du site, et la panne qui dormait dans un mot
Some checks are pending
verifier / verifier (push) Waiting to run
Le site a son temoin : site-mon-01 (VLAN 36) porte PostgreSQL, Icinga et un
relais. Le depot lui rapporte, et son premier verdict fut un vrai defaut —
site-mon-01 n avait jamais depose son propre etat. Les trois sont au vert.

serveur_backup_verification_locale repasse a true sur le depot : le drapeau ne
dit plus  on renonce  mais  verifie ce que tu peux ouvrir . La liste des noeuds
attendus derive de l inventaire ou tourne le role, donc du site seul.

serveur_postfix gagne un mode relais : un site n heberge aucune boite, il
expedie. Directement par sa frontiere — emprunter le MTA d un locataire ferait
dependre l hebergeur d un ecosysteme qu il peut outvivre.

LA PANNE DE FOND : le modele de routes OPNsense lit enabled ; on lui envoyait
disabled=0, un champ ignore. enabled restait a son defaut, ETEINT. Chaque route
creee par Set-OPS depuis l origine l etait desactivee — invisible, parce qu une
route eteinte EXISTE dans le modele et compte  posee . Le rechargement lance
pour activer la nouvelle patte a fait reprendre au noyau sa table depuis le
modele : 14 routes sur 15 disparues, 11 machines de Chezlepro injoignables le
lendemain de sa reconstruction, et le devis toujours vert.

Trois corrections empilees : la cause (enabled), la cecite (le plan lit la table
du NOYAU et signale absent ou eteint), l inaction (la reconfiguration ne se
declenchait que sur un changement du modele).

Aussi : un role du site peut enfin en appeler un autre a travers la frontiere —
le devis traitait tout pair nomme comme  l exterieur . Et nftables_admin_ssh se
DERIVE de la carte : recopie a la main, il retardait d une zone, et m a
verrouille dehors de la machine que je venais de creer.

Reste ouvert : le destinataire des alertes est encore root@localhost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 17:34:01 -04:00
cf9abe74b6 sauvegarde : le site protege enfin son propre etat
Some checks are pending
verifier / verifier (push) Waiting to run
L hebergeur protegeait l etat de tous ses locataires et pas le sien. Deux choses
qu il detient et que personne ne peut regenerer : la racine de son AC, et la
forge du genome. Le reste est reconstructible par le code.

Preuve faite, pas annoncee : sauvegarde reelle puis restic check et restitution.
13 fichiers pour l AC (root_ca_key et intermediate_ca_key compris), 835 pour la
forge.

Le site depose avec SON identite, sur le compte restic que serveur_backup lui
cree, separe des comptes des locataires par les memes permissions. La cible est
derivee du expose de l application qui porte serveur_backup_site : le nom du
service, pas une adresse — client_backup_repo la grave dans le chemin de chaque
instantane.

P36 ne lisait que le plan de l instance montee, donc jamais celui du site — qui
detient pourtant le plus. Elle lit desormais les deux. Verifiee en la faisant
echouer : integration retiree, la preuve tire.

Reste ouvert : personne ne verifie les sauvegardes du site. Le depot tourne avec
verification_locale a false (pose pour les locataires, dont il ne peut pas ouvrir
les depots) et le site n a pas de supervision. La sauvegarde existe et se
restaure ; c est son SILENCE qui n alerte pas encore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 11:21:52 -04:00
53c5f41a1a durcissement : le site recoit enfin serveur_durci
Some checks are pending
verifier / verifier (push) Waiting to run
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de
sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban,
ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE
affirmait pourtant le contraire, ce qui rendait l ecart invisible.

Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable :
- aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire
  dynamique) -> commande nftables-site, branchee sur make flux
- le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour
  un inventaire dynamique) -> host_var explicite
- aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion
  arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le
  generateur refuse desormais de produire des regles sans cet intrant.

Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de
site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee —
juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on
pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la
veille.

MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que
dans son play, et le passage de serveur_durci le remettait au defaut sans rien
dire. Un reglage qui depend de l ordre des plays revient en arriere.

Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot
(2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
0854b2a93c reconstruction : quatre defauts que seule une flotte rasee pouvait montrer
Some checks are pending
verifier / verifier (push) Waiting to run
Chezlepro detruite (15 VM, disques compris) et refaite depuis le gabarit
minimal. 15/15 hotes, 0 echec ; make valider passe, test de restitution
compris. Aucun defaut ne venait de la flotte ni du gabarit.

1. Un avertissement n est pas un echec. Proxmox rend WARNINGS: n pour une
   tache ABOUTIE ; la garde n acceptait que OK et declarait perdues cinq VM
   clonees a 100 pourcent. Le message parlait d etat stopped — celui de la
   TACHE, pas de la VM.

2. client_artefacts se contredisait : son commentaire disait de degrader, son
   code arretait. L autorite monte en premier, donc avant le cache du
   locataire : aucun ordre ne pouvait satisfaire la garde.

3. harden-below-nxdomain etendait le NXDOMAIN signe de la racine pour le TLD
   internal a toute la zone du site, sans jamais interroger l autoritatif.
   Declencheur : toute question sur un nom absent sous internal, y compris la
   zone d un autre locataire. Le cache contenait la bonne reponse ET un
   message negatif ; c est le negatif qui etait servi.
   aggressive-nsec: no avait semble marcher — c est le redemarrage qui vidait
   le cache, pas le reglage.

4. Un locataire doit savoir a qui demander la zone de son hebergeur, sans quoi
   il ne peut plus nommer son depot de sauvegarde. La derivation prenait
   dns_amorcage pour le resolveur du site : faux chez Technolibre, dont
   l amorcage est 9.9.9.9. P03 l a attrape avant tout deploiement.

Au passage : instancier tentait encore le mot de passe unique d avant la
separation des voutes ; comparer echouait en exit 4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 09:59:15 -04:00
3ea0c95492 sauvegarde : l etat d un locataire quitte enfin sa propre flotte
Some checks are pending
verifier / verifier (push) Waiting to run
Chezlepro rangeait ses instantanes sur une VM DE SA PROPRE FLOTTE. Raser
l ecosysteme pour le reconstruire, c etait raser le filet avec.

Le site a son depot ; les neuf detenteurs d etat y deposent ; une
restitution est sortie (annuaire LDAP lisible, hors flotte).

Isolation par compte Unix, pas par convention : home 0700, cle exclusive,
racine partagee a root en 0711 (traversable, non listable). Les deux refus
constates. Le site heberge du chiffre : il ne peut ni lire ni ouvrir, d ou
la verification deplacee chez le locataire qui detient la cle.

Quatre defauts reveles par ce deuxieme usage :
- la racine des depots ne peut etre le home de personne (StrictModes rendait
  Permission denied publickey pour un refus de CHEMIN)
- la racine nie le TLD internal, et harden-below-nxdomain etendait ce non a
  toute la zone sans jamais interroger l autoritatif : aucun locataire ne
  pouvait nommer un service du site
- le gabarit transporte des fichiers de durcissement perimes, et les machines
  du site ne recoivent jamais ssh_hardening
- MaxStartups compte les connexions non authentifiees : un depot de site en
  voit la somme de ses locataires

Constat non corrige : les machines du site ne sont pas durcies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-01 22:16:20 -04:00
4abf875e5f gabarit : q35 n est pas un reglage, c est la raison de la procedure manuelle
Some checks are pending
verifier / verifier (push) Waiting to run
CONSTAT DE L EXPLOITANT, PAYE EN ANOMALIES : convertir une machine deja installee
d `i440fx` a `q35` produit une serie de pannes dont chacune ressemble a autre chose
qu a sa cause. Ce n est pas une correction, c est une transplantation.

LE MECANISME, ECRIT POUR QU ON NE LE REDECOUVRE PAS : `i440fx` est un chipset PCI,
`q35` est PCIe. La topologie des bus change, donc les NOMS D INTERFACES
PREDICTIBLES changent avec le chemin PCI (enp0s3 -> enp1s0) et la machine perd le
reseau ; les chemins de disques bougent ; l ordre d enumeration suit.

C EST AUSSI POURQUOI SET-OPS N UTILISE PAS L IMAGE CLOUD OFFICIELLE DE DEBIAN :
`genericcloud` est livree configuree pour `i440fx`. Une machine nait `q35`, ou elle
ne le sera jamais proprement — et c est ce que l installation depuis l ISO garantit.

Ces deux lignes de la procedure n etaient qu une ligne de tableau. Elles portent
maintenant leur pourquoi, et le SITE les declare comme DONNEES (cle `gabarit`),
plus seulement comme prose.

`make gabarit-etat` compare le gabarit reel a ce que le site declare de lui.

ON VERIFIE LA SOURCE, PAS CHAQUE COPIE. Ma premiere version gardait le CLONAGE : la
propriete s herite, donc verifier chaque clone coute a chaque creation sans rien
dire de plus que verifier le gabarit une fois. Retiree.

TROIS FOIS J AI DEVINE LA FORME DE LA REPONSE AU LIEU DE LA REGARDER — regex_search
a groupe qui rend None, proxmox_vm_info sans `config: current` qui ne rend que
l etat. La garde a declare « ? » sur une VM parfaitement conforme : une garde qui
crie toujours est pire qu aucune, on apprend a l ignorer.

A la demande et non dans `make prouver` : ce controle exige le cluster, que le
harnais ne suppose pas joignable. Meme nature que genome-etat et underlay-plan.
Controle negatif verifie : declarer i440fx fait echouer, rc=1.

make verifier : vert. make prouver : CONFORME, 56 OK, 0 echec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-01 14:42:58 -04:00
9aef3614ab gabarit : refabrique, minimal, et puise aux ressources du SITE
Some checks are pending
verifier / verifier (push) Waiting to run
REFABRIQUE (VMID 9006, modeleSetOPS-minimal). Quatre roles au lieu de dix-sept :
qemu_guest_agent, cloud_init, sudo_ansible, ssh_baseline — des conditions
d existence, pas des choix d efficacite. Tout le reste vient du socle, et P56 refuse
qu un role retire ne soit repris par personne.

FABRIQUE CHEZ LE SITE, ET C EST LA DECISION QUI COMPTE. « Les ressources du SITE
font autorite pour tous ses artefacts ; elles servent les tenants jusqu a ce qu ils
s emancipent. »

Il se fabriquait DEHORS : `-i "<ip>,"` ne porte aucun group_vars, donc ni mandataire
ni resolveur. L ancien gabarit allait chercher ses paquets chez Debian et resolvait
chez l ancien LAN — 192.168.10.10, lu sur la VM 99998. Le site avait son cache et
son resolveur, et son propre artefact les ignorait.

Desormais la fabrication DERIVE ses ressources du plan du site (underlay --adresses)
et tourne sur le reseau du genome. PROUVE : dix requetes de 10.0.33.31 servies par
site-cache-01, resolveur pose a 10.0.34.11.

L IDENTITE DU GABARIT VIENT DU SITE, PLUS DU TENANT. `proxmox_clone_vmid_modele`
vivait dans les group_vars de l ecosysteme : deux tenants pouvaient cloner deux
gabarits differents sans que rien ne le dise, et un tenant decidait d un objet dont
descend chaque VM de chaque ecosysteme. Meme mouvement que l INDEX (2026-08-25) : le
site ALLOUE, le tenant RECOIT. Cle `gabarit` du plan du site, lue par
underlay --gabarit.

PIEGE FERME EN CHEMIN : creer-vm passait VMID_MODELE="$SETOPS_VMID_MODELE" a
cloner-vm, or inventory_host.py n emet PAS cette variable. Elle valait donc le VIDE,
et ce vide ECRASAIT la valeur derivee — le gabarit du tenant reprenait la main sans
bruit.

ET UN PIEGE DEJA DOCUMENTE, PAYE UNE TROISIEME FOIS : le chemin du controleur
contient une espace, et `lookup('pipe', ...)` le decoupait. Guillemets.

EPROUVE DE BOUT EN BOUT : VM clonee du gabarit minimal — nom, adresse, machine-id
neuf, cle d hote regeneree, agent invite actif ; puis socle applique dessus,
changed=5, 0 echec, auditd compris. VM d essai retiree.

make verifier : vert. make prouver : CONFORME, 56 OK, 0 echec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-01 13:46:31 -04:00
2f2323bd3e gabarit : minimal — il etait un cache du socle, et il perimait sans le dire
Some checks are pending
verifier / verifier (push) Waiting to run
Il portait DIX-SEPT roles : exactement ceux du socle et du durcissement, que le
deploiement rejoue a l identique. C etait donc un CACHE — et comme tout cache, il
perimait sans le dire.

MESURE : derniere recapture le 2026-08-09, et SIX de ses roles avaient change depuis
— common_packages, cloud_init, ssh_baseline, ssh_hardening, auditd,
nftables_baseline. Rien ne le signalait : le deploiement masquait la derive en
reappliquant tout, donc personne ne pouvait la voir. Aucune preuve du harnais ne
regardait sa fraicheur.

IL NE GARDE QUE CE QUI DOIT EXISTER AVANT QU ANSIBLE PUISSE AGIR :

    qemu_guest_agent  l agent repond AVANT SSH — P52 s en sert
    cloud_init        le seul chemin vers la premiere seconde
    sudo_ansible      la porte par ou tout entre
    ssh_baseline      le serveur SSH

Ce ne sont pas des choix d efficacite, ce sont des conditions d existence.

CE QUE CA COUTE, ET QUI EST COUVERT : une VM neuve n est plus durcie a la naissance.
Elle nait cependant DERRIERE LE PARE-FEU DE L HYPERVISEUR, policy_in=REJECT arme au
clonage — verifie sur obs-01. La fenetre d exposition est fermee par la fabric, pas
par le gabarit. Mon objection initiale tombait devant la mesure.

P56 GARDE LES DEUX MOITIES. Qu il ne REGROSSISSE pas : un role ajoute recree le
cache, donc la peremption invisible. Et que rien de retire ne soit PERDU : un role
absent du gabarit ET du socle disparaitrait de toutes les machines neuves, sans
erreur ni trace, et la panne arriverait des mois plus tard sur une machine qu on
croyait durcie. Verifie : 14 retires, 14 repris, zero orphelin. Deux controles
negatifs.

make verifier : vert. make prouver : CONFORME, 56 OK, 0 echec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-01 10:35:20 -04:00
fde0903ed8 flux + postfix : un port symbolique dans le pare-feu, un rechargement qui n applique rien
Le deploiement depuis ops-01 passe de 3 plays a 21 : douze machines sur quinze
deployees completement. Deux defauts restaient.

UN PORT SYMBOLIQUE ECRIT TEL QUEL.

    /etc/nftables.conf:36: Could not resolve service: Servname not supported
        ip saddr { ... } udp dport derive accept   # serveur_powerdns

`port: derive` dit que le port depend du deploiement. Le devis de la frontiere le
resout depuis le 2026-08-25 ; le generateur nftables ecrivait le mot, et nft
refusait TOUT le fichier. Il resout desormais par le plan et n emet rien quand il
ne peut pas, en le DISANT — un flux tu en silence est une porte qu on croit
ouverte. Verifie que ca ne ferme rien : infra-dns-01 garde son 53 par
serveur_resolveur, et l omission de PowerDNS est juste puisqu il ecoute en loopback
derriere lui.

LA VALIDATION NE POSE PAS LA MEME QUESTION QUE L EMISSION. Ma premiere garde
refusait tout port non numerique et a fait echouer P09, qui valide les roles HORS
instance — la ou derive est legitime. PORTS_SYMBOLIQUES nomme le vocabulaire : le
mot passe a la validation, jamais dans un fichier, et un mot inconnu reste refuse
des deux cotes.

RECHARGER N APPLIQUE PAS UN CHANGEMENT D ECOUTE, deuxieme fois.

    warning: to change inet_protocols, stop and start Postfix
    fatal: :::submission: Address family for hostname not supported

main.cf porte inet_protocols, que Postfix refuse de changer a chaud. Le master garde
all, tente d ouvrir :::submission en IPv6 et meurt — APRES avoir accepte une
configuration valide. postfix check ne dit rien parce que la configuration EST
valide : c est la transition qui ne l est pas. Meme famille que nginx. main.cf
notifie desormais le redemarrage.

Diagnostic corrige en chemin : j ai d abord accuse postfix@-.service, dont
l assertion echouait — c est moi qui l avais declenche par un demarrage manuel,
postfix.service le declare en Conflicts.

make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 13:00:54 -04:00
a5c9a88f00 resolveur du site : le troisieme service prete pendant la jeunesse d un tenant
Apres les paquets (serveur_cache_site) et le genome (serveur_forge_site), les NOMS.
Meme patron : un installateur, un marqueur.

CE QUE CA CORRIGE, mesure sur infra-pki-01, premiere machine a porter l autorite
de Chezlepro :

    DNS               BLOQUE      un tenant resout chez lui, sa requete ne sort pas
    443 sortant       OK          par adresse
    apt via le cache  OK          le mandataire resout a sa place

apt s en sortait ; TOUT LE RESTE ETAIT AVEUGLE AUX NOMS. Versionner la cle de
signature Smallstep a fait passer une tache et l echec s est deplace d un cran : le
depot lui-meme devait etre joint par son nom.

POURQUOI PAS L UNBOUND DE LA FRONTIERE : il tourne sur le boitier sans etre un
service gere, et le plan du site l avait deja ecrit une fois — un processus n est
pas un service. site-dns-01 est deploye par le moteur et verifie par le harnais.

OU VIT LA DERIVATION : dans site_inventaire.py, qui connait le plan du site ET le
registre des tenants. Le role tourne sur une machine qui n a aucune raison de lire
la carte de l hebergeur — la derivation appartient a qui detient les deux sources.

PIEGE DE FILTRE, ATTRAPE EN LE BRANCHANT : _supernets_voisins() EXCLUT l instance
montee (personne n est son propre voisin). Le site sert TOUS ceux qu il heberge — y
compris celui qu on deploie. Reutilise tel quel, il privait de resolution le seul
tenant en cours de montage. On reutilise donc la DECOUVERTE partagee sans son
filtre : deux recensements divergent, deux filtres non.

Le marqueur verifie deux choses : que le resolveur ECOUTE, et que sa liste
d autorisation ADMET reellement les tenants. Sans la seconde, la panne chez le
locataire ressemblerait a un DNS mort alors que c est une ACL.

make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 12:37:33 -04:00
bb63865a37 cles : le terrain etait inoccupable — la cle du tenant nait avec ses machines
Le deploiement lance depuis ops-01 s est arrete au premier geste, sur les quinze
machines a la fois : Permission denied (publickey). Les VM neuves n acceptaient que
la cle de l exploitant. Celle du runner EST declaree au plan, mais c est le SOCLE
qui la depose — et le socle doit etre applique par quelqu un qui peut deja entrer.
Boucle fermee : le tenant recevait un terrain qu il ne pouvait pas occuper.

DEUX CLES, DEUX PORTEES :

    la cle du SITE     -> sur le SEUL runner du tenant     l insemination
    la cle du TENANT   -> sur TOUTES ses machines          il va les configurer

POSER UNE CLE AU CLONAGE N EST PAS ENTRER CHEZ LE TENANT. C est un parametre de
creation, au meme titre que l adresse ou le disque : le site ecrit les conditions
de NAISSANCE, il n ouvre aucune session. Le site n obtient aucun acces sur ces
machines ; seul le runner du tenant en obtient un.

Forme tranchee par l exploitant : le site renseigne le seul runner, qui se charge
de toute sa flotte. Le plancher /etc/hosts suit le meme chemin — ops-01 a le sien
depuis son insemination et resout ses quinze voisines par leur nom.

LA REVOCATION EST HONOREE A LA NAISSANCE : une entree a etat absent n est pas
reposee. Sans cette lecture, une cle retiree de la flotte serait ressuscitee sur
chaque VM creee ensuite — panne lente, silencieuse, invisible au plan.

P55 garde les deux moities : la cle du site ne nait que sur un porteur de
serveur_ops_tenant, et aucune machine ne reste sans celle de son tenant. Une
frontiere tenue a une seule couche n est pas tenue. Deux controles negatifs.

make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 08:54:17 -04:00
5c59f19530 voutes : une cle absente n est pas une faute — elle est souvent la bonne reponse
Sur le runner d un tenant, la cle du SITE DOIT manquer : il porte la carte de la
fabric et ne doit jamais pouvoir l ouvrir. Ma premiere version sortait en erreur
des qu une cle manquait — elle presentait une SEPARATION REUSSIE comme un defaut,
et a fait echouer une chaine parfaitement saine.

Seule l absence de la cle de l INSTANCE MONTEE empeche la machine de travailler.
C est elle, et elle seule, qui decide du code de sortie ; le reste s affiche.

Constate sur ops-01 le jour de son armement :

    instance    OPS-Chezlepro     cle presente
    hebergeur   SITE-Chezlepro    sans cle

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 18:41:21 -04:00
732aff9c70 sdn : le puits avalait le plan d administration — une route, et le chemin existe
CE QUE LA MESURE A ETABLI, saut par saut. Le poste de l exploitant atteint la
frontiere, qui LAISSE PASSER (rule 31/0 match, pass out vlan040). Asgard RECOIT le
paquet sur vlan40. La VM ne le voit jamais. Et le noyau dit pourquoi :

    ip route get 10.17.19.41 from 10.0.31.11 iif vlan40  ->  dev vrf_t17
    ip route get 10.17.19.41 from 10.17.0.17 iif vlan40  ->  Invalid cross-device link

LA SOURCE EST LE DISCRIMINANT, pas l interface. Le chemin de RETOUR, vu du VRF :

    vers 10.0.31.11  ->  via 10.0.4.1 dev vlan40
    vers 10.17.0.17  ->  Invalid argument          <- le puits

LE PUITS A SES RAISONS et on n y touche pas : sans lui, une adresse non attribuee
du supernet sort par le defaut, revient par la frontiere dans la table PRINCIPALE
et repart vers le reseau de gestion (mesure du 2026-08-09). Le defaut n est pas
qu il existe, c est qu il est TROP LARGE : il couvre la bande basse ou D-77 place
justement l underlay d un site. Deux regles justes separement, contradictoires
ensemble.

LA CORRECTION EST DERIVEE, PAS ECRITE : pour chaque tenant, les reseaux
d administration qu il DECLARE (nftables_admin_ssh) et qui tombent dans son propre
supernet recoivent une route vers la frontiere, plus specifique que le puits. Ceux
qui vivent dehors n en ont pas besoin — la route par defaut les joint deja.

    vrf_t17   ip route 10.17.0.0/24    ...     l admin de Chezlepro
    vrf_t23   (rien)                           le sien est hors de son supernet
    vrf_t29   ip route 10.29.19.41/32  ...     l admin de patient 0 : son ops-01

CE QUE CA REPARE AU-DELA DE L ACCES : les regles administration -> tenant de la
frontiere etaient VRAIES et INAPPLICABLES a la fois. Elles correspondaient, elles
laissaient passer, et le paquet mourait un saut plus loin. Un devis vert sur un
chemin qui ne pouvait pas aboutir.

make verifier : vert. make prouver : CONFORME, 54 OK, 0 echec, 0 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 17:46:40 -04:00
90ea474745 inseminer : le geste sort de mes mains et entre dans le depot
Some checks failed
verifier / verifier (push) Has been cancelled
L insemination avait ete conduite A LA MAIN depuis le runner du site — hors du
depot, donc sans preuve. Elle a maintenant sa cible :

    make inseminer TENANT=OPS-Chezlepro

TENANT= PLUTOT QUE LE SYMLINK instance : le runner du SITE amorce PLUSIEURS
locataires ; pointer un lien global sur l un d eux le ferait se prendre pour ce
tenant. Il en NOMME un par commande. Ca borne aussi le couplage que creer-vm
imposait en silence — rien ne disait sur quels tenants ce lien pouvait pointer.

L HOTE SE DERIVE : celui qui porte serveur_ops_tenant. Meme critere que le flux
d insemination et que la cle SSH du runner — le meme mot borne les trois pouvoirs.

P54 GARDE LA LIGNE DE PARTAGE la ou elle glisserait sans bruit. Les deux couches
retenues sont les seules qui ne reclament aucun secret. Le jour ou l on en
ajouterait une, le deploiement echouerait chez le tenant sur une valeur vide, et ce
message ne dirait pas qu un POUVOIR a ete franchi. Controle negatif : ajouter
client_pki, la couche suivante, fait echouer la preuve.

UN GARDE-FOU EXISTANT A INTERCEPTE UNE INSEMINATION MAL DIRIGEE. Le make parent
exporte SETOPS_INVENTAIRE ; ma resolution en heritait et visait l inventaire d un
AUTRE ecosysteme. Le refus vient d inventory_rules, pas de la cible — exactement
l erreur qu un runner servant plusieurs locataires commettrait en silence.

make verifier : vert. make prouver : CONFORME, 54 OK, 0 echec, 0 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 17:14:37 -04:00
0595bf03c1 sonder : rapporter ce qui distingue, au lieu d un echec
Some checks are pending
verifier / verifier (push) Waiting to run
Trois faux diagnostics en une journee, tous dus a l instrument et aucun au
composant. curl et bash /dev/tcp ecrasent quatre causes incompatibles dans le meme
mot : ouvert, une POLITIQUE qui refuse, une machine ABSENTE, et la frontiere muette.

LE MEME CODE DIT DEUX CHOSES SELON LE DELAI, et c est la distinction qui a coute le
plus cher : EHOSTUNREACH immediat = pas de route ; le meme apres trois secondes =
il n y a pas de machine, c est l ARP qui renonce. Les confondre a fait appliquer un
pare-feu pour reparer un vide.  est pure, donc gardee par un test qui
exige que les deux ne se lisent jamais pareil.

LA GARDE ECHOUAIT DU COTE SILENCIEUX. L outil dit par quelle SOURCE le paquet part
et refuse de conclure sur une passerelle anycast — le piege qui m a fait declarer
muet un REJECT qui emettait bien ses RST. Ma premiere version rendait « pas
anycast » quand inventory_rules manquait, c est-a-dire sur un hyperviseur ou l on a
copie le seul fichier : precisement la ou le piege se produit. Une garde qui echoue
doit crier, pas se taire.

ET « NON CONCLUANT » EST UN RESULTAT : sur une source anycast et un silence, l outil
ne tranche pas, il dit quoi faire — compter les paquets du cote qui refuse, ou
sonder depuis une VM.

TCP seulement, et c est dit : UDP n a pas de poignee.

Valide contre le reel depuis ops-01 : trois cas sur quatre, le quatrieme attendant
deux machines vivantes dans un meme tenant.

make verifier : vert. make prouver : CONFORME, 53 OK, 0 echec, 0 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 17:02:20 -04:00
4beb7e0724 flux : l interne refuse a voix haute, la bordure se tait (P53)
Some checks are pending
verifier / verifier (push) Waiting to run
Decision de l exploitant : block vers l Internet, reject a l interieur, parce que
c est prudent. Ce n est pas le refus qui informe, c est CE QU IL FAIT AU SILENCE :
sous drop partout, un timeout voulait dire aucune machine, aucune route, ou une
politique. Quand la politique parle, il n en reste qu une.

    nftables par hote   policy drop + reject with icmpx type admin-prohibited
    pare-feu Proxmox    policy_in = REJECT  (POLITIQUE_VM, source unique)
    frontiere OPNsense  block  — INCHANGE, et c est la condition

admin-prohibited ET NON tcp reset : un RST est indiscernable d un port ferme sans
service. La chaine forward reste muette : elle porte le trafic qui TRAVERSE l hote,
et y repondre ferait parler cette machine au nom d une destination qui n est pas
elle.

POURQUOI C EST PRUDENT : l obscurite etait deja nulle a l interieur (chaque machine
porte un /etc/hosts qui liste ses voisines), et la bordure protege le reject —
rien d indeclare ne franchit le perimetre, donc il ne repond jamais a l Internet.
982 000 entrees par jour a la frontiere, dont 82 % un balayage VNC.

LA MESURE A CORRIGE LA MESURE, DEUX FOIS.

Ma preuve interdisait le litteral DROP et a fait echouer un code JUSTE : la
detection d une politique posee AU DATACENTER, qui est un garde-fou. Une preuve qui
interdit un mot au lieu de mesurer une propriete finit par accuser ce qu elle
devrait proteger.

Et l absence parlait deja : EHOSTUNREACH en 3,05 s pour une machine inexistante,
timeout a 6 s pour un refus de la frontiere. Mes deux erreurs de diagnostic ne
venaient pas du drop mais de ma SONDE — curl et bash /dev/tcp ecrasent les deux
dans un meme echec.

Applique : 6 VM en REJECT, 0 creee, 0 retiree. Rien ne se ferme.
Trois controles negatifs verifies.

make verifier : vert. make prouver : CONFORME, 53 OK, 0 echec, 0 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 16:43:22 -04:00
377462b141 voutes : une voute, une cle — separer avant de distribuer
Decision de l'exploitant : chaque runner est maitre de sa voute et en detient la
cle. C'est ce qui rend un runner autonome, donc ce qui rend l'emancipation
atteignable. Elle exigeait un prealable, mesure ce matin : UN SEUL mot de passe
ouvrait les SIX voutes de la flotte, celle de la fabric comprise.

DISTRIBUER AVANT DE SEPARER AURAIT ETE PIRE QUE LE STATU QUO : poser « la » cle
sur chaque runner rendait chaque runner capable d'ouvrir les autres. Compromettre
le plus petit locataire donnait les secrets de l'hebergeur.

Cinq cles, une par ecosysteme, sous ~/.config/setops-vault-<depot>. Verification
croisee apres rechiffrement : la diagonale, et rien qu'elle. L'ancienne cle
maitresse n'ouvre plus aucune des six.

UN SEUL MOT DE PASSE NE POUVAIT PLUS SUFFIRE, et pas pour la raison qu'on croit :
`cloner_vm_debian.yml` charge la voute du TENANT puis celle de l'UNDERLAY dans la
meme execution. `ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie
toutes — un seul export suffit pour les 28 appels a ansible-playbook, sans en
toucher un seul. La liste se derive dans scripts/voutes.py.

CE QUI BORNE LE POUVOIR N'EST PAS LA LISTE MAIS LA PRESENCE DES FICHIERS. Sur le
poste, toutes les cles sont la — c'est l'humain qui les detient toutes, et P03 lit
les inventaires de tous les freres. Sur un runner, une seule existe. Le code est
identique, le pouvoir ne l'est pas.

DEUX PREUVES ONT DIT CE QUI MANQUAIT. P03 est tombee des la separation : elle lit
les inventaires voisins, donc il lui faut leurs cles — c'est elle qui a etabli que
la liste devait couvrir le voisinage. P16 s'est mise a SAUTER : sa garde ne
connaissait que ANSIBLE_VAULT_PASSWORD_FILE. Une preuve sautee se lit trop
facilement comme une preuve passee.

COUT ASSUME : le chiffre et sa cle cohabiteront sur la meme machine des que les
runners recevront la leur. Le pari tient parce que le perimetre est borne.

Les six voutes sauvegardees avant rechiffrement. L'ancienne cle maitresse reste
sur le poste : la retirer est une decision, pas un nettoyage.

make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans
ANSIBLE_VAULT_PASSWORD_FILE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 14:23:33 -04:00
ef832d11d6 insemination : la cle d'amorcage, bornee au meme groupe que le flux
Le flux etait declare et applique ; il manquait l'IDENTITE. Le runner du SITE
atteignait la porte de ops-01 sans avoir de cle.

LE PIEGE COMPTE PLUS QUE LE CORRECTIF. La cle s'injecte au CLONAGE, et `creer-vm`
cree TOUTES les machines d'un tenant. L'injecter a chaque clonage aurait donne a
l'hebergeur un acces SSH a la flotte entiere de chaque locataire, en silence — ca
aurait defait a la couche IDENTITE ce que le pare-feu venait de borner a la couche
RESEAU. Le meme critere gouverne donc les deux : porter `serveur_ops_tenant`.
`SETOPS_CLES_AMORCAGE` est vide partout ailleurs. Elle S'AJOUTE a celle de
l'exploitant, elle ne la remplace pas : c'est l'humain qui arme.

La cle vient du PLAN DU SITE, pas du disque local : materialiser depuis le poste
et depuis le runner doit produire la meme VM.

DEUX COUCHES MANGEAIENT LES ESPACES. Proxmox rendait `SSH public key validation
error` — message muet sur la cause. Isole par un CONTROLE (rejouer sans la cle :
la tache passe), puis par la mesure de ce qui arrivait au module :

    "sshkeys": "ssh-ed25519"     <- le premier mot, rien d'autre

J'ai accuse `make` d'abord ; c'etait `ansible-playbook -e cle=valeur`, qui decoupe
AU SHLEX. D'ou l'environnement pour le transport et `-e '{...}'` en JSON pour
l'entree. (Un scalaire YAML plie ne produit pas non plus de saut de ligne.)

TROIS TESTS QUI NE GARDAIENT RIEN. `test_inventory_host` inscrit ses tests dans une
liste explicite ; mes deux nouveaux n'y etaient pas — definis, jamais joues. La
garde d'exhaustivite ajoutee en a trouve un TROISIEME le jour meme,
`test_etiquette_vlan_repli_et_vide_explicite`, jamais inscrit depuis sa creation :
inscrit, il levait un KeyError sur une fixture qu'il lisait mal. Un test non
inscrit est pire qu'un test absent : on croit l'avoir.

PREUVE, AVEC SON CONTROLE NEGATIF :

    runner du SITE -> ops-01        ops-01  10.17.19.41/24   entre
    runner du SITE -> 10.17.19.21   Connection timed out     refuse

make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 13:45:44 -04:00
d3ba520500 insemination : declarer le lien, l'emettre d'un seul cote, et un test rouge
L'insemination avait un nom depuis ce matin ; elle n'avait pas de flux. Deux
declarations, aux deux bouts, et rien d'autre :

    serveur_ops_site     egress  22/tcp -> serveur_ops_tenant
    serveur_ops_tenant   ingress 22/tcp <- runner_site         partage: true

ETROIT PAR CONSTRUCTION : il vise le GROUPE `serveur_ops_tenant`, qu'un ecosysteme
ne pose que sur une machine. Au socle, il aurait ouvert le SSH du site vers toute
la flotte du tenant.

L'en-tete disait « ce role n'entre JAMAIS chez un tenant ». Frontiere intenable :
`creer-vm` exige `_instance-requise`, et le runner du site avait deja du basculer
son symlink `instance` sur OPS-Chezlepro pour materialiser ses VM. Declarer ne cree
pas ce pouvoir — ca rend limitable un pouvoir qui s'exercait sans borne. Ce qui
reste interdit n'est pas une regle mais un FAIT : il n'a pas la voute du tenant.

LA REGLE EST EMISE D'UN SEUL COTE, et pas celui qu'on croit. Le paquet penetre le
pare-feu par la patte du SITE, pas par le transit : la regle appartient au cote
site du devis. L'emettre aussi depuis l'`ingress` du tenant aurait produit une
seconde regle sur la mauvaise interface — jamais evaluee, indiscernable d'une regle
utile. La declaration du tenant pose sa regle nftables, et elle seule :

    ip saddr { 10.0.31.11 } tcp dport 22 accept

L'adresse DERIVE du plan du site. Ecrite a la main, elle aurait survecu au prochain
deplacement du runner sans bruit — le site a deja deplace ses machines le 08-25.

Plan de la frontiere : 2 objets a creer, 0 a retirer, 126 inchanges. RIEN D'APPLIQUE.

P41 APPLIQUEE AU PLAN DU SITE : `resoudre_flux` en avait besoin a son tour ; les
trois lecteurs demenagent dans `underlay` et `devis_opnsense` delegue.

DEUX GARDES ONT TRAVAILLE : P33 a refuse `ingress 22` sur un hote portant deja le
sshd du socle (reponse : `partage: true`, comme `serveur_backup`), et le devis a
refuse d'emettre vers un alias vide.

UN TEST ROUGE DEPUIS TROIS JOURS. `test_adressage_derive` construisait un site avec
un `index` — or un SITE n'en a pas depuis add94f2 (08-25), remplace par
`bande_basse_de`. Invisible parce que le geste quotidien est `make prouver`, qui ne
joue pas les tests. Remis sur le contrat actuel, avec sa contrepartie : sans
`bande_basse_de`, aucun chevauchement n'est tolere.

make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 13:09:28 -04:00
6fe62f74e1 creer-vm : prouver la materialisation sans entrer chez le tenant
`creer-vm` confirmait son succes en attendant une reponse SSH. Le runner du SITE
materialise le terrain de TOUS les tenants, mais la frontiere lui refuse d'entrer
chez eux — c'est le sens meme de leur isolation. La premiere VM qu'il a creee a
donc ete declaree en echec apres 600 secondes alors qu'elle tournait, avec
l'adresse exacte que le plan lui destinait :

    Attente de SSH sur ops-01 ............ ECHEC: injoignable apres 600s.

LA TENTATION ETAIT D'OUVRIR LE SSH du runner vers tous les tenants. Ca aurait
repare la mesure en detruisant ce qu'elle protege : une machine capable d'entrer
chez chaque locataire est precisement ce que cette architecture refuse d'avoir.

L'agent invite repond sans rien ouvrir — l'API des hyperviseurs est deja le flux
par lequel la VM vient d'etre creee, donc qui peut la creer peut la voir naitre —
et il PROUVE DAVANTAGE. « Quelque chose ecoute sur le port 22 » ne dit ni quel
systeme a demarre, ni si cloud-init a pose la bonne adresse. Sur ops-01 :

    10.17.19.41 portee sur asgard — Debian GNU/Linux 13 (trixie) 6.12.101

Ses echecs distinguent deux causes tres differentes : « la machine demarre mais
rapporte une AUTRE adresse — cloud-init, ou le pont sur lequel elle est posee »,
et « introuvable sur la fabric ».

ATTENDRE LA DISPONIBILITE A CHANGE DE MAIN. Guetter cloud-init et la liberation de
dpkg appartient a qui va CONFIGURER : `deployer` le fait desormais, la ou il se
contentait d'un `ping` unique. Il y gagne ce qu'il n'avait pas — attendre le verrou
APT, faute de quoi la premiere couche echouait dessus. Contrepartie assumee : un
nom d'hote errone patiente au lieu d'echouer vite ; echouer vite interdirait de
chainer creation et deploiement, le geste central d'une reconstruction.

P52 garde le couplage ferme. Deux controles negatifs verifies.

make prouver : CONFORME, 52 OK, 0 echec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:48:49 -04:00
5d0f82792a portabilite : declarer ce dont le moteur depend — quatre defauts reveles par le runner
Premiere materialisation d'une VM de tenant depuis le runner du SITE. Elle a
echoue quatre fois d'affilee, sur quatre dependances que le moteur ne declarait
nulle part. Chacune fonctionnait chez le mainteneur pour une raison DIFFERENTE,
et aucune de ces raisons n'existe sur une autre machine.

1. VERSIONS DES COLLECTIONS. `requirements.yml` les nommait sans les epingler. Le
   runner a recu `community.general` 13.3.0 quand le poste porte la 10.3.0 — et la
   11 a retire les modules Proxmox de cette collection.
       ERROR! couldn't resolve module/action 'community.general.proxmox_pool'

2. INSTALLATION EN AVANT. `ansible-galaxy` ne retrograde pas : declarer la 10.3.0
   ne suffisait pas a defaire une 13.3.0 deja posee. `--force`.

3. EMPLACEMENT. Le `Makefile` pose `ANSIBLE_HOME ?= $(CURDIR)/.ansible` : sous
   `make`, Ansible ne lit QUE `<moteur>/.ansible/collections`. Le role deposait
   dans `<racine>/.ansible/collections` — a cote, jamais lu. Invisible chez le
   mainteneur, ou les collections viennent du paquet systeme, toujours dans le
   chemin quel que soit ANSIBLE_HOME. Et le garde-fou d'installation suivait le
   DEPOT des archives : changer la destination ne le declenchait pas. Il mesure
   desormais la destination — meme piege qu'en 2026-08-24, deplace d'un cran.

4. BIBLIOTHEQUES PYTHON. Le venv recevait `ansible-core` et `pyyaml`, ecrits en
   dur. Les modules Proxmox tournent `delegate_to: localhost` et exigent
   `proxmoxer` SUR LE CONTROLEUR.
       La bibliotheque Python proxmoxer est absente

   Ne d'ici : `requirements-python.txt`, avec sa frontiere ecrite — il ne declare
   QUE ce qui tourne sur le controleur. `python-ldap` et `psycopg2` s'executent
   sur leurs cibles ; le poste du mainteneur ne les a pas et la flotte se deploie,
   ce qui prouve que la ligne est au bon endroit.

P51 garde les trois faiblesses de cette famille : une dependance utilisee sans
etre declaree (le defaut du 2026-08-24, `ansible.posix`), une dependance declaree
sans version, et `proxmoxer` absent alors que des playbooks Proxmox existent.
Quatre controles negatifs verifies.

RESULTAT : ops-01 materialisee par le runner du site. L'agent invite rapporte
`eth0 10.17.19.41/24`, Debian 13 trixie — l'adressage derive du plan, applique.

Un seul executant masque ces ecarts indefiniment ; un second les revele tous en
une soiree. Meme mecanique que le second tenant en aout.

make prouver : CONFORME, 51 OK, 0 echec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:38:35 -04:00
3fa6e4c3e1 collections : epingler les versions — le runner echouait la ou le poste reussissait
Premiere materialisation de VM depuis le runner du SITE :

    ERROR! couldn't resolve module/action 'community.general.proxmox_pool'

Meme depot, meme playbook, meme plan que chez le mainteneur. La difference tenait
a une seule chose que le moteur ne disait pas : la VERSION de ses collections.

Le poste porte `community.general` 10.3.0. Le runner, monte un jour plus tard, a
recu la 13.3.0 — et la version 11 a RETIRE les modules Proxmox de cette collection
(ils vivent desormais dans `community.proxmox`). `requirements.yml` nommait ses
collections sans dire lesquelles : chaque machine installait donc ce qui etait
courant le jour de son montage. Une dependance non epinglee n'est pas une
dependance, c'est un pari sur l'etat d'Internet a la date du deploiement.

TROIS CORRECTIONS.

`requirements.yml` epingle les trois collections aux versions eprouvees.

`serveur_ops` installe avec `--force`. Sans lui, ansible-galaxy laisse en place une
version SUPERIEURE a celle demandee : il ne retrograde pas. Un poste peut etre en
avance, pas seulement en retard, et le depot doit faire autorite dans les deux sens.

P51 garde les deux faiblesses de ce fichier — celle du 2026-08-24, une collection
utilisee sans etre declaree (`ansible.posix`), et celle d'aujourd'hui, declaree sans
version. La preuve ne lit que les fichiers de TACHES : un `defaults/main.yml` porte
`net.ipv4.ip_forward`, qu'un motif trop large prend pour un module.

MIGRATION CONNUE, PAS FAITE : passer les quatre modules Proxmox a
`community.proxmox` permettra de suivre `community.general` au-dela de la 11.

make prouver : CONFORME, 51 OK, 0 echec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 18:58:23 -04:00
3b65384b0f frontiere : le devis sait desormais refuser SANS consigner
L'outil ne savait qu'AUTORISER — `"action": "pass"` etait en dur dans l'emetteur.
Une regle de silence ne pouvait donc pas naitre du depot, et j'en avais pose deux
a la main sur le boitier : exactement ce que ce projet refuse.

CE QUI L'A MOTIVE. Le journal de la frontiere ecrivait 982 000 entrees par jour,
dont 82 % un balayage Internet contre le port VNC et le reste du bavardage de
decouverte du reseau local. Sa fenetre utile etait tombee a QUARANTE-QUATRE
SECONDES. J'y ai cherche la trace d'un flux du site vers les hyperviseurs, je n'ai
rien trouve, et j'en ai conclu a tort qu'aucune regle ne bloquait. Un journal noye
ment aussi surement qu'un journal mort. Mesure apres declaration : ~20 700/jour.

Une regle de silence ne change AUCUN comportement : ce qu'elle vise etait deja
refuse par le defaut. Elle ne supprime qu'une trace que personne ne lira.

TROIS PIECES.

`cle_regle` accepte une action sans changer d'un octet la cle des regles `pass`
deja posees. L'ajout naif d'un champ les aurait toutes detruites pour les recreer
a l'identique, sur la frontiere, en production. Le plan l'a confirme : 7 a creer,
0 a retirer, 121 inchangees.

`_corps_regle` lit l'action, la consignation et la SEQUENCE depuis le devis. La
sequence est ce qui rend un `block` sur : OPNsense evalue en `quick`, donc un
blocage large emis avant les `pass` fermerait courrier, web et acces distant.

`devis_opnsense` lit `opnsense_silences` et en fabrique regles et alias, motif
compris — une regle `block` muette sans raison ecrite est indiscernable d'un oubli.

P50 garde ces deux dangers. Controles negatifs verifies : un silence en sequence 1
echoue, un silence sans motif echoue.

Carte : 28 pieces d'audit (P48 l'avait vu juste).

make prouver : CONFORME, 50 OK, 0 echec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 18:10:39 -04:00
561c034eb4 flux : P49 — le registre des flux avait derive sans bruit
Some checks failed
verifier / verifier (push) Has been cancelled
`docs/registre-flux.md` est GENERE depuis les `roles/*/meta/flux.yml`, et c'est le
document qu'un humain lit pour savoir ce que le pare-feu laisse passer. Son
EXISTENCE etait verifiee depuis longtemps ; sa FRAICHEUR ne l'etait pas.

Il avait derive : la garde d'administration y portait encore `10.0.0.0/24` alors
que le reseau d'administration vaut `10.17.0.0/24`, deux flux `client_resolveur`
ajoutes depuis n'y figuraient pas, et un hote manquait des listes de sources. Un
lecteur y aurait lu un pare-feu qui n'existe plus.

L'inventaire avait deja sa garde — P03, le diff-vide du plan. Le registre des flux
est le meme genre d'artefact : genere, versionne, lu par un humain. Il lui
manquait la meme. `generer_registre` etant une fonction PURE, P49 la rejoue en
memoire et compare — une preuve qui repare ce qu'elle mesure ne mesure plus rien.

Controle negatif ideal, et il ne s'invente pas : la version commitee elle-meme.
Restauree, la preuve echoue ; regeneree, elle passe.

CE QUE CETTE DECOUVERTE CORRIGE AUSSI DANS MA TETE. J'avais decrit le symptome
comme « le runner salit ses propres clones » — une contradiction structurelle
entre un depot-clone et un repertoire de travail. C'etait faux, et la question de
l'exploitant l'a mis au jour. Regenerer un artefact DOIT produire un diff quand
les sources ont change ; ce qui manquait n'etait pas une architecture, c'etait une
garde. Un symptome observe depuis un seul endroit ressemble toujours a une
propriete de cet endroit.

Ce commit emporte aussi la regeneration elle-meme : le registre du moteur, et les
quatorze fichiers nftables de Chezlepro, remis en accord avec leurs sources.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:58:17 -04:00
9e8679215d carte : P48 — l'index du mainteneur ne peut plus mentir
Some checks are pending
verifier / verifier (push) Waiting to run
`docs/carte-set-ops.md` est l'index du MAINTENEUR : l'ordre de lecture du corpus,
et surtout le catalogue des MECANISMES TRANSVERSES avec, pour chacun, OU IL VIT
DANS LE CODE. Son but est ecrit en toutes lettres : « ne plus re-deterrer ce qui
existe ».

Elle n'avait aucune garde, alors que `catalogue-services.md` a la sienne depuis
P38. Ses sept chiffres etaient faux — 54 roles annonces contre 60, 34 documents
contre 38, 15 pieces d'audit contre 27, 70 decisions contre 78.

Le defaut couteux n'est pourtant pas la. C'est le POINTEUR MORT : la carte dit ou
vit un mecanisme, quelqu'un ne l'y trouve pas, et le reimplemente a cote —
exactement la panne qu'elle existe pour prevenir. Aucun de ces nombres ne fait
travailler personne ; mais un document dont les faits verifiables sont faux cesse
d'etre consulte, et c'est alors ses pointeurs qu'on perd.

P48 verifie les deux : 84 chemins cites existent, et 7 chiffres correspondent a
la mesure. Controle negatif verifie sur les DEUX moities — un chiffre fausse, un
pointeur casse, la preuve echoue dans les deux cas.

QUATRE DISTINCTIONS ont du etre ecrites pour qu'elle ne soit pas fausse dans
l'autre sens : un gabarit de nom (`preuve-<date>.md`) decrit une forme, pas un
fichier ; un chemin hors depot (`~/.config/setops-vault-pass`) vit sur le poste de
l'exploitant, et c'est tout l'interet de la doctrine des voutes ; un fragment
(`meta/acces.yml`) vaut comme SUFFIXE, parce qu'un index se lit ainsi ; et un
artefact GENERE (`hosts.yml`) n'a pas a exister dans le moteur. Les quatre sont
nommees dans le code plutot que sautees en silence.

Le tableau « Le depot en chiffres » remplace les comptes en prose : ce qu'on
n'entretient pas, on ne l'affirme pas — ou bien on le fait recompter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 13:56:58 -04:00
87716cefc7 genome : la forge du SITE fait autorite (D-81), et make genome-etat le verifie
Some checks are pending
verifier / verifier (push) Waiting to run
Decision de l'exploitant : la forge du SITE fait autorite pour le genome. Toute
autre copie — y compris celle d'ou le moteur a ete pousse jusqu'ici — est un
MIROIR.

Un ecosysteme se reproduit depuis la forge de son site : c'est de la qu'il clone
son moteur, ses plans, ses modeles. Si l'autorite est ailleurs, cette forge
devient un cache qu'on croit a jour — et le 2026-08-26 elle etait quatre commits
en arriere sans que rien ne le signale, dont le correctif qui desarme le pare-feu
Proxmox.

UNE AUTORITE QU'ON NE VERIFIE PAS EST UNE AUTORITE QU'ON SUPPOSE.

`make genome-etat` confronte, depot par depot, ce que le poste porte a ce que la
forge porte. Il REFUSE en cas d'ecart plutot que de le signaler : un ecart connu
et tolere redevient un ecart oublie, et la commande qui le corrige tient en trois
mots. Il dit aussi quand la copie locale n'est pas propre — des commits pas
encore faits sont une autre forme de retard.

Mesure au passage, et traitee plutot qu'ignoree : le premier contact avec la
forge echoue une fois sur six — poignee TLS expiree, puis cinq reponses de suite.
Ce n'est pas le chemin, qui est prouve ; c'est l'acceptation TLS apres un temps
d'inactivite. Les deux cibles reessaient.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 09:17:24 -04:00
6a5a49f924 site : le runner travaille, et le genome remonte chez lui
Some checks are pending
verifier / verifier (push) Waiting to run
`serveur_ops` et `serveur_ops_site` sont deployes sur `site-ops-01` : le moteur,
les plans des trois tenants, les collections hors ligne, et la voute de
l'underlay deposee CHIFFREE. Le site a son runner.

DEUX FORMES DE RUNNER, ET UNE SEULE ETAIT PREVUE.

`serveur_ops` exigeait un depot `role: instance` et un `serveur_ops_instance` —
la forme d'un runner de TENANT, qui pilote un ecosysteme, monte son plan par le
lien `instance`, detient sa voute.

Un runner de SITE ne pilote rien : il MATERIALISE le terrain que d'autres
occuperont, et son inventaire est dynamique. Son plan declarait donc ses depots
de tenants en `role: tenant`, a dessein et avec ses raisons ecrites — et le role
le refusait au nom d'une exigence qui ne le concernait pas. Il exige desormais
que le runner pilote QUELQUE CHOSE, sans prescrire quoi.

Et il posait le lien quand meme : `serveur_ops_instance: ""` donnait
`instance -> /opt/setops/`, un repertoire qui n'est l'ecosysteme de personne. Un
lien qui existe et ne designe rien est pire qu'un lien absent : tout ce qui le
suit croit avoir trouve une instance.

LE CERTIFICAT COUVRE LES NOMS DU SERVICE.

`client_pki` ne mettait dans ses SAN que le FQDN, le nom court et l'IP. Le
clonage du genome a bute dessus : « certificate subject name
(site-forge-01.genese.internal) does not match target hostname
'forge.genese.internal' ». Le nom declare `expose:` etait publie partout —
plancher, zone DNS — et couvert nulle part.

Les expositions que cet hote SERT REELLEMENT entrent maintenant dans le
certificat. Un certificat qui revendiquerait le nom d'un service rendu ailleurs
serait une usurpation, pas une commodite.

`make genome-pousser` — LE RUNNER POUSSE, LE POSTE NE ROUTE PAS.

La forge du genome vit DANS le site, sur un reseau que le poste de l'exploitant
ne route pas. On ne perce pas un chemin pour lui : on lui retire le role. Le
poste emballe les commits dans un `git bundle` — un fichier, verifiable, qui ne
demande aucun reseau — et le runner verifie, avance EN AVANCE RAPIDE SEULEMENT,
puis pousse avec SA cle, autorisee en ecriture sur ces depots-la seulement.

Ce qui est pousse se DERIVE de `serveur_ops_depots`. `DEPOT=<nom>` en cible un
seul.

Trois pieges rencontres, tous inscrits dans le code : l'outil ne peut pas etre
supposé present sur le runner (il ne recevrait cette version qu'APRES la poussee
qu'elle sert a faire — le controleur le porte) ; la branche n'est pas toujours
`main` (deux depots vivent sur `master`, et le plan le declarait deja) ; le
dossier des depots freres contient un espace, d'ou `argv` et non `cmd`.

Le juge n'est pas le journal du playbook mais LA FORGE : on demande a son API ce
qu'elle porte, et on refuse si ca differe de ce que porte le runner. Six depots
verifies.

Pourquoi ca comptait : la forge du site etait restee quatre commits derriere le
poste, sans que rien ne le signale — dont le correctif qui desarme le pare-feu
Proxmox. Un ecosysteme qui se reproduit depuis une forge en retard reproduit ses
defauts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 08:52:37 -04:00
e5841ea684 dns : quatre zones inverses pour le site, et rien de plus
Some checks are pending
verifier / verifier (push) Waiting to run
Le site ne servait aucun PTR. `serveur_powerdns_zone_inverse` derivait d'un
supernet /16 — la forme d'un TENANT, qui tire tout de son index. Un site ne
derive pas : il declare plusieurs /24 et n'a pas de supernet unique, si bien que
la derivation rendait une chaine vide et qu'aucune zone n'etait generee.

Le site revendique desormais exactement ce qu'il occupe :

  31.0.10.in-addr.arpa   32.0.10.in-addr.arpa
  33.0.10.in-addr.arpa   34.0.10.in-addr.arpa

Revendiquer `0.10.in-addr.arpa` d'un seul geste aurait ete plus simple et faux :
cette zone couvre aussi la frontiere, le transit et les hyperviseurs, qui ne sont
pas a lui. Une autorite qu'on s'attribue sans l'exercer est une panne differee —
le resolveur repondrait NXDOMAIN pour des adresses qu'un autre sait nommer.

LA DERIVATION VIT DANS UN FILTRE (`zones_inverses`) parce qu'elle a DEUX
appelants : `serveur_powerdns` ecrit ces zones, `serveur_resolveur` les delegue a
l'autoritatif. Deux calculs separes finiraient par diverger, et la divergence ne
se verrait qu'au premier PTR interroge. Le nom d'une zone dit sa profondeur —
trois etiquettes numeriques valent un /24, deux valent un /16 — et le modele en
deduit seul la forme du PTR.

DEUX CHEMINS MORTS TROUVES EN ROUTE.

Les zones etaient servies, et personne ne les demandait. `serveur_resolveur`
deleguait la zone directe par une `stub-zone` mais pas les inverses : `dig -x`
rendait vide depuis les cinq machines alors que la meme requete posee directement
a l'autoritatif repondait juste. Un service correct derriere un chemin que rien
n'emprunte.

Puis, les stubs poses, Unbound repondait toujours NXDOMAIN avec le drapeau `aa` —
une reponse AUTORITAIRE, sans jamais consulter le stub. Il embarque des
`local-zone` pour tout l'espace RFC1918 inverse. `nodefault` n'y change rien : ce
mode n'agit que si le nom correspond EXACTEMENT a une zone par defaut, et la
sienne est `10.in-addr.arpa`, le /8 entier. C'est `transparent` qui laisse la
requete suivre son cours — pour nos quatre zones seulement, la ou
`unblock-lan-zones` aurait ouvert tout l'espace prive.

Rien ne distinguait ce blocage d'une absence : le meme NXDOMAIN qu'un nom qui
n'existe pas.

AUSSI : les zones inverses sont desormais validees par `named-checkzone` comme la
directe (un fichier mal forme etait refuse en silence par PowerDNS), et une zone
qu'un ecosysteme cesse de revendiquer est retiree du repertoire.

VERIFICATION. Les cinq PTR resolvent depuis les cinq hotes par le resolveur, les
huit noms directs par le plancher ET par le DNS, et les deux roles sont
idempotents (changed=0).

P47 evalue la derivation sur cinq cas — un site a quatre zones, un tenant a une,
deux machines d'un meme /24 qui n'en font qu'une, aucune adresse, une adresse
illisible. Controle negatif verifie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 20:58:50 -04:00
a74e35bf21 site : le plancher survit au redemarrage, et la zone dit les vraies adresses
Some checks are pending
verifier / verifier (push) Waiting to run
Le decoupage du site en quatre zones a deplace cinq machines. Ni le plancher
/etc/hosts ni la zone DNS n'avaient suivi. Quatre defauts, tous dans le moteur.

LE PLANCHER ETAIT EFFACE A CHAQUE DEMARRAGE — ET LE PREMIER CORRECTIF N'EN
ETAIT PAS UN.

`hosts_statiques` posait `99-setops-hosts.cfg` avec `manage_etc_hosts: false`,
pendant que `cloud_init` posait `99_setops.cfg` avec `true`. Dans `cloud.cfg.d`
l'ordre est LEXICAL et le dernier gagne : `-` vaut 0x2D, `_` vaut 0x5F. On a
donc retire la cle de `cloud_init` — le role qui POSSEDE le fichier decide —
puis renomme notre fragment `zz-` pour passer apres le `99_chezlepro.cfg` du
gabarit dore.

Et ca ne suffisait toujours pas. Redemarrage d'epreuve : plancher encore efface.
La cause reelle est ailleurs — Proxmox inscrit `manage_etc_hosts: true` dans la
USER-DATA de son lecteur cloud-init, et la user-data prime sur `cloud.cfg.d`
tout entier. Aucun fragment ne pouvait gagner ; renommer pour parler en dernier
ne servait a rien, le dernier mot n'appartenant pas a ce repertoire.

Ce que cloud-init regenere, il le regenere depuis `hosts.debian.tmpl` — le
gabarit le documente lui-meme. `hosts_statiques` le pose desormais avec le MEME
contenu que /etc/hosts, et une garde compare les deux a chaque passage.
Redemarrage d'epreuve : les neuf entrees sont la.

La garde precedente affirmait « conforme » en mesurant l'ordre lexical — vrai,
et sans rapport avec ce qui se passait. Une garde qui mesure la mauvaise chose
est pire qu'aucune.

LA ZONE DNS NE PUBLIAIT PAS LES NOMS DE SERVICE.

`forge.genese.internal` et `pki.genese.internal` — des noms que les certificats
portent et que les clients appellent — n'avaient aucun enregistrement. Seul le
plancher savait les resoudre. Trois causes empilees :

  - le plan du site coupait `serveur_powerdns_publier_expositions`, au motif que
    « le site n'a pas d'edge » : ca confondait PUBLIC et EXPOSE ;
  - `expositions_des_applications` rendait `domaine: None` faute de
    `domaines.yml`, et le modele de zone ecarte les expositions dont le domaine
    n'est pas la zone. Repli ajoute, symetrique de celui deja ecrit pour `edge` :
    sans domaine public declare, le domaine est celui que porte le FQDN ;
  - `serveur_powerdns` exigeait les deux registres et echouait si `domaines.yml`
    manquait — le meme tout-ou-rien que `hosts_statiques` a corrige le meme jour.

Puis `named-checkzone` a refuse la zone : `dns.genese.internal` heritait d'un
CNAME par defaut du role ET d'un A par exposition. La garde a bien joue son role
— elle a arrete une zone cassee avant qu'elle soit servie. Le plan l'emporte
desormais sur le defaut du role.

Un service ne doit pas dependre d'un plancher pour etre joignable : le plancher
est un filet, pas le sol.

VERIFICATION. Les huit noms — cinq machines, trois services — resolvent vers les
bonnes adresses depuis les cinq hotes, par le plancher ET par le DNS, et le
plancher survit au redemarrage.

P46 refuse desormais deux choses : plus d'un role ecrivant `manage_etc_hosts`,
et l'absence du gabarit maitre. Controle negatif verifie.

RESTE NOMME, PAS CORRIGE : la zone INVERSE. `serveur_powerdns_zone_inverse`
derive d'un supernet /16 — la forme d'un tenant. Un site declare plusieurs /24
et n'a pas de supernet unique : aucune zone inverse n'est generee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 20:31:01 -04:00
ac48b7650b proxmox : le pare-feu ne s'arme que dans le SDN — et 45e preuve
Some checks are pending
verifier / verifier (push) Waiting to run
Le decoupage du site en quatre zones a revele un defaut qui dormait dans le
moteur. `firewall=1` sur l'interface d'une VM fait passer tout le trafic ponte
par conntrack. Sur un VNet SDN c'est sans consequence : en EVPN le routage
inter-VNet se fait dans le VRF, SUR LE NOEUD, et le flux ne quitte jamais
l'hyperviseur. Sur un pont classique route par une frontiere externe, deux VM du
MEME noeud dans deux VLAN differents ne se parlent qu'en EPINGLE : la trame sort
par le lien physique, la frontiere la route, elle revient sur le meme pont. La
meme table conntrack voit alors les deux moities de la connexion, classe le
retour INVALID, et PVEFW-FORWARD le jette.

La mesure a tranche :

  ops(asgard)  -> pki(asgard)     0/8    dns(gandalf) -> cache(gandalf)  0/8
  ops(asgard)  -> forge(vishnu)   6/8    dns(gandalf) -> pki(asgard)     6/8
  ops(asgard)  -> cache(gandalf)  8/8    dns(gandalf) -> forge(vishnu)   8/8

Toutes les paires intra-noeud echouent, toutes les paires inter-noeuds passent.
Douze tentatives faisaient monter le compteur `ctstate INVALID` de +112 sur
asgard et +116 sur gandalf ; `firewall=0` pose, il ne bouge plus — 0 sur les
deux, pour le meme trafic.

Le defaut se deguise en panne reseau : la poignee TCP ABOUTIT, et ce sont les
paquets de DONNEES qui disparaissent. L'AC le disait dans son propre journal
(`TLS handshake error ... i/o timeout` : elle accepte et attend un ClientHello
qui n'arrive jamais). Ecartes un par un, par la mesure : regles identiques champ
par champ, alias corrects, assignation des interfaces confirmee, ARP et routes
saines, IPS desactive, shaper vide, NAT source limite a `wan`, MTU a 1500 de
bout en bout — et la taille sans effet, 100 octets se perdant comme 1460.

L'INTENTION DECLAREE NE SUFFIT PAS : un tenant declare
`proxmox_clone_parefeu_interface: true` avec `proxmox_clone_pont: vmbr1` comme
valeur PAR DEFAUT, chaque hote la remplacant par son VNet derive. L'hote qui
retombe sur `vmbr1` naitrait arme sur un pont classique. `cloner_vm_debian.yml`
croise donc l'intention avec le pont REELLEMENT utilise, et le dit quand il
desarme — un desarmement muet serait le meme piege, en silence.

P45 EVALUE l'expression du playbook sur quatre cas plutot que d'en lire le
texte, dont un qui DOIT rendre vrai : sans lui, une expression constamment
fausse passerait la preuve sans rien garantir. Controle negatif verifie — le
drapeau remis a plat fait echouer la preuve.

Ce commit emporte aussi le desarmement d'`unattended-upgrades` dans
`common_packages`, jusqu'ici applique sur les machines mais pas versionne : le
verrou dpkg tenu 48 minutes par une vague de maj automatiques a coute deux
deploiements.

Le site a revele ce defaut parce qu'il a ete le premier a porter plusieurs
zones. Ce n'est pas une particularite du site : un tenant derive les siennes du
meme principe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 19:50:53 -04:00
1a7c042e5b site : quatre zones d'autorite, et l'ordre inscrit dans les integrations
Le site n'est plus un /24 plat. Une zone par nature d'autorite — pilotage, autorite,
genome, service — chacune son VLAN et sa patte sur la frontiere. L'inversion corrigee,
mesuree : les cinq VM du site n'avaient AUCUN filtrage est-ouest, contre policy_in=DROP
sur une machine de tenant. La plus autoritaire etait la moins protegee.

Filtrage nord-sud par choix de l'exploitant : un seul point de police, un seul devis.
90 -> 117 regles. Chaque flux `flotte` produit une regle par zone SOURCE, destination
nommee — ce qui etait gratuit devient police.

L'ORDRE FAIT PARTIE DE L'INTEGRATION. client_pki tournait en parallele sur tous les hotes ;
sur celui qui porte l'autorite il recharge step-ca, et les quatre autres echouaient dans
cette fenetre sur `TLS handshake timeout` — un message qui accuse le reseau. Chaque
integration declare desormais sa dependance dans meta/integration.yml, les playbooks en
sont le miroir genere, et P44 refuse l'ecart (4 controles negatifs). `serveur: ~` est une
reponse valable : client_metrique pose un exportateur qu'on vient LIRE.

Autres defauts du meme soir :
  - le plancher /etc/hosts venait APRES le premier apt, qui vise le cache par son NOM :
    boucle fermee des que les adresses changent. Il ne s'installe pas, il rend installable.
  - dns_amorcage ecrit en dur a eu tort deux fois ; il se derive de serveur_resolveur.
  - l'ACL du resolveur derivait d'un seul sous-reseau : trois zones refusees sur quatre.

ET UNE ERREUR A MOI : j'ai diagnostique un trou noir de MTU et declare 1450. Faux — pas de
VXLAN sur ce chemin, tout est a 1500 de bout en bout. Ma mesure etait reelle, mon
interpretation non : je venais de debrancher la carte a chaud, et chaque `ip link set mtu`
reconfigurait l'interface. C'est la reconfiguration qui debloquait, pas la valeur.

44 preuves vertes, ansible-lint profil production.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:31:07 -04:00
158c3b314a preuve : P43 — la frontiere voit-elle les machines du site ?
Couvre ce qui a failli coûter 36 objets ce soir : le devis de la frontiere avait cesse de
voir le site et proposait de retirer tous ses alias et toutes ses regles. Harnais vert,
lint vert — seule la lecture manuelle du plan avant application l'a attrape.

La premiere version etait inutile, et c'est instructif : elle lisait le plan par
`devis_opnsense._machines_du_plan_site()`, la fonction meme dont la panne etait a
detecter. Eprouvee sur la regression reelle, elle SE TAISAIT — les deux voyaient le vide,
et elle concluait « rien a prouver ». Une preuve qui partage la source de ce qu'elle
verifie ne verifie rien.

La version retenue lit plan/serveurs.yml et plan/applications.yml DIRECTEMENT et confronte
au devis produit. Eprouvee sur la regression reelle : elle refuse et nomme la cause.

43 preuves vertes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 14:11:39 -04:00
ea67b75a15 dns : le site resout chez lui, et le socle cesse de le defaire
Mise au point de l'exploitant : la frontiere n'a pas de service DNS actif et gere. Un
Unbound y tourne — il repondait, ce qui m'a induit en erreur — mais un processus n'est pas
un service. La delegation de zone que j'y avais posee est retiree.

Et le site a son propre DNS : `dns_amorcage` pointait encore sur la frontiere, valeur d'un
moment ou site-dns-01 n'existait pas. Elle vaut desormais 10.0.3.51.

Deux defauts revelés au passage :

  - devis_opnsense lisait encore underlay.machines(), vide depuis le deplacement du plan.
    Il proposait de RETIRER 36 objets — tous les alias et regles du site. Aucune preuve ne
    couvre ce devis : c'est le plan avant application qui l'a attrape.
  - le socle defaisait la bascule de client_resolveur a chaque passage. Sa garde ne
    protegeait que l'hote du resolveur (127.0.0.1). Invisible chez un tenant, ou
    dns_amorcage vaut l'adresse du resolveur : la coincidence masquait le defaut. Ca ne
    s'est vu qu'en retirant la regle de pare-feu — les cinq machines ont perdu la
    resolution d'un coup.

L'amorcage ne s'applique plus que si rien de sense n'est en place. Verifie : socle
`changed=0`, les cinq machines restent sur 10.0.3.51.

42 preuves vertes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:57:48 -04:00
43666cff6e site : un SITE a bien un plan — et huit defauts que lui seul pouvait reveler
J'ai repete toute la journee qu'un SITE n'est pas un plan. C'etait faux : j'en
reconstruisais un morceau par morceau dans underlay.yml sans le nommer. Ce qui est vrai,
c'est qu'un site ne DERIVE pas — il declare ses adresses parce qu'il EST le terrain — et
ne passe donc pas par `instancier`. Mais ne pas deriver n'est pas ne pas avoir de plan.

SITE-Chezlepro/plan/{10-intrants,serveurs,applications}.yml ; underlay.yml ne garde que
la fabric. Ce qui est MESURE d'un cote, ce qui est VOULU de l'autre. Les groupes viennent
du registre des applications, les gardes ont suivi le plan (4 controles negatifs).

Huit defauts, tous invisibles chez un tenant parce qu'il a toujours tout :
  - client_pki codait `infra-pki-01` EN DUR — vrai par coincidence de nomenclature ;
  - aucun moyen pour un service non-root de lire la cle (Forgejo tourne en `git`) ;
  - le certificat, PUBLIC par nature, restait en 0600 ;
  - l'unite Forgejo n'avait pas d'ExecReload : un cert renouvele aurait ete servi perime ;
  - le role ne savait pas servir TLS lui-meme (il y avait toujours un edge) ;
  - le cert ne couvrait pas le nom de SERVICE, faute d'`expose:` ;
  - les registres se chargeaient en tout-ou-rien : sans domaines.yml, plancher vide ;
  - le flux declarait 3000 en dur.

Le message accusait presque toujours autre chose : le DNS quand c'etait un nom faux, une
permission de fichier quand c'etait un port privilegie, rien du tout quand le plancher
s'ecrivait vide.

ROOT_URL est gravee dans les URL de clonage : la forge sert desormais sur 443, avec
CAP_NET_BIND_SERVICE, et son URL n'a plus de port.

Verifie depuis le reseau : Verify return code 0, https://forge.genese.internal/ -> 200.

42 preuves vertes, flux coherents (34 roles, 92 flux).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:34 -04:00
add94f2cbd underlay : un SITE n'a pas d'index — le vestige est retire
`index: 17` ne disait pas « voici mon adressage » : un site ne derive rien, ses machines
vivent sur un reseau de fabric hors de toute derivation. Il disait UNE seule chose — quel
supernet de tenant est le mien — pour autoriser un reseau du site a en occuper la bande
basse (D-77). Un reste de la coincidence hebergeur<->tenant chez Chezlepro, qui est les
deux a la fois ; ailleurs il aurait fallu inventer un index a un hebergeur qui n'heberge
pas son propre ecosysteme.

L'exception se declare desormais sur le RESEAU qui en a besoin, et elle NOMME le tenant
dont elle occupe la bande basse (`bande_basse_de: OPS-Chezlepro`). Plus vrai : un seul
reseau est concerne. Plus verifiable : le nom se resout dans le registre `tenants:`, donc
une faute de frappe est refusee au lieu de passer.

Quatre controles negatifs, tous refuses — tenant inconnu, exception absente, debordement
sur la bande des zones, et le mauvais tenant nomme.

42 preuves vertes, les quatre devis du panneau OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 12:11:20 -04:00
467b05cbc0 site : un ecosysteme complet — PKI, DNS, forge, cache, runner
Le site portait trois services sur les sept du modele `origine`. Sa forge servait LE
GENOME EN CLAIR et aucune de ses machines n'avait de certificat — donc aucun chiffrement
est-ouest, ce que la doctrine zero-confiance interdit. site-pki-01 (step-ca) et
site-dns-01 (PowerDNS + resolveur colocalises) rejoignent les trois autres.

Quatre defauts que le site a fait tomber, chacun invisible chez un tenant :

  - DNS bloque par notre propre default-deny. Un tenant a son resolveur DANS son reseau
    et ne traverse jamais la frontiere ; le site interroge la sienne. La regle est derivee
    de `site.dns_amorcage`, destination declaree, jamais `any`.
  - serveur_cache_site n'installe rien : il marque un cache et lit les variables de
    serveur_artefacts. Les defauts d'un role ne sont en portee que dans le play qui
    l'inclut — une dependance de role regle l'ordre ET la portee.
  - resoudre_idp partait meme avec OIDC desactive, et exigeait un plan. La resolution
    suit desormais l'usage.
  - le plancher /etc/hosts etait VIDE : `hotes_actifs` n'existait pas dans l'inventaire
    du site. Un role qui reussit en n'ecrivant rien est la pire forme d'echec.

Une machine du site peut desormais se configurer (`variables:`), appliquee en dernier :
ce qu'une machine declare d'elle-meme prime sur ce que le site declare pour toutes.

Verifie et non suppose : systemd disait `active` mais rien n'ecoutait sur 443 — step-ca
sert sur 8443. La zone souveraine resout et la recursion marche, mesurees sur la machine.

42 preuves vertes, ansible-lint profil production.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 12:00:17 -04:00
a530d5f70e federation : c'est le SITE qui determine l'index d'un tenant
SITE et OPS sont deux classes distinctes. Le site decide de la fabric et du reseau et
PRODUIT les intrants ; le tenant les consomme et n'a d'intelligence que sur ses
applications, leur configuration et leurs integrations. Une valeur reseau qu'un OPS decide
est une valeur mal placee.

L'index est l'intrant reseau par excellence — supernet, sous-reseaux de zone, VLAN, VMID,
noms de VNet en descendent. Il etait declare DEUX FOIS : dans la nomenclature du tenant et
dans l'underlay du site. Et le site ne declarait meme pas qui il heberge : la cle
`tenants:` existait dans le code, jamais dans la carte — la decouverte se faisait par
balayage des dossiers freres.

`tenants:` devient un registre d'allocation (nom -> index). Le site alloue, le tenant
recoit, la nomenclature n'est plus que la copie verifiable d'une decision prise ailleurs.
Quatre gardes neuves, chacune eprouvee par un controle negatif, sous P23.

Un tenant qui s'emancipe recoit un index NEUF de son nouveau site : l'index n'est pas une
propriete du tenant, c'est une place sur une fabric.

42 preuves vertes, quatre devis du panneau OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:42:14 -04:00
fcde343ef0 devis reseau : un segment non etiquete n'appartient a aucune fabric de switch
`segment_physique: true` retire la cle `vlan`. devis_reseau.py ecrivait `r['vlan']` en une
vingtaine d'endroits : KeyError, /api/devis-reseau en 500, et LA VUE RESEAU DU PANNEAU
RESTAIT VIDE — une panne a deux couches de sa cause.

Filtre a la source plutot que colmatage : devis_reseau definit son propre
`reseaux_de_fabric` qui ecarte les reseaux sans etiquette, et les dix appels y passent.
Colmater les vingt occurrences aurait laisse la vingt-et-unieme.

Le bloc de gestion d'un switch echappait au filtre (il lit son reseau directement). Sans
etiquette, aucune `interface VlanN` n'existe : l'adresse va sur l'interface de gestion
native du boitier. Le devis le DIT au lieu d'inventer une syntaxe.

Expose au passage que bifrost-3 et bifrost-4 declarent leur gestion sur un segment qui ne
les traverse pas, a des adresses qui n'ont jamais repondu.

Les quatre devis du panneau repassent. 42 preuves vertes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 09:51:57 -04:00
a6713ffdcd doctrine : patient 0 n'est pas un tenant — corriger la raison, garder le geste
Le commit dc92f01 justifiait le retrait des roles de site de patient 0 par « patient 0
est un tenant comme les autres, c'est meme tout ce qu'il prouve ». C'est FAUX.

Un tenant ordinaire existe POUR SES GENS : Chezlepro heberge de l'identite, du courriel,
de la collaboration. Patient 0 n'heberge que LA LIGNEE. Il est l'ecosysteme d'origine et
le detenteur du genome, et il le reste.

Ce qui le quitte, ce sont deux responsabilites de SITE qu'il tenait faute d'un SITE
capable de les porter — a l'epoque un site n'etait qu'un fichier de carte, sans machines.
Le SITE existe desormais comme objet a part entiere : le geste etait donc juste, seule sa
raison ne l'etait pas.

Le texte est corrige plutot qu'efface, dans le CHANGELOG comme dans les fichiers : une
doctrine juste appuyee sur une raison fausse finit toujours par se retourner.

42 preuves vertes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 09:34:02 -04:00
6715f0d5c5 clonage : quatre defauts que la premiere VM placee hors du noeud du gabarit a reveles
Toutes les VM naissaient sur le noeud du gabarit puis migraient. site-cache-01 est la
premiere a etre placee ailleurs, et elle a fait tomber quatre defauts enchaines :

1. l'URL de clonage visait le noeud de DESTINATION, alors que l'API veut celui qui DETIENT
   le gabarit ; la destination se dit par `target`. D'ou un 500 sur tout clonage
   inter-noeuds. Le noeud du gabarit se decouvre dans l'inventaire du cluster.
2. ce 500 etait avale par failed_when: false + no_log: true. Une assertion le releve
   desormais, message de l'API compris.
3. l'attente interrogeait nodes/<destination>/tasks/<UPID> ; la tache vit sur le noeud du
   gabarit. 60 tentatives x 10 s pour un clonage termine en 87 s, puis la suite qui reprend
   sans un mot. Le noeud se lit dans l'UPID lui-meme.
4. la garde d'apres-attente retombait sur `exitstatus | default('OK')` : une attente qui
   n'avait rien observe passait pour un succes. Elle exige d'avoir VU la tache s'arreter.

Et une taille de disque porte toujours son unite : `disque: 40` etait lu comme un
retrecissement, erreur toleree a raison — la machine naissait donc avec les 16 Go du
gabarit au lieu de 40, sans que rien ne le dise.

Le site declare enfin ce qu'il materialise (materialisation: gabarit, stockage,
clone_complet) au lieu de l'heriter du tenant actif.

Chrono : clonage reel 59 s et 87 s pour 3,3 Gio alloues.

42 preuves vertes, ansible-lint profil production.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 02:44:21 -04:00
dc92f01fe4 frontiere : le devis voit le site — fabric, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.

Deux flux manquaient :
  - serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
    cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
  - serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
    sont deux autres pouvoirs, declares a part.

Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.

Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.

Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.

42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
6e126dd8a9 site : le chemin qui materialise les machines de l'hebergeur
- site_machines.py : traduit une declaration d'underlay en SETOPS_*, que cloner-vm
  consomme deja. Le clone reste le seul chemin eprouve.
- site_inventaire.py : inventaire DYNAMIQUE. Un site ne derivant de rien, sa declaration
  est deja sa forme finale — un hosts.yml genere ne rendrait rien plus inspectable.
  Les groupes sont les services : playbooks/groupes/<role>.yml trouve ses hotes seul.
- cibles site-decrire / site-inventaire / site-creer (CONFIRMER=true) / site-appliquer,
  toutes independantes d'une instance montee : le site existe avant tout tenant.
- playbook de groupe manquant pour serveur_cache_site, et son classement en couche.

Le premier garde-fou de site-appliquer refusait un GROUPE vide : il ne pouvait jamais
se declencher (GROUPE a un defaut global). Le vrai risque, observe en le testant : un
playbook de tenant contre l'inventaire du site ne matche aucun hote et sort avec 0 — un
succes qui n'a rien fait. Refus desormais de tout groupe absent de cet inventaire.

42 preuves vertes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 23:11:32 -04:00
93dde5b4f2 site : un SITE n'est pas un plan — ses machines vivent dans l'underlay
La branche « adressage declare » du generateur de tenants est retiree. Un tenant se
derive de son index ; un site n'a pas d'index et ne derive de rien. Les faire passer par
la meme moulinette donnait une nomenclature de site vide de sens, et un site exclu des
devis par ABSENCE d'index plutot que par nature.

Les machines de l'hebergeur se declarent desormais dans underlay.yml, a cote des switches
et des hyperviseurs qui les portent. Six gardes neuves, chacune eprouvee par un controle
negatif.

Mesures qui ont corrige la carte :
  - 10.17.0.0/24 n'a pas d'etiquette VLAN (segment physique sur igb0 de la frontiere) ;
    le VLAN 10 de vmbr1 est l'ancien plan 10.0.0.0/24, vide.
  - aucun pont d'hyperviseur ne porte ce segment : trois sondes muettes, temoin positif
    reussi. Une VM y naitrait sourde — le validateur le refuse.
  - les machines du site vont donc sur grappe-controle (vmbr0), seul plan de l'hebergeur
    porte par un pont reel, avec passerelle et sortie.

Role d'hote neuf : passerelle_amont — un routeur reel que nous n'administrons pas.

42 preuves vertes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 23:02:05 -04:00
9960cfd68e instancier : accepter un adressage declare, pour les plans sans index
Un SITE decrit les machines de l'hebergeur : pas d'index, pas de cohabitation
avec les tenants, il vit dans le reseau d'administration. Rien ne peut donc
deriver d'un seed inexistant.

L'explicite (`ip`, `vmid`, `vlan`) gagne sur le derive, et les deux chemins se
rejoignent sur un seul jeu de hostvars. La garde reste entiere : une machine
sans adresse -- ni declaree ni derivable -- est toujours refusee.

Revele en preparant le plan du site : l'underlay ne dit PAS quel pont Proxmox
porte quel reseau. Les tenants ne s'en apercevaient pas, leur pont etant un VNet
derive de leur index. Un SITE n'en a pas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 20:35:52 -04:00
bf8e27ef85 pare-feu est-ouest : applique, et le port derive enfin compris des deux cotes
verifier_ports traitait depuis toujours un port non numerique comme « pas une
ecoute fixe ». Le generateur est-ouest l'envoyait tel quel a l'API Proxmox :
« invalid port 'derive' », six regles refusees. Une meme notion, comprise d'un
cote et pas de l'autre.

Sauter est la bonne reponse : depuis que le resolveur est la seule porte,
PowerDNS n'ecoute que sur 127.0.0.1:5300 -- aucune regle est-ouest n'a d'objet
pour lui. Les trois groupes t*-srv-powerdns sont retires.

Mais un flux qu'on n'applique pas doit SE VOIR : le devis recense et affiche les
ports sautes avec leur raison. Sans cette note, sauter proprement serait devenu
un trou silencieux.

Les deux devis sont clos. Flotte verifiee : DNS, Internet, apt, cache joignable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 17:58:54 -04:00
a1997774be frontiere : appliquee, et la contrainte de nommage d'OPNsense consignee
Devis clos : 0 a creer, 0 a retirer, 65 inchange + 15 routes. Flotte verifiee
apres coup -- DNS interne, Internet et apt sans erreur sur les cinq hotes.

OPNsense refuse un alias de 32 caracteres ou plus. La contrainte n'etait ecrite
nulle part et se manifestait a l'APPLICATION, pas au devis. nom_alias abrege
desormais (SERVEUR_ -> SRV_, comme le pare-feu est-ouest) et REFUSE bruyamment
si le nom deborde encore : un devis qui promet un objet que la cible rejettera
n'est pas un devis. Le role devient serveur_cache_site.

« RIEN N'EST APPLIQUE » signifiait « pas encore recharge », pas « rien ecrit » :
les objets etaient dans la config, le pare-feu en marche les ignorait. J'avais
lu le message a l'envers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 17:51:29 -04:00
fa7e656844 artefacts : le chainage des caches, et la regle qui appartient au SITE
Une regle inter-tenant n'appartient ni a l'emetteur ni au recepteur : c'est le
site qui autorise un flux entre deux de ses tenants, et son runner qui prepare
le terrain.

Deux silences fermes dans le generateur de frontiere. flux_frontiere() ne
retenait que les flux `externe` -- or deux tenants vivent sur des VLAN routes
par la frontiere, leur trafic la traverse. Et un egress vers un voisin recevait
!SETOPS_INTERNES en destination : le port ouvert vers l'INTERNET. La destination
est desormais nommee.

Deux roles parce que meta/flux.yml est statique : un role unique aurait declare
l'ingress pour TOUS les caches -- maillage complet, visible au devis.
serveur_artefacts_site porte l'ingress, serveur_artefacts l'egress vers l'amont.

Debian est desormais telecharge une fois pour toute la fabric. Le cache du site
ne voit que des requetes agregees, jamais quelle machine installe quoi.

Rien n'est applique : le devis se lit avant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 17:40:33 -04:00
c391d1ed30 frontiere : rendre les flux inter-tenants, sans encore les declarer
Le mot `voisins_site` etait accepte par la validation mais AUCUN generateur ne
le rendait : zero regle au devis, et rien ne le signalait. Deux causes, toutes
deux fermees.

flux_frontiere() ne retenait que les flux `externe`. Or deux tenants de la meme
fabric vivent sur des VLAN distincts, routes par la frontiere : leur trafic la
traverse, donc elle doit le porter.

Et le rendu manquait : une regle inter-tenant s'attache au meme lien de transit
que le reste -- les tenants s'y distinguent par leur ALIAS SOURCE, pas par une
interface. Ma mise en garde precedente reposait sur un modele faux.

LA REGLE APPARTIENT AU SITE, pas a l'un des deux tenants : c'est son runner qui
prepare le terrain, aucun ecosysteme n'ouvre de porte chez un autre.

Les declarations de chainage restent RETIREES : le devis obtenu ouvrait plus que
voulu -- maillage complet entre tous les caches, et un egress 3142 vers
l'Internet au lieu du seul voisin. Deux raffinements a faire avant de declarer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 17:32:35 -04:00
6547eb2fac flux : le mot voisins_site, et l'aveu de ce qui manque pour s'en servir
Le registre ne savait pas dire « les autres tenants de ma fabric ». ingress +
externe signifie DEPUIS L'INTERNET : declarer ainsi un cache partage l'aurait
publie au monde.

voisins_site rend les supernets des tenants que CE SITE heberge, en reutilisant
devis_reseau.decouvrir_du_site() plutot qu'en ecrivant un second recensement.
Ma premiere version lisait un `federe` absent comme « non federe » et excluait
Chezlepro et Technolibre en silence -- la decouverte canonique dit l'inverse.

LE CHAINAGE DES CACHES N'EST PAS LIVRE. Aucun generateur ne rend ce mot : zero
regle 3142 au devis de frontiere. Les declarations ont donc ete RETIREES plutot
que laissees a moitie -- un flux declare que personne n'applique est le piege
que ce depot traque.

La difficulte est structurelle : les regles OPNsense s'evaluent sur l'interface
d'ARRIVEE, donc une regle inter-tenant doit etre posee sur l'interface du
VOISIN. Le generateur construit tenant par tenant, sur les interfaces de ce
tenant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 17:15:19 -04:00
db2d6f6bf0 resolveur : un par tenant, et non plus un par machine
Mesure avant de decider : ~21 Mo de RSS par VM pour 28 a 189 requetes servies.
Le gain de cache est negligeable ; ce qu'on recupere, c'est un demon au lieu de
cinq et un endroit a regarder au lieu de cinq.

Pas sur la frontiere : un resolveur de SITE devrait connaitre la zone interne de
chaque tenant, et un tenant dont le resolveur vit chez l'hebergeur ne peut plus
s'emanciper avec. La recursion est generique, la zone interne ne l'est pas.

L'autoritatif se replie sur 127.0.0.1:5300 -- derive de la colocalisation, pas
declare a la main. Son port devient `derive` dans meta/flux.yml : les deux lient
53 mais sur des adresses differentes.

Deux pieges. Unbound refuse d'interroger une loopback par defaut : sans lever
do-not-query-localhost, toute la zone rendait SERVFAIL. Et le plancher
/etc/hosts MASQUAIT la panne -- getent repondait, dig disait SERVFAIL. La tache
de validation du role avait raison contre moi.

client_unbound n'installant plus Unbound, son nom mentait : client_resolveur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 16:59:34 -04:00
70e57076e8 collabora : en natif, la derniere exception conteneurisee tombe
Le role lancait collabora/code dans Docker -- seule exception de la flotte au
principe « logiciel libre en natif ». Desormais : paquet coolwsd du depot amont,
systemd, derriere l'edge nginx. community.docker est retiree de requirements :
plus aucun role n'a besoin de Docker.

Eprouve avant d'ecrire, sur une Debian 13.6 reelle et sans rien installer :
apt-get install --simulate coolwsd resout jusqu'a « Conf coolwsd (26.04.3.1-1) »,
libgcc1 est fourni par libgcc-s1, et le depot CODE-deb est PLAT.

La configuration passe par un fragment systemd (--o:) : le coolwsd.xml livre,
439 lignes commentees, reste intact.

Trois outils du harnais ont pese. voute.py lisait les COMMENTAIRES : documenter
le nom d'une clef suffisait a l'exiger -- corrige, controle negatif fait. Et
verifier_intrants avait raison : une garde ecrite en deux morceaux promettait un
secret pour une console fermee ; reecrite en implication, elle dit le vrai
contrat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 16:23:35 -04:00
e6c5f9f539 instancier : refuser une machine dont la fonction n'est pas declaree
deriver_nomenclature(...) or {} avalait l'echec : une fonction absente rendait
un dictionnaire vide, et la machine entrait dans l'inventaire avec
ansible_host: None. La generation se declarait reussie ; la panne serait
apparue au deploiement, sous une forme incomprehensible.

Revele en portant serveur_ops dans les modeles : presence-web range son socle
en zone 1 et n'a pas de categorie 4. Le defaut n'est pas apparu en ecrivant le
role ni en le deployant chez patient 0 -- il a fallu le porter ailleurs.

serveur_ops ne nomme plus patient 0 dans ses defauts : un ecosysteme distrait
aurait clone le genome d'un autre, en silence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 11:42:32 -04:00
665b07ba82 serveur_ops : la difference entre une archive et une matrice
Some checks are pending
verifier / verifier (push) Waiting to run
Un ecosysteme pouvait DETENIR son genome sans savoir l'executer. Le poste
d'exploitation porte Ansible epingle, le genome clone depuis SA PROPRE forge, et
une cle SSH qui n'appartient qu'a lui. Il n'emporte ni la voute ni son mot de
passe : la structure se reconstruit depuis la forge, les secrets depuis la
sauvegarde. Deux sources qu'un meme incident n'atteint pas ensemble.

Une machine neuve traverse tout le moteur sans rien heriter, et revele ce qu'un
etat anterieur masquait. Deux defauts silencieux en sont sortis.

setops_plan_dir pointait le lien `instance` en dur : neuf roles lisaient donc le
plan d'une AUTRE instance. Le plancher de patient 0 portait les FQDN de
Chezlepro, et son edge publiait server_name forge.chezlepro.internal. La
variable suit desormais l'inventaire reellement charge.

Trois instances sur quatre declaraient les SAN d'exposition de leur edge ; la
quatrieme et le modele public ne les avaient pas. La forge de patient 0 etait
donc publiee derriere le certificat auto-signe de Debian, sans que rien ne le
signale -- `git clone` fut le premier a refuser, a juste titre. P42 le reclame
maintenant pour tout ecosysteme qui declare un edge.

Le harnais a ecrit la moitie de ce role : 5 echecs sur la piece neuve, aucun
n'empechait le code de tourner, tous la rendaient invisible a la carte, au
graphe et au lecteur. 42 preuves, 0 echec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 11:58:10 -04:00
4af1fcd0e2 frontiere : un boitier injoignable etait lu comme un boitier vide
Some checks are pending
verifier / verifier (push) Waiting to run
L'exploitant lance `make frontiere-appliquer` dans son terminal : « a creer 0, inchange
53 + 15 routes — la frontiere dit deja ce que le devis dit ». Elle etait DEJA conforme.

J'avais annonce quelques heures plus tot « 89 a creer, 0 inchange, donc les regles
heritees sont invisibles a l'API ». FAUX. L'intrant `opnsense_api_url` pointait sur
10.0.0.1, l'adresse d'avant la migration du boitier : chaque lecture rendait
{"_erreur": ...}, le plan lisait `.get("rows")`, n'y trouvait rien, et concluait au vide.
Avec CONFIRMER, on aurait pousse une politique entiere EN DOUBLE.

TROISIEME FOIS EN UNE SOIREE, meme confusion — « pas de reponse » pris pour « rien » :
  devis_placement      iterait un dict d'erreur comme une liste
  devis_underlay       declarait morts les reseaux qu'il ne joignait pas
  appliquer_opnsense   lisait un boitier injoignable comme un boitier vide
Toutes les lectures de la frontiere passent desormais par une garde qui refuse en nommant
l'hote, la cause et le piege evite. Eprouvee contre l'ancienne adresse : elle refuse.

CE QUE CA DIT DE LA METHODE : ce n'est ni le harnais ni moi qui avons trouve, c'est
l'exploitant, en lancant la commande la ou il VOIT la sortie. Mes commandes s'executent
dans ma session ; il n'en voit rien. Une commande lente ressemble a un blocage, et un
blocage a une commande lente — j'ai conclu deux fois a tort avant qu'il ne regarde.

make verifier 41/41 ; le plan reel reste « rien a faire ».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 00:56:30 -04:00
d1db332ed3 frontiere : lire ses reglages a la racine du depot de site
Some checks are pending
verifier / verifier (push) Waiting to run
`opnsense.yml` decrivait le monde physique depuis les group_vars d'un tenant. Le devis et
le GUI le cherchent desormais d'abord a la racine du depot de site, comme underlay.yml et
proxmox-hebergeur.yml, par la meme derivation depuis le symlink.

CE QUE LE MAUVAIS RANGEMENT A COUTE : l'adresse d'API de la frontiere y etait restee a
10.0.0.1 apres migration vers 10.17.0.1. `make frontiere-appliquer` restait suspendu sur
une adresse morte, sans aucun message — trouve par l'exploitant en lancant la commande
dans son terminal, apres que j'aie moi-meme conclu deux fois a tort.

DEUX LECONS, ecrites plutot que corrigees en silence :
- un objet range chez celui qui n'en est pas responsable derive sans que personne le voie ;
- mes commandes s'executent dans ma session : l'exploitant ne voit pas leur sortie. Une
  commande lente ressemble alors a un blocage, et un blocage a une commande lente. Pour
  toute ecriture longue sur du materiel, c'est a lui de la lancer.

Les anciens emplacements restent lus : un site pas encore migre continue de fonctionner.
make verifier 41/41.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 00:47:32 -04:00
caee04385b voutes : le monde physique a la sienne, les tenants n'en portent plus les cles
Some checks are pending
verifier / verifier (push) Waiting to run
Demande de l'exploitant : « l'underlay et son tenant doivent avoir chacun sa voute ».

CE QUI ETAIT FAUX. Le jeton d'API du cluster et la cle d'API de la frontiere vivaient dans
la voute de CHAQUE tenant. Patient 0 a du les recopier pour exister. Consequence : on ne
pouvait plus revoquer l'acces d'un locataire sans le revoquer pour tous — la faute des
neuf copies, appliquee aux secrets.

CE QUI EST POSE :
- `underlay.vault.yml`, chez l'hebergeur, a cote d'underlay.yml. Quatre secrets deplaces
  (jeton Proxmox, cle et secret d'API OPNsense), retires des deux voutes de tenants.
- `proxmox_api.voute()` lit l'underlay APRES le tenant, donc l'hebergeur fait foi ; un site
  non encore migre continue de fonctionner sur son ancienne voute.
- `appliquer_opnsense._voute()` n'a plus sa propre lecture : elle appelle celle du cluster.
  La reecrire aurait fait une dixieme copie le jour ou l'on refermait les neuf autres.
- Le playbook de clonage charge la voute de l'underlay APRES celle du tenant, par la meme
  derivation que proxmox-hebergeur.yml : le symlink designe deja l'hebergeur.
- `voute.py` sait que ces secrets ne sont plus attendus chez un tenant (P18).

EPROUVE SUR LE REEL, apres retrait des cles chez les deux tenants : l'API du cluster
repond, le devis de placement est conforme pour les deux ecosystemes, le devis de
frontiere se genere (86 objets), le SDN est convergent.

UNE PRECAUTION APPRISE EN CHEMIN : le premier essai a ecrit la voute EN CLAIR avant de la
chiffrer, et le chiffrement a echoue — il a fallu detruire le fichier. La sequence est
desormais l'inverse : chiffrer dans un dossier de travail, ne deposer que le resultat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 17:13:48 -04:00
9f36f08db0 underlay : la frontiere entre les deux mondes, et l'instrument qui la mesure
Some checks are pending
verifier / verifier (push) Waiting to run
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>
2026-08-22 16:31:35 -04:00
5be3fbd5e1 dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Some checks are pending
verifier / verifier (push) Waiting to run
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>
2026-08-22 14:54:00 -04:00
5bb503be45 doc : enseigner la classe de defaut, pas seulement la corriger
Some checks are pending
verifier / verifier (push) Waiting to run
L'exploitant, apres le refactor : « je n'y comprends rien ». C'est la mesure qui compte —
la regle fondatrice du depot est qu'un humain pilote sans IA, et une correction qu'il ne
peut pas expliquer ne lui appartient pas.

- L'unite « La preuve » gagne une section : le defaut le plus dangereux n'est pas
  l'erreur, c'est la COPIE. Neuf copies ne vieillissent pas ensemble, et la divergence ne
  se voit jamais de l'interieur d'une copie. Avec le cas vecu — un devis qui repondait
  CONFORME sur le mauvais ecosysteme parce que les deux avaient les memes valeurs.
- Le glossaire gagne « source unique » et « resolution d'instance » ; P39 les exige.
- Le rapprochement qui rend la chose evidente : c'est la meme lecon que
  proxmox-hebergeur.yml, ou les listes du cluster recopiees chez chaque tenant avaient
  deja diverge. Une source, pas N copies — pour les donnees comme pour le code.

Plan de recette regenere. make verifier 41/41.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:31:52 -04:00
c18debc25e resolution d'instance : une seule, partagee — au lieu de neuf copies
Some checks are pending
verifier / verifier (push) Waiting to run
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.

- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
  _frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.

Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.

LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.

CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.

P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.

make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:19:55 -04:00
bd1b897815 forgejo : apprendre SQLite, et retirer ce qui ne servait pas
Some checks are pending
verifier / verifier (push) Waiting to run
Doute de l'exploitant sur patient 0 : « je doute de la pertinence de pgsql ». Mesure
plutot que discussion.

REDIS NE SERVAIT A RIEN : le role serveur_forgejo ne le mentionne ni dans son app.ini, ni
dans ses defauts, et ne declare aucun lien. Heritage du modele `forge`. Retire du plan.

POSTGRESQL ETAIT EXIGE PAR LE ROLE : DB_TYPE = postgres en dur, resoudre_base sans
condition. Le doute etait fonde, le moteur ne savait pas faire autrement.

INTERRUPTEUR `serveur_forgejo_bd: postgres|sqlite`. En sqlite la base devient un FICHIER
sous serveur_forgejo_data. Ce que ca change ailleurs : rien. Le job de sauvegarde
`serveur_forgejo` emporte deja ce dossier ; PGSSLROOTCERT etait deja conditionne au mode
TLS ; et P35 lit desormais l'interrupteur (convention `<role>_bd`, group_vars de
l'instance puis defaut du role), donc n'attend aucune entree de registre. Une valeur
inconnue est REFUSEE au debut du role plutot que de retomber en silence sur PostgreSQL.

PATIENT 0 PASSE DE SIX A QUATRE MACHINES (Dovecot, Redis, PostgreSQL et sa VM). Sur la
machine dont tout descend, chaque service en moins est une chose de moins a defendre, a
sauvegarder et a rebatir. Et l'effet depasse patient 0 : une offre `forge` pour un petit
organisme cesse d'exiger une VM PostgreSQL.

LA NEUVIEME. En verifiant P35 sur patient 0, elle a rendu un verdict JUSTE SUR LE MAUVAIS
ECOSYSTEME : `plan = RACINE / "instance" / "plan"`, le symlink en dur. Neuvieme resolution
d'instance codee en dur en cinq jours. Ce n'est plus une serie de bogues, c'est une piece
manquante : une resolution unique et partagee, a faire en une fois et de tete reposee.

Enseigne : SQLite au glossaire (P39 l'exige desormais), et le README du role documente
l'interrupteur et ce qu'il ne change pas.

make verifier 40/40 ; make ci 40/40 ; lint et syntaxe du role verts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 13:10:05 -04:00
79461fdc38 filiation : signer, inscrire la parente, et compter les temoins
Some checks failed
verifier / verifier (push) Has been cancelled
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>
2026-08-21 12:56:29 -04:00
742bcbf4b0 genome : les quatre depots sans lesquels un ecosysteme ne renait pas
Un ecosysteme Set-OPS ne se reproduit pas depuis ses machines, mais depuis QUATRE depots :
le moteur, le plan du tenant, le depot de l'hebergeur (sa fabric) et les modeles. Perdre
les VM coute du temps ; perdre ces quatre-la coute l'ecosysteme. Rien ne les nommait.

`scripts/genome.py` les DERIVE au lieu de les declarer : le moteur est ce depot,
l'instance vient de SETOPS_INSTANCE, l'hebergeur se lit du symlink underlay.yml, et les
modeles se reconnaissent a leur FORME — des plans en sous-dossiers, aucun a la racine.

Deux criteres appris d'un faux positif : sans le second, le detecteur designait le lab,
qui porte un lien `OPS-Technolibre -> ../OPS-Technolibre` que le motif traversait. Un
lien vers un frere n'est pas un contenu.

Trois cibles : `make genome` (constater), `genome-inscrire` (ecrire la parente),
`genome-verifier` (tient-elle encore ?).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:46:42 -04:00
1f47e9bca8 glossaire : le metier n'etait explique nulle part
Some checks are pending
verifier / verifier (push) Waiting to run
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>
2026-08-21 12:15:55 -04:00
61488777ee placement : le devis validait un pont que la flotte n'utilise pas
Some checks are pending
verifier / verifier (push) Waiting to run
Remarque de l'exploitant en preparant patient 0 : « le pont ne me semble pas approprie du
tout, depuis qu'on cree des VNets pour des tenants ». Juste, et plus grave que cosmetique.

`make placement-plan` confrontait `proxmox_clone_pont` (vmbr1) au cluster. Ce n'est PAS la
que les VM de la flotte atterrissent : `instancier` pose dans chaque hote le pont DERIVE
de sa zone (le VNet du tenant), et `make creer-vm` le passe au clone en ecrasant ce
defaut. vmbr1 n'est que le repli des clones MANUELS, hors plan. Le devis mesurait donc un
objet qui ne sert pas, et ignorait celui qui sert.

SUR PATIENT 0 : avant, « pont vmbr1 existe -> CONFORME ». Apres, « reseaux VM : t29appl,
t29donn, t29fron, t29serv INTROUVABLE — passer `make sdn-appliquer` AVANT de creer les
VM ». Aucun de ses quatre VNets n'existe sur le cluster : le devis d'avant-vol declarait
conforme un tenant dont les VM n'auraient eu nulle part ou naitre.

D-80 avait pourtant ete corrigee le 2026-08-13 — la liaison de placement est noeud,
stockage et gabarit, le pont se derive. Le devis continuait de compter quatre objets et de
nommer le mauvais : une doctrine corrigee dans un document ne se propage pas toute seule
dans le code qui l'applique.

MESURE MAINTENANT : en `sdn`, les VNets derives confrontes a /cluster/sdn/vnets ; en
`switch`, les ponts du noeud retenu. Avec le geste correctif quand il en manque.

NON-REGRESSION sur l'ecosysteme de reference : ses six VNets existent, conforme, code 0.
Quatre tests (Cluster simule, aucun reseau touche), harnais 38/38.

Au passage, dans patient 0 : le commentaire annoncait « les QUATRE valeurs qui rattachent
un tenant a une fabric » — trois, et le pont n'en est pas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:39:26 -04:00
254268d5f2 devis de placement : mourir n'est pas un diagnostic
Some checks are pending
verifier / verifier (push) Waiting to run
Soir de reconstruction, VPN pas encore monte. `make placement-plan` — le devis qu'on lance
AVANT quarante minutes de deploiement — rendait `AttributeError: 'str' object has no
attribute 'get'` : dix lignes de trace Python pour dire « le nom `asgard` ne se resout pas
d'ici ».

En panne, `Cluster.__call__` rend {"_erreur": "..."} — un DICT. Le devis l'iterait comme
une liste, et un dict itere rend ses CLEFS. `Cluster.rate()` existait pour ca et n'etait
appele nulle part ici. Les quatre appels passent desormais par une garde qui nomme la
cause, l'hote interroge et le geste a tenter (resolution, VPN, jeton).

ET LE DEVIS MESURAIT LE MAUVAIS TENANT. `placement_du_tenant()` lisait `instance/` en dur :
viser patient 0 avec SETOPS_INSTANCE mesurait en silence le placement de l'instance
montee. Le verdict etait juste — pour l'autre tenant. Les deux portaient les memes quatre
valeurs, ce qui est exactement la circonstance ou l'erreur ne se voit pas. C'est la
HUITIEME resolution d'instance ou d'inventaire codee en dur trouvee en trois jours ; a ce
compte ce n'est plus une serie de bogues, c'est une piece manquante.

L'en-tete annoncait aussi « tenant instance » — le nom du lien, pas celui du tenant.

TROIS TESTS, aucun reseau touche (Cluster simule) : une panne devient un refus lisible ;
une reponse qui n'est pas une liste est refusee — c'est le cas silencieux, celui qui
franchirait la premiere garde ; le cas nominal traverse sans gene. Branches sur `make test`.

Verifie ensuite contre le cluster reel : le devis nomme « OPS-Patient0 » et confirme ses
quatre objets (asgard, TrueNAS, vmbr1, gabarit 99998).

make test OK ; make verifier 38/38 ; make ci 38/38.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:26:32 -04:00
a68b9cd10e CI : le harnais ne se declenchait que par memoire
Some checks are pending
verifier / verifier (push) Waiting to run
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>
2026-08-20 19:29:46 -04:00
f01f06df5e catalogue : la carte des services avait quatre mois de retard sur le moteur
`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>
2026-08-19 11:07:59 -04:00
98ab74d047 underlay : les tenants declares sont confrontes aux dossiers reels
Suite du filtre de portee : `underlay.tenants` nomme des DOSSIERS FRERES, et une faute de
frappe y etait invisible — le tenant disparaissait des trois devis du site, qui restaient
« conformes » sur ce qu'il en restait.

Sur un site a UN SEUL tenant — le cas de la prochaine implantation — la faute rend un
devis VIDE : une frontiere sans regle, un commutateur sans VLAN. Rien dans le mot
« conforme » ne dirait qu'on vient de dessiner le vide.

L'ecart est lisible sans toucher au materiel : d'un cote une liste de noms, de l'autre
les dossiers presents. Il se dit donc a `make underlay` (D-75). Quatre situations, quatre
messages distincts : dossier absent ; dossier sans plan/nomenclature.yml ; nomenclature
non federee (index absent, categories vide, federe: false) ; plus aucun nom qui
corresponde.

CE QU'UN GABARIT NE DOIT PAS SUBIR. Un modele decrit du materiel, pas un site deploye :
sans garde, tout modele portant un exemple de `tenants` echouerait chez quiconque n'a pas
ce dossier, et P17 deviendrait rouge sur la machine du voisin. La distinction existait
deja : modeles.py passe des reperes de tenants EXPLICITES (gabarit), le site les laisse
deriver. La verification ne s'applique qu'au second cas.

Trois tests dans test_adressage_derive.py — nom introuvable, clef absente, gabarit
epargne — avec un nom absurde pour qu'aucun test ne depende des dossiers de la machine.

make test 15 + 9 ; prouver 37 OK, 0 echec, 0 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:55:49 -04:00
3445ccb836 portee : les trois devis d'un site partagent enfin la meme regle
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>
2026-08-18 16:38:30 -04:00
577c78e659 correction : 94 lignes de commentaire perdues, pas 121
Le 121 etait le total des lignes SUPPRIMEES au diff — il comptait aussi les lignes de
valeur deplacees par le retri des clefs. Compte des seules lignes de commentaire, les
quatre fichiers de l'enregistrement de 13:48 : 129 -> 35, dont ce qui reste est l'entete
que le panneau reecrit lui-meme. Soit 94.

Rien ne change au correctif ni a sa mesure (41 -> 41 lignes, 30 -> 30 commentaires, diff
d'une ligne). Un depot qui mesure ce qu'il affirme ne garde pas un chiffre approximatif.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:06:03 -04:00
ed84494c40 GUI : le panneau d'intrants effacait la memoire ecrite du depot
Un enregistrement du panneau « Intrants de base », a 13:48, a emporte 121 lignes
d'explications dans quatre fichiers — dont celle qui disait POURQUOI la valeur qu'on
venait de changer avait ete choisie (le plan de gestion reste en 10.0.0.0/24 tant que la
frontiere ne sait pas classer un second CIDR, D-61). Ces phrases sont la seule trace de
raisonnements qu'aucun code ne redit. `safe_dump` les effacait toutes a chaque
sauvegarde, en retriant les clefs au passage.

LE DEPOT CONNAISSAIT DEJA LE GESTE JUSTE. `_ecrire_intrants_fabric` (underlay.yml) et
`_ecrire_index_nomenclature` remplacent LA LIGNE sans toucher au reste ; leur commentaire
dit meme « un safe_dump les effacerait toutes ». Les quatre fichiers d'intrants n'avaient
jamais recu ce traitement. Ce qui manquait pour l'etendre : savoir remplacer une valeur
de LISTE, qui tient sur plusieurs lignes.

`_fusion_chirurgicale`, trois regles :
- une clef dont la valeur ne change pas n'est PAS reecrite (zero bruit au diff) ;
- les commentaires internes a un bloc remplace sont conserves, jamais juges ;
- une clef absente du fichier est ajoutee a la fin, jamais inseree au hasard.

La deuxieme est assumee : une explication devenue fausse survit a la valeur qu'elle
explique. Corriger une phrase est un geste humain ; l'effacer parce qu'un champ a bouge,
non. Meme principe que la fusion des clefs posee le 2026-08-10.

EPROUVE SUR LE FICHIER REEL, pas sur un exemple : l'enregistrement de 13:48 rejoue sur la
version d'avant (tiree de git) rend 41 lignes -> 41, 30 commentaires -> 30, 0 perdu, et un
diff d'UNE ligne. Neuf tests dans scripts/tests/test_gui_intrants.py, branches sur
`make test`.

DEUX FOIS LE MEME GESTE DESTRUCTEUR, SUR LE MEME CHEMIN : le 10 aout ce panneau perdait
des CLEFS (dns_amorcage, amorcage_acces_courriel — une VM qui nait sans resolution), le
18 des COMMENTAIRES. La premiere fois avait valu une fusion, pas un test. C'est le test
qui manquait.

Non touche, et dit comme tel : les registres (bases, applications, serveurs, domaines)
passent toujours par safe_dump. Ils sont structures et n'ont pas perdu de commentaires au
meme enregistrement — a reprendre si l'un d'eux en porte un jour.

make test 9/9 sur le nouveau fichier ; prouver 37 OK, 0 echec, 0 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:05:12 -04:00
4462c13b3d P03 : la preuve comparait chaque instance a l'inventaire d'UNE SEULE
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>
2026-08-18 14:35:01 -04:00
7339cc64b4 frontiere : une frontiere ne police que les tenants de SON site
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>
2026-08-14 00:55:39 -04:00
073c5b6b69 frontiere : poster en formulaire encode — independant de la version du boitier
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>
2026-08-13 23:57:10 -04:00
e5bf7eb7b7 GUI : le « pont reseau » n'est pas un reglage de la flotte — l'intitule le dit
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>
2026-08-13 11:31:41 -04:00
3af607e8b5 placement : un devis confronte les QUATRE objets au cluster — dont le gabarit
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>
2026-08-13 10:11:25 -04:00
199783f5fd D-80 : un tenant est agnostique de son underlay, a trois cles pres
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>
2026-08-13 09:47:38 -04:00
f95e2113a4 D-78 : destination ou chemin — et le VLAN 50 pour eviter une collision
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>
2026-08-12 17:50:05 -04:00
93a92aae86 P03 : la preuve regarde TOUTES les instances — et trouve une collision d'emblee
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>
2026-08-12 17:21:09 -04:00
2c7849f6b3 P03 : la preuve du diff-vide peut enfin echouer — et elle echoue
instancier.py comparer affichait l'ecart puis renvoyait TOUJOURS 0. P03
« Diff-vide du plan » ne pouvait donc pas echouer : elle est passee au vert
pendant que quatorze hotes divergeaient du plan.

Deux appelants, deux besoins — d'ou un MODE plutot qu'un changement de
comportement : `make instancier` est une INSPECTION (voir le diff avant de
decider ; un code d'erreur y transformerait la lecture en panne), P03 est une
AFFIRMATION (« le plan reproduit l'inventaire »).

prouver sort desormais a 1, et c'est EXACT : depuis le retrait du decalage de
+10, le plan derive 10.17.x.x tandis que l'inventaire applique — et les quatorze
VM qui tournent — portent 10.27.x.x. Le depot est sciemment dans cet etat
jusqu'au renumerotage.

Une preuve rouge qui dit vrai vaut mieux qu'une verte qui ne regarde rien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:28:30 -04:00
8b4ad276ca adressage : l'index est borne [0-255] — 10.300.0.0/16 n'est pas un reseau
Rien ne bornait `index`. supernet_de(300) rendait « 10.300.0.0/16 » : une CHAINE
qui ressemble a un reseau. Elle traverse tout le moteur sans bruit et n'echoue
qu'au premier ip_network() qui la lit, tres loin de l'index fautif.

La borne est posee A LA SOURCE (valider_index), pas dans un validateur de plan :
supernet_de, base3_de et vlan_de y passent toutes, donc aucune ne peut fabriquer
une adresse invalide — d'ou qu'on l'appelle : plan, GUI, devis ou test.

Elle protege un SECOND plafond, moins visible : a l'index 255 le VLAN vaut
3550+zone, sous les 4094 du 802.1Q. Un index a trois chiffres debordait aussi la.

Garde statique ajoutee au controle de federation (P21) : elle nomme le depot
fautif au lieu de laisser l'erreur remonter d'une bibliotheque.

LE TEST A TROUVE CE QUE LA RELECTURE N'AVAIT PAS VU : valider_index n'attrapait
que TypeError et ValueError, or int(float('inf')) leve OverflowError — un infini
faisait PLANTER la garde au lieu d'etre refuse.

test_underlay_bande_basse.py -> test_adressage_derive.py (il ne parlait plus
seulement de la bande basse). 12 cas, dont les refus.

Piege de structure au passage : les nouveaux cas, ajoutes apres le bloc
`if __name__ == "__main__"`, ne s'executaient pas — le bloc tourne avant que les
fonctions suivantes ne soient definies, et le compte affichait « 7 tests » au lieu
de 12. Un harnais qui compte ses propres tests doit etre lu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:21:33 -04:00
5ace6bdb95 adressage : le decalage de +10 est retire, l'index se lit dans l'adresse
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>
2026-08-12 16:16:08 -04:00
b7239149bb P23 : la bande basse de D-77 devient une regle outillee
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>
2026-08-12 15:59:56 -04:00
77809477b2 gabarit : modeleChezlepro -> modeleSetOPS, et une porte pour l'hebergeur
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>
2026-08-12 14:36:44 -04:00
8eedeaf31f sauvegarde : le catalogue derive du groupe, et P36 le prouve
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>
2026-08-11 20:50:46 -04:00
e9b9e9b7ea forgejo : epingle 16.0.2, et le verificateur accepte la cle PRIMAIRE
Six majeures d'un coup, mais la decouverte importante est ailleurs.

LA « ROTATION DE CLE » N'EN ETAIT PAS UNE. Quatre versions, trois signataires
differents — 10.0.0 par B3B1F60A, 12.0.0 par D0A82005, 14.0.0 et 16.0.2 par
C4186DF6. Ce ne sont pas des cles distinctes : ce sont des SOUS-CLES de
signature sous une primaire stable depuis 2022 (EB114F5E...C5923710, « Forgejo
<contact@forgejo.org> »). La sous-cle 0F527CF9...0E1609E5 est bien celle qui
avait signe la 12.0.0.

D'ou une correction du verificateur : il comparait l'empreinte du SIGNATAIRE,
donc une sous-cle, et aurait echoue a chaque rotation LEGITIME — on aurait
appris a lever la garde pour avancer, ce qui est la pire chose qui puisse
arriver a un controle. Il accepte desormais la cle primaire (dernier champ de
VALIDSIG), qui survit aux rotations et refuse quand meme une cle etrangere.

FORGEJO A L'ANCRE QUE KEYCLOAK N'A PAS. forgejo.org/download publie
l'empreinte, et le binaire vient de codeberg.org : la source de confiance est
INDEPENDANTE du canal de livraison. Le projet annonce lui-meme la rotation
(« the GPG key is updated on a regular basis »), ce qui confirme qu'epingler la
primaire est le bon choix. Somme sha256 egalement publiee et verifiee conforme.

Eprouve dans les deux sens : nominal 0 ; binaire altere d'un octet 1 ;
empreinte de Keycloak appliquee a Forgejo 1 ; signature d'un autre artefact 1 ;
et Keycloak ne regresse pas apres modification du comparateur.

Verifie : versions-mesurer 0 en retard, role applique sur forge-01,
ansible-lint production sur 50 fichiers, prouver.py 35 OK (code lu sans tube).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 01:04:16 -04:00
9e4101723b preuve : P31 ignorait les scripts invoques par un role ou un playbook
En ajoutant la verification de signature, P31 a declare
`verifier_signature.py` « injoignable » — alors qu'il tourne a chaque
deploiement de Keycloak. Elle ne regardait que le Makefile et les autres
scripts, jamais les roles ni les playbooks.

Une preuve qui ignore un chemin d'appel reel accuse du code sain, et on
apprend a passer outre : le pire sort possible pour une garde.

ET J'AI POUSSE AVEC CETTE PREUVE EN ECHEC. Troisieme fois de la session, et
toujours la meme faute : `prouver.py | tail -1` rend le code de sortie de
`tail`, donc le `&&` qui suit continue quoi qu'il arrive. Le commit precedent
(verification de signature Keycloak) est parti avec 34 OK / 1 echec. Corrige
ici, et la lecon vaut plus que le correctif : ne jamais lire le verdict d'une
garde a travers un tube.

Verifie : prouver.py 35 OK, code de sortie lu SANS tube.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 00:52:21 -04:00
dea7769ff3 keycloak : verifier la signature PGP contre une empreinte epinglee
« J'ai besoin d'une confiance reelle. Keycloak est probablement l'element le
plus dangereux de cet ecosysteme. » C'est exact : il signe les jetons de TOUT
l'ecosysteme, une archive substituee la et l'identite entiere tombe.

CORRECTION D'ABORD. J'avais ecrit que Keycloak ne publie aucune somme de
controle. Faux, et l'exploitant l'a releve. Mesure : .sha1 et .md5 existaient
jusqu'a 26.6.2 puis ont disparu a partir de 26.7.0 ; le .asc, lui, est present
sur toutes les versions — et je l'avais rate, sans meme le chercher. Une somme
prouve qu'un fichier n'est pas corrompu ; une signature prouve QUI l'a produit.

ETABLI : la meme cle 861AB50E...6FD6EEBA a signe 26.0.7 (alors en production),
26.3.0, 26.6.2 et 26.7.1. NON ETABLI : aucune source independante ne publie
cette empreinte — ni keycloak.org, ni SECURITY.md, ni un fichier KEYS ; absente
de keys.openpgp.org, trouvee sur keyserver.ubuntu.com qui n'est pas une
autorite. On prouve la continuite, pas l'origine. L'ancre reste une decision
humaine — desormais ecrite, versionnee, et verifiee a chaque telechargement.

scripts/verifier_signature.py impose trois choses, chacune contre un
contournement precis : la cle publique vit DANS LE DEPOT (aucun serveur de
cles au deploiement) ; l'empreinte est EPINGLEE, donc une rotation amont
devient un echec bruyant ; trousseau JETABLE, donc le resultat ne depend pas
du trousseau personnel. Il lit VALIDSIG et compare l'empreinte du signataire
REEL — « bonne signature » seule laisserait passer une signature valide faite
par une autre cle du trousseau.

Eprouve sur cinq cas : nominal 0 ; artefact altere d'un octet 1 ; empreinte
differente 1 ; cle du depot corrompue 1 ; signature absente 1.

Ce que ca ne prouve PAS : que l'empreinte epinglee soit la bonne. Aucune
machine ne peut l'etablir ; le script garantit qu'on ne s'en ecarte plus sans
le voir.

Verifie : role applique de bout en bout sur idm-01, ansible-lint production,
prouver.py 35 OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 23:35:32 -04:00
a86a82b7ae devis : make versions-mesurer — l'ecart avec l'amont devient lisible
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>
2026-08-10 23:02:16 -04:00
a1908f2c63 raser : lire le RESULTAT de la destruction, pas son accuse de reception
Le defaut note hier est corrige. `raser` concluait au succes sur la reponse
immediate de l'API : le DELETE rend un UPID et la main tout de suite, la
destruction se fait en tache de fond, et elle peut echouer APRES. Le
2026-08-10, six VM ont ete rapportees « detruites » alors qu'elles etaient
toujours la — la tache sortait sur « VM is locked (clone) », verrou laisse par
des clonages interrompus.

Confondre « demande acceptee » et « travail fait » est le pire mensonge
possible pour la SEULE commande destructive du moteur : on croit la place
libre, on relance la construction, et rien ne se cree sans qu'on comprenne
pourquoi.

_attendre_tache() relit l'UPID, interroge l'etat jusqu'a `stopped` et rend
l'exitstatus reel. Chaque VM est annoncee detruite OU en echec, avec la cause
telle que le cluster l'a donnee ; le compte final ne ment plus.

test_raser_resultat.py fabrique la situation exacte : un faux cluster qui
accepte tout puis rend une tache terminee en erreur. EPROUVE DANS LES DEUX
SENS — avec l'ancien comportement retabli temporairement il echoue en
designant le defaut, avec le correctif il passe. Raccorde a `make test`, donc
rejoue par P02.

Meme motif que le clonage corrige une heure plus tot, dans l'autre sens : une
operation asynchrone dont on ne verifie pas l'issue. Les deux venaient du
passage a des appels d'API directs, ou plus rien n'attend a notre place.

Verifie : make test vert, prouver.py 35 OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 22:47:28 -04:00
6173d5bfe9 mtu : le 1450 de la zone n'atteignait pas les invites
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>
2026-08-10 17:54:03 -04:00
777bea8408 portabilite : Technolibre debout, six devis, et P35
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>
2026-08-10 13:36:44 -04:00
9c1c790f9d portabilite : le repli nftables survivait a la bascule et annulait tout
Le socle pose l'un de DEUX fichiers dans /etc/nftables.conf : le ruleset
derive (table setops_flux) s'il existe, sinon un gabarit de repli plat (table
setops_filter) qui n'ouvre que le 22. Ce sont des ALTERNATIVES, jamais des
couches.

Mais le rechargement est `nft -f`, qui AJOUTE sans purger, et le fichier
derive retirait setops_flux — jamais setops_filter. Un hote passe du repli au
derive gardait donc DEUX chaines input sur le meme hook, toutes deux en
policy drop. Le paquet traverse les deux : seule l'intersection de leurs
accept passait.

Symptome parfaitement trompeur : AC debout, 8443 en ecoute, regle posee et
acceptante, ICMP entre les deux VM a 0,15 ms — et step ca bootstrap qui
expire. Il a fallu lire le ruleset entier pour voir la seconde table.

Le fichier derive retire desormais les DEUX tables (le repli portait deja
flush ruleset). Pas de flush ruleset cote derive : choix du depot pour ne pas
detruire de tables etrangeres, respecte.

Ce qui l'avait rendu possible : `make reconstruire` ne generait JAMAIS les
flux. `make flux` en est maintenant la premiere etape.

Et `no_log` a masque la cause trois fois dans la journee — dont au terme d'un
deploiement de 157 taches. Les taches concernees extraient desormais le
verdict a part (ce qui a echoue, quel code, quel message) sans toucher aux
en-tetes. Une garde qui protege un secret ne doit pas emporter le diagnostic.

Verifie : TLS OK depuis infra-dns-01 vers l'AC, une seule table nftables,
13/14 hotes deployes sans echec, prouver.py 34 OK, ansible-lint production.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:24:41 -04:00
3b16b8845d portabilite : monter un SECOND tenant revele trois defauts invisibles
Les deux reconstructions de la semaine rebatissaient Chezlepro sur son propre
materiel : une preuve de reproductibilite, pas de portabilite. La vraie
epreuve est un second tenant — Technolibre, index 11, plan distinct, voute
separee, meme cluster.

1. LE CLONAGE RESOLVAIT PAR NOM. proxmox_kvm cherche une VM portant le `name`
demande ; s'il en trouve une il rend `ok` et ne clone RIEN — aucune tache
cote cluster. Or les noms courts sont VOLONTAIREMENT identiques d'un tenant a
l'autre. `backup-01` de Technolibre tombait sur celui de Chezlepro. Mesure :
id-ldap-01 et sup-01 (noms absents chez Chezlepro) passaient du premier coup,
backup-01 echouait toujours. Le clonage cible desormais par VMID, via l'API.

Deux defauts de ce correctif, trouves en le mesurant : le corps assemble en
Jinja avec `>-` rendait une CHAINE et perdait le `pool` (VM nee hors de son
pool, sans un mot) ; et l'application du gabarit de calcul expirait a 5 s,
laissant la VM aux valeurs du gabarit — 2 coeurs/2 Go au lieu du plan, en
silence. Corps en mapping YAML avec omit ; six tentatives espacees.

Et `no_log` a masque la cause au moment ou elle servait. Les deux attentes
disent maintenant ce qu'elles ont constate.

2. LE GUI DETRUISAIT DES INTRANTS. Dans ecrire_intrants, `identite` etait la
seule branche sur quatre a ecrire par-dessus le disque au lieu de fusionner.
Un enregistrement a supprime dns_amorcage et amorcage_acces_courriel. Le meme
geste sur Chezlepro aurait mange les memes cles.

3. LE VERROU DE `raser` N'ETAIT PROUVE QUE POUR UN TENANT. Le faux cluster
codait en dur les VMID de Chezlepro ; monte ailleurs, le test rendait 0 au
lieu de 2. Il fabrique desormais la collision sur le plan courant.

Verifie : prouver.py 34 OK sur Technolibre, test_raser vert sur les deux
tenants, ansible-lint production sur le playbook de clonage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 11:18:14 -04:00
ac85278366 preuve : P34 — chaque document declare son lecteur (D-74)
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>
2026-08-10 07:53:04 -04:00
91355bcdef frontiere : etanche — CONFORME sur 56 lignes, dans les deux sens
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>
2026-08-09 21:33:04 -04:00
a546b03c3a devis : sixieme instrument — « ce qui n'est pas declare est-il refuse ? »
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>
2026-08-09 21:03:28 -04:00
67565aade6 frontiere : « vers Internet » n'est plus « vers n'importe ou »
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>
2026-08-09 17:00:08 -04:00
3ef470be76 frontiere : ne declarer que ce qui existe, et reconcilier les routes
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>
2026-08-09 16:44:58 -04:00
df05d90059 frontiere : ne router que les sous-reseaux REELLEMENT attribues
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>
2026-08-09 16:07:10 -04:00
78f2f7fd6f SDN : un puits sur le supernet du tenant dans son propre VRF
« Je ne trouve pas ca normal » — l'exploitant avait raison. Pendant deux jours
nous avons attribue ce comportement a l'anti-usurpation de la frontiere, et je
l'avais consigne comme tel.

Mesure depuis DEUX points de vue : depuis le poste, quatre connexions sur
quatre s'etablissaient ; depuis l'interieur du tenant, le comportement etait
correct partout ou un VNet existe. L'anomalie ne touchait que les portions de
supernet sans VNet.

Cause : vrf_t17 portait les six /24 et un defaut, RIEN pour le reste du /16.
Une adresse non attribuee sortait du VRF, atteignait la frontiere, revenait a
l'hyperviseur dans la table PRINCIPALE — pas dans le VRF — et repartait vers
192.168.11.254, la passerelle du reseau d'ADMINISTRATION.

strophe_frr pose desormais un puits sur le supernet du tenant, moins specifique
que ses /24 donc invisible au trafic legitime. Derive du seed.

Verifie : depuis le tenant, 10.27.99.99 et 10.27.18.99 refusees, 10.27.18.21
etablie. Une machine du tenant ne peut plus atteindre le reseau de gestion par
une faute de frappe.

Reste, sans arbitrage : depuis le VLAN d'administration le connect() reussit
encore, ce trafic n'entrant jamais dans le VRF. Deux remedes possibles, dont
l'un touche la table qui porte l'administration des hyperviseurs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 16:01:24 -04:00
9cdfcc5deb bascule du plan sur le gabarit recapture, prouvee par une VM reelle
proxmox_clone_* pointent sur modeleChezlepro-travail (99998). L'ancien reste
sur le cluster — il porte encore un resolv.conf et une cle privee d'hote SSH
du reseau de fabrication ; a supprimer quand plusieurs VM seront nees du
nouveau.

Preuve par une machine, pas par lecture de config : web-dorsal-01 rase puis
recree par le chemin normal. Herite du nouveau gabarit un resolv.conf reduit a
ses commentaires, un machine-id neuf, une cle d'hote regeneree. Deploiement
complet sans echec, cinq devis CONFORME.

raser accepte --hote : les quatre verrous restent en vigueur, on ne fait que
RESTREINDRE la liste derivee du plan.

Releve, non corrige : le premier deploiement s'est interrompu sur
client_unbound, a l'instant de la bascule du resolveur vers 127.0.0.1 — sshd
fige sa resolution a l'ouverture de session (« banner exchange »). L'hote
repondait une minute plus tard et la reprise est passee integralement. C'est
une COURSE, meme famille que celles de la reconstruction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 11:50:34 -04:00
711676ca69 test : epingler SETOPS_DOMAINE dans le contrat de parametres-proxmox
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>
2026-08-09 10:26:44 -04:00
70a0698ba7 gabarit : deriver le domaine de recherche, et vider resolv.conf a la capture
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>
2026-08-09 10:25:03 -04:00
28e0c45696 P33 / D-73 : aucune collision de port entre roles co-localises
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>
2026-08-09 09:46:17 -04:00
d52f256366 P32 / D-72 : tout intrant exige par un role est fourni par l'instance
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>
2026-08-09 09:40:25 -04:00
1295eeaf4e D-71 : PKI et DNS debout avant tout, et la zone inverse manquait
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>
2026-08-08 15:29:57 -04:00
872d590031 P31 : le motif laissait passer toute cible contenant une majuscule
« 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>
2026-08-08 14:34:00 -04:00
a430edf014 make raser : la seule commande destructive du moteur, avec ses quatre verrous
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>
2026-08-08 14:22:04 -04:00
5308574730 D-70 / P31 : l'exigence de documentation devient une preuve
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>
2026-08-08 13:21:23 -04:00
c5fd3aa70b docs : tisser les devis dans les points d'entree, et corriger trois faits perimes
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>
2026-08-08 12:33:35 -04:00
a90dff7ba7 courriel : routage local par identifiant, et la livraison interne remise en marche
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>
2026-08-08 09:42:45 -04:00
4bfcf4944f devis PostgreSQL et courriel : la serie des devis de service est complete
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>
2026-08-08 08:53:49 -04:00
0cc04177fb devis des expositions : une vraie requete, depuis deux points de vue
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>
2026-08-08 07:33:49 -04:00
5fde136e9f devis des certificats : disque contre memoire, et l'AC etait expiree
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>
2026-08-08 07:20:56 -04:00
5aa5f2e479 devis d'identite : comparer le deploye au declare
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>
2026-08-08 06:55:06 -04:00
c79aae200d flux : admin devient un pair, et make ca-racine livre la racine
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>
2026-08-07 17:26:35 -04:00
cc6026641f empreintes muettes, courses de premier demarrage, collision de noms
Quatre meta/empreinte.yml declaraient leurs valeurs A LA RACINE, sans la cle
`setops_empreinte:` : collabora, nextcloud, web_dorsal, web_frontal. Le lecteur
les voyait vides et rendait {0,0,0} — en silence. collab-01 s'est retrouvee avec
1 coeur / 1 Go pour porter Nextcloud ET Collabora, et a cesse de repondre en SSH
faute de memoire. Corrigee : 4c/5632Mo. Une garde refuse desormais cette forme.

Un fichier qui existe mais ne dit rien est pire qu'un fichier absent : le repli
aurait donne des valeurs sensees.

Deux courses de premier demarrage :
- le clone rend la main avant que son .conf existe -> attente active sur l'API ;
- le verrou dpkg frappait hors de `common_packages` -> `lock_timeout` pose en
  module_defaults sur les 30 playbooks de groupe, une declaration au lieu de 30.

Au passage, j'ai failli livrer pire que le defaut : une URL coupee avec `>-`
inserait une ESPACE en son milieu. Le lint passait, la requete non.

Enfin : `proxmox_kvm` identifie une VM par son NOM. Une VM heritee homonyme lui
a fait rapporter `ok` sans rien cloner — un deploiement peut donc PARAITRE
reussi alors qu'aucune VM n'existe. Touche D-37 directement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:22:38 -04:00
ba70b80197 amorcage_acces : le role qui cree UN acces puis se retire
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>
2026-08-07 13:42:27 -04:00
7cb358dab6 instancier : emet setops_supernet, derive du seed
Keycloak ne demarrait pas : pg_hba n'autorisait que 10.11.0.0/16 pour un tenant
en 10.27.0.0/16. La valeur etait figee dans group_vars, sous un commentaire
« AJUSTER au sous-reseau reel » que personne n'a suivi.

Postfix portait la meme valeur perimee et aurait echoue plus tard sur le
courriel. Technolibre aussi (10.12.0.0/16 pour un tenant en 10.21.0.0/16).
Quatre fichiers, une seule faute, repetee parce que recopiee.

`setops_supernet` se derive comme le VMID et l'adresse ; les quatre fichiers le
consomment. Chezlepro -> 10.27.0.0/16, Technolibre -> 10.21.0.0/16, sans saisie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:33:25 -04:00
f18715bc78 deployer : l'ordre des couches, et l'autorite qui se signe elle-meme
`make deployer` triait les groupes alphabetiquement apres le socle :
`client_metrique` passait avant `serveur_step_ca`, donc exigeait un certificat
que l'autorite, pas encore deployee, ne pouvait pas avoir emis — sur l'hote de
l'AC lui-meme. `docs/couches-deploiement.yml` existe pour definir cet ordre et
dit « integrations deployees en dernier » ; `make site` le lit, `deployer` ne
l'avait jamais lu. Meme registre desormais.

Deux politiques universelles se contredisaient : `client_pki` exemptait l'AC,
`client_metrique` refuse toute exemption. L'exemption confondait « ne pas
s'enroler » et « ne pas avoir de certificat ». `client_pki` distingue les deux
chemins : bootstrap pour les autres, EMISSION LOCALE sur l'AC. L'exemption
disparait, la doctrine reste.

Effet de bord instructif : `step ca bootstrap` ecrit aussi le defaults.json qui
porte l'URL de l'AC. En sautant le bootstrap on perdait l'information sans le
voir. `--ca-url` et `--root` sont explicites pour tous les hotes.

Mesure : infra-pki-01 et infra-dns-01 entierement deployees, aucun echec,
step-ca health=ok, powerdns resout la zone souveraine, et les deux hotes portent
un certificat de l'AC interne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 23:57:01 -04:00
3786cc055b flux : l'ICMP n'a pas de port — nftables n'avait jamais demarre
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>
2026-08-06 21:47:54 -04:00
e276e76f32 dns : client_unbound universel, et un resolveur d'amorcage
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>
2026-08-06 21:41:58 -04:00
83c05b8320 premiere VM tenant : quatre defauts leves sur le chemin
`infra-pki-01` recreee pour eprouver la chaine complete. Tout ce qui derive du
seed est exact : VMID, pont t17serv sans etiquette, adresse, passerelle, pool,
et desormais le gabarit de calcul.

1. Le pare-feu est-ouest aurait enferme Ansible : `t<idx>-srv-debian`
   n'autorisait SSH que depuis la flotte du tenant, et `admin_de(nom)` etait
   collecte puis jamais utilise. IPSet `t<idx>-admin` dedie + regle sourcee
   dessus. Pas d'ajout a `flotte`, qui sert aussi LDAP, SQL et les metriques.

2. L'applicateur ne convergeait pas : Proxmox range `x/32` en `x`, et la
   comparaison litterale laissait un ecart perpetuel. `_norm()` le ferme.

3. `cloner-vm` refusait toute VM de tenant : son garde exigeait un VLAN, vide
   par construction en SDN. Accepte desormais si un pont est fourni.

4. Le clonage ignore `cores`/`memory` : la VM heritait du gabarit (2/2048) au
   lieu du plan (1/1024). Une tache les repose apres le clone.

Et le troisieme verrou de D-64 : `proxmox_clone_parefeu_interface: true`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:33:12 -04:00
d61c187dcf proxmox-fw : le DROP se pose par VM, jamais au datacenter (D-64)
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>
2026-08-06 20:03:41 -04:00
b3447d5a96 proxmox-fw : un applicateur, et un refus assume
Reconcilie les trois couches du devis : 26 IPSets, 36 groupes, affectations aux
VM. Cree, met a jour, retire — meme contrat que les applicateurs de la frontiere
et du SDN.

Il n'active JAMAIS le pare-feu du datacenter : ce reglage vaut pour toutes les
VM du cluster, y compris les 37 heritees sans regle, et le basculer couperait le
parc. L'ecart est signale a chaque execution ; la decision reste humaine.

Les 28 VM du devis n'existent pas encore : listees comme differees, pas comme
erreurs. Les objets poses sont donc inertes, ce qui rend l'application sure.

`scripts/proxmox_api.py` extrait ce que les deux applicateurs Proxmox partagent,
en particulier la recomposition du jeton dont la voute ne porte que le nom.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 19:39:33 -04:00
3ed25ff703 sdn : un applicateur qui cree, met a jour et RETIRE
Deuxieme applicateur du depot, meme contrat que celui de la frontiere. Deux
cibles : les objets de cluster par l'API Proxmox, la sortie du VRF par SSH —
`/etc/frr/frr.conf.local` n'est expose par aucune API.

Perimetre strict : seules les zones du devis et les anciens nommages listes sont
touches ; une zone inconnue est signalee et laissee intacte. Un VNet encore
branche a une VM est refuse, avec le nom des machines. L'ordre suit les
dependances : sous-reseaux, VNets, zones.

`devis_sdn.strophe_frr()` est la source unique : le devis l'affiche,
l'applicateur la compare au fichier distant.

Defaut trouve a la premiere execution : la lecture sans `sudo` echouait, un
`|| true` masquait l'echec, et un fichier present etait declare absent puis
reecrit. Trois etats distingues desormais : absent, illisible, different.

Rejeu a vide, six VRF avec leur defaut, sortie tenant fonctionnelle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 19:30:13 -04:00
917b22ccc8 sdn : emet le noeud de sortie PRIMAIRE, et trois noeuds pour t17
`proxmox_sdn.sortie_primaire` etait declare et utilise par `devis_opnsense`
pour deriver le prochain saut de la frontiere, mais `devis_sdn` ne l'emettait
jamais : une zone creee depuis ce devis n'aurait pas eu de primaire, et les deux
devis se seraient contredits. Le devis l'emet, et refuse un primaire absent de
la liste des noeuds de sortie.

Cluster aligne : t17 passe a asgard,gandalf,vishnu (primaire asgard), strophe
FRR posee sur les trois noeuds, six VRF portant leur defaut.

Note de methode : une adresse ANYCAST ne peut pas servir de source de test. Le
ping depuis gandalf semblait mort a 100 % ; la capture a montre asgard recevant
les quatre reponses — la frontiere route le supernet vers le primaire, qui porte
la meme passerelle localement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 16:12:38 -04:00
fa69ec7c0f sortie des tenants : un saut emprunte (D-62) et le NAT derive (D-63)
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>
2026-08-06 15:59:11 -04:00
081c1ac545 frontiere : un reconciliateur qui sait aussi RETIRER
`scripts/appliquer_opnsense.py` remplace les scripts jetables du bac a sable.
Il travaille dans les deux sens : ce que le devis demande et qui manque est
cree, ce qu'il ne demande plus est retire. Sans ce second sens, un devis qui
change laisse des regles mortes — elles n'ouvrent rien, mais decrivent une
politique qui n'est plus la notre.

Perimetre strict : seuls les objets marques `setops:` ou prefixes `SETOPS_`
existent pour ce script. Ce qu'un humain a pose a la main lui est invisible.

L'ordre porte une propriete : alias, creations, PUIS retraits. A aucun instant
la politique n'est plus permissive qu'avant ; si un retrait echoue on reste en
surcouverture, jamais avec un trou.

Garde de la regle 4 : sans CONFIRMER=true, aucune ecriture. Le devis doit en
outre passer P24 avant qu'une requete ne parte.

Applique sur la frontiere : 3 alias et 3 regles crees, 2 regles perimees et 2
alias orphelins retires. Rejoue, le plan est vide — convergence prouvee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 14:51:51 -04:00
510568e3b4 frontiere : l'interface d'une regle se derive de l'attachement (D-61)
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>
2026-08-06 14:39:51 -04:00
b526160cbc flux : réseau d'administration = VLAN 10, et make flux réparé
`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>
2026-08-06 13:59:11 -04:00
f8977d2d29 frontière : le nœud de sortie dérive, l'identifiant d'interface est vérifié
Le prochain saut des routes tenants ne s'écrit plus <NOEUD-DE-SORTIE-EVPN> : il
dérive. Deux déclarations doivent concorder, et c'est voulu — l'hébergeur nomme
le nœud (proxmox_sdn.sortie_primaire, propriété du cluster), l'underlay dit son
adresse SUR LE LIEN DE FRONTIÈRE. Nommer un nœud absent du lien rend le devis
muet plutôt que faux.

  route add 10.21.0.0/16 via 10.0.4.41
  route add 10.27.0.0/16 via 10.0.4.41

Le devis explique pourquoi cette adresse-là — ni l'underlay ni la gestion, la
frontière n'atteint que celle-là — et pourquoi un seul saut : une route statique
n'en porte qu'un, et deux nœuds actifs en sortie avec une seule route en entrée
donneraient un chemin asymétrique.

Identifiant d'interface vérifié plutôt que supposé : l'API des règles donne
elle-même sa liste (lan → GESTION, opt1 → TENANTS, wan → WAN). Ni le
périphérique vlan040, ni le libellé. L'intrant opnsense_if_transit: opt1 était
juste — question ouverte depuis deux jours, tranchée par la mesure.

Consigné au CHANGELOG : l'EVPN tourne (six sessions BGP, tunnels sortis de la
carte de gestion), une alarme retirée sur des VRF que je croyais inversés, et une
brèche réelle à filtrer avant la première VM tenant — un nœud de sortie expose au
tenant tout ce que l'hyperviseur touche directement, et déplacer la route par
défaut n'y suffira pas puisqu'une route connectée l'emporte.

30 preuves OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:46:58 -04:00
e6c259be4d nommage : bifrost-N, où N est le dernier octet (D-60 ; D-12 renversée)
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>
2026-08-04 20:01:10 -04:00
89f33c5b43 réseau : le VNet d'une VM se dérive, un hyperviseur a plusieurs pattes (D-55 à D-59)
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>
2026-08-04 16:20:56 -04:00
63aa79e710 underlay : l'invariant du dernier octet retrouve sa portée (D-04, D-52 à D-54)
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>
2026-08-04 14:46:06 -04:00
1bc516fd92 routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
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>
2026-08-04 14:23:57 -04:00
d645532c88 séparation des plans réseau + où vont les services de l'hébergeur (D-46 à D-48)
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>
2026-08-04 13:02:59 -04:00
3b1d9b6660 SDN : make devis-sdn — zone, VNets et sous-réseaux dérivés du seed (D-43/44, P30)
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>
2026-08-04 11:06:42 -04:00
22ef279464 authentification : chaque rôle déclare sa position, gardé par P29
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>
2026-08-03 17:05:43 -04:00
4dd3d46412 proxmox : pools créés, jeton normalisé, reliquat de voûte supprimé
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>
2026-08-03 15:52:44 -04:00
60a60b6fb1 pools Proxmox : un par tenant, dérivé de l'index (D-37, P28)
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>
2026-08-03 15:15:14 -04:00
b0e56cbfc0 intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
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>
2026-08-03 14:09:22 -04:00
e9786700aa pare-feu Proxmox : le SSH inter-nœud était perdu
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>
2026-08-03 12:04:16 -04:00
7b2c9272d7 pare-feu Proxmox : une règle par rôle source, plus aucune adresse en dur
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>
2026-08-03 11:59:58 -04:00
dbb4465602 pare-feu Proxmox : des IPSets partout où c'est possible, et rien d'inutile
Six règles par tenant portaient quatorze adresses en dur : les mots-clés
`flotte` et `edge` n'avaient pas droit à un IPSet, seuls les rôles en
avaient. `flotte` en reçoit un, `edge` renvoie à celui de nginx. 36 des 40
règles se lisent maintenant `-source +t17-…`.

Et le devis listait 28 à 30 IPSets par tenant dont la moitié n'était
référencée nulle part : un opérateur en aurait créé 58 pour n'en utiliser
qu'une douzaine. Seuls les IPSets référencés sont émis — 6 par tenant. Un
devis crée ce qu'il liste.

Restent quatre règles en liste explicite, celles dont la source est plusieurs
rôles à la fois. Aucun IPSet unique ne les couvre et Proxmox n'accepte qu'une
référence par règle ; les éclater gonflerait le devis pour un gain
discutable.

Preuves : 25 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:53:38 -04:00