MEME PATRON QUE serveur_artefacts + serveur_cache_site : un installateur, un
marqueur. Le site rend DEUX services a ses locataires pendant leur jeunesse, les
PAQUETS et le GENOME. Le premier etait declare, le second ne l etait pas — alors
que D-81 fait de cette forge l autorite dont tout ecosysteme se reproduit.
Mesure sur ops-01, premiere machine de la reconstruction de Chezlepro :
cache du SITE 10.0.33.21:3142 : OK
forge du SITE 10.0.33.11:443 : BLOQUE
Le plan du tenant declarait pourtant lire son genome a cette adresse. UNE
DEPENDANCE DECLAREE CHEZ LE CONSOMMATEUR, SANS FLUX CHEZ LE FOURNISSEUR — et rien
ne le signalait : la garde de matrice de resoudre_flux ne verifie que les paires
role->role, jamais un pair symbolique comme voisins_site.
POURQUOI UN ROLE A PART : ajouter cet ingress a serveur_forgejo aurait ouvert LA
FORGE DE CHAQUE TENANT a ses voisins. La responsabilite appartient a une machine
precise, pas au logiciel qu elle fait tourner.
Il verifie que la forge ECOUTE vraiment : sans ca il attribuerait l autorite du
genome a un port muet, et l ecosysteme venu s y reproduire attendrait sans savoir
pourquoi — ce qui est exactement arrive, quinze minutes durant.
TROIS GARDES ONT TRAVAILLE : le catalogue a refuse un role qu il ne nomme pas, la
carte a corrige ses deux chiffres, et P49 a exige la regeneration du registre.
P33 a impose partage: true — le marqueur emprunte l ecoute de serveur_forgejo.
Frontiere : 4 objets crees, 0 retire. Verifie depuis ops-01 : 443 OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
LA DERNIERE DEPENDANCE QUI SORTAIT PAR ELLE-MEME. Les collections Ansible suivent
depuis longtemps l idiome de ce depot : le CONTROLEUR telecharge une fois puis
pousse par SSH, aucun flux nouveau depuis la cible, et l artefact devient
deployable hors ligne. pip sortait encore vers PyPI, en HTTPS et par nom.
Un tenant n a NI DNS SORTANT NI 443 vers l Internet, et c est voulu. Mesure sur
ops-01, premiere machine de la reconstruction de Chezlepro :
ERROR: Could not find a version that satisfies the requirement ansible-core
Echec temporaire dans la resolution du nom
PAS LE PAQUET DEBIAN : trixie propose ansible-core 2.19.4, hors de la plage
epinglee. Y aller demanderait de deplacer l epinglage vers une version majeure aux
changements de gabarits connus, trois jours apres l avoir pose pour cause de
portabilite.
--no-index EST LE POINT, PAS UNE OPTIMISATION. Sans lui, pip resterait capable de
sortir vers PyPI le jour ou le depot local serait incomplet : la reussite
dependrait d un flux que ce tenant n a pas le droit d avoir, et une lignee
autonome deviendrait une lignee qui a l air autonome.
LE CONTROLEUR ET LA CIBLE DOIVENT PARTAGER LEUR PYTHON, et ca ne se devine pas.
pyyaml porte du C compile ; une roue cp313 posee sur un python 3.12 ne s installe
pas, et l echec accuserait le depot local au lieu de l ecart entre deux machines.
Mesure et dit ici, ou on peut encore le nommer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
La separation faite, la distribution devient sure. `serveur_ops_site` et
`serveur_ops_tenant` deposent desormais LA CLE de la voute qu'ils deposaient deja
chiffree. Le runner du site l'a recue : une seule cle chez lui, en 0600, et il
ouvre sa voute tout seul. La fabric, pas un tenant.
`false` PAR DEFAUT, ET CE N'EST PAS DECORATIF. Armer est le moment ou un humain
remet la cle — le seul geste que la reproduction exige de lui. Un defaut a `true`
armerait des machines sans que personne ne l'ait decide. Les deux roles exigent
une declaration au plan et refusent d'armer sans qu'on dise AVEC QUELLE cle :
poser un fichier vide laisserait un runner se croire arme.
ECRIRE PUIS RELIRE, contre la faute SYMETRIQUE de celle de la voute : la on
craignait un chiffre devenu clair, ici on craint une cle qui serait une voute, ou
vide — Ansible accepte un mot de passe vide et n'ouvre rien. Existence, taille et
mode mesures apres ecriture.
CE QUE CA CHANGE : le mot de passe se tapait a chaque deploiement. Un geste repete
vingt fois par semaine ne prouve rien, et il rendait tout deploiement non
interactif impossible sans blocage. Il se remet une fois, et c'est un evenement.
P32 A TROUVE LA MOITIE MANQUANTE : le role du tenant exigeait
`serveur_ops_tenant_cle_source`, absente du plan de Chezlepro. Dit avant tout
deploiement, dans les bons termes.
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
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
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>
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>
Ansible vit dans un venv isole — c'est voulu, il est epingle. Mais rien ne le
mettait sur le PATH du compte d'exploitation : `make instancier` y echouait sur
« [Errno 2] No such file or directory: 'ansible-inventory' », un message qui
accuse un fichier manquant alors que le fichier est la, deux dossiers plus loin.
Mesure du 2026-08-26, en EPROUVANT la sequence depuis le runner du site. Un
exploitant y serait tombe exactement de la meme facon — et c'est precisement ce
qu'un outil « utilisable sans IA » ne doit pas faire.
Un fragment `profile.d` plutot qu'un `.bashrc` : il vaut pour toute session du
compte, `sudo -iu setops` compris, et se retire d'un seul geste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La doctrine des runners decrit trois portees depuis le 2026-08-22 :
calculer plan -> inventaire aucune voute serveur_ops
configurer roles sur ses machines voute du TENANT <- revendiquee, jamais recue
materialiser creer/detruire des VM voute du SITE serveur_ops_site
La deuxieme ligne etait un trou. RIEN ne deposait jamais la voute d'un tenant sur
son runner. Un runner pouvait deriver son inventaire et ne rien pouvoir en faire :
chaque role qui demande un secret echouait sur son assertion, et l'echec ne disait
pas qu'il manquait un FICHIER, seulement que les valeurs etaient vides.
Decouvert en preparant la reconstruction de Chezlepro. Le runner du SITE peut
materialiser ses quinze machines ; il ne peut pas les configurer, parce que
Chezlepro a sa PROPRE autorite de certification — donc `client_pki` y reclame un
secret de Chezlepro. Le runner du site ne l'a pas, et NE DOIT PAS l'avoir : c'est
la ligne qui rend l'hebergement mutualise defendable.
`serveur_ops_tenant` est le symetrique exact de `serveur_ops_site` : un MARQUEUR,
pas un installateur. Il depose la voute `decrypt: false`, puis RELIT l'en-tete du
fichier depose — une voute dechiffree par accident est une fuite silencieuse. Le
dossier d'inventaire (`principal` ou `production`) se DECOUVRE sur la machine
plutot que d'etre ecrit.
Pourquoi un role a part et non une option de `serveur_ops` : donner sa voute a un
runner est un POUVOIR, pas un reglage. Le declarer au plan force a repondre a
« cette machine a-t-elle le droit de configurer cet ecosysteme ? » — une option
activee par defaut y repondrait a notre place.
CHEZLEPRO LISAIT ENCORE SON GENOME CHEZ PATIENT 0.
Trouve au passage, et bloquant pour la reconstruction : `serveur_ops_forge_amont`
pointait sur `10.29.16.11` — la forge de patient 0, l'amont d'avant que le site
ait la sienne. D-81 a tranche depuis. Laisse tel quel, Chezlepro se serait
reconstruit depuis un moteur perime, sur un reseau que le decoupage en zones a de
toute facon deplace.
Corrige vers la forge du site, PAR SON IP — `forge.genese.internal` appartient a
la zone souveraine du site, et un tenant ne resout que la sienne ; le certificat
SERVI porte bien `IP Address:10.0.33.11`. La racine de l'AC du site est desormais
versionnee a cote de la carte de la fabric a laquelle elle appartient, et son
chemin se DERIVE du symlink `underlay.yml` : il vaut depuis le poste comme depuis
un runner.
Le role est inscrit dans les quatre registres qui l'exigeaient — couches de
deploiement, graphe des dependances, catalogue des services, carte d'orientation.
Ce sont les preuves P08, P38 et P48 qui l'ont reclame, chacune a son tour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`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>
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>
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>
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>
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>
Toute l'API rendait 403 — ni 401, ni message de droits : jeton valide, identite reconnue,
requete rejetee. La cause etait `must-change-password` sur le compte d'administration.
Le role passait DEJA `--must-change-password=false`, corrige le 2026-08-23 sur la premiere
forge de patient 0. Forgejo 16 a ignore l'option : le compte de la forge du SITE est ne
avec le drapeau pose malgre elle. La correction d'il y a deux jours ne protegeait plus
rien, et rien ne le disait.
Le role ne demande donc plus a la creation de bien se comporter : il CONSTATE l'etat voulu
et le retablit, par une tache idempotente sans effet quand la creation a tenu parole.
Le genome vit desormais sur la forge du site : six depots, 606 commits, chaque tete
confrontee entre le poste et la forge — identique. L'ancienne forge reste comme second
remote : « une famille, pas un maitre ».
42 preuves vertes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
Mesure sur forge-01 (par l'agent invite, sans dependre du reseau) : quatre drop-ins SSH
coexistent la ou il devrait y en avoir deux. Les roles se sont appeles `chezlepro` avant
de porter le nom du moteur ; le renommage a change le fichier DEPOSE sans retirer le
precedent.
Et les paires divergent : MaxSessions 2 contre 10, MaxStartups 5:30:20 contre 10:30:60.
sshd retient la PREMIERE valeur rencontree pour chaque mot-cle et lit les drop-ins dans
l'ordre lexical — c'est donc l'ANCIEN fichier qui gagne. Une configuration qu'on croit
avoir remplacee reste aux commandes, en silence.
ssh_baseline et ssh_hardening retirent chacun le fichier de la nomenclature qu'ils ont
abandonnee, AVANT de deposer le leur, et notifient la meme validation. La liste est
declarative et ne contient que des noms reellement deposes un jour : supprimer un fichier
qu'on n'a jamais ecrit serait effacer la configuration d'un autre.
PAS ENCORE EPROUVE : les seules machines portant les anciens fichiers sont celles de
patient 0, hors d'atteinte depuis le poste. Ecrit, linte, reste a le voir agir.
42 preuves vertes, ansible-lint profil production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Une forge sert a deux choses que le meme logiciel confondait : porter le GENOME
-- meme contenu pour tous les tenants d'un site -- et heberger le TRAVAIL de ses
gens, qui est un service du tenant. La premiere se mutualise, la seconde non.
Lire le genome chez un voisin suppose de faire confiance a SON autorite. On ne
pose PAS cette racine dans le magasin systeme : `git config
http.<url>.sslCAInfo` limite la confiance a cette seule forge. Une porte, pas un
trousseau.
L'adresse est une IP : `forge.genese.internal` ne resout pas depuis un autre
tenant, et le certificat de l'edge porte l'IP dans ses SAN.
Effet de bord recherche : le genome devient disponible AVANT que la forge du
tenant soit debout. Un ecosysteme neuf n'attend plus sa propre forge pour se
remplir.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
ssh_baseline gere les cles d'administration declarees au plan, avec un `etat`
par entree : revoquer devient un changement de plan, pas une visite sur chaque
machine. Pas d'exclusive -- il effacerait la cle de cloud-init et fermerait la
flotte a tout le monde.
cloud-init reecrivait /etc/hosts a chaque demarrage et effacait le plancher de
resolution. Constate sur infra-dns-01 apres un redemarrage : six entrees
perdues, revelees deux jours plus tard par un apt update qui ne resolvait plus.
requirements.yml ne declarait pas ansible.posix ni community.docker, pourtant
utilisees. Ca marchait chez le mainteneur, pas sur un runner. Et le cache des
collections suivait l'existence du fichier au lieu de son contenu.
Enfin : deux `when` sur une meme tache, c'est un seul -- le dernier. Le
check-mode avait disparu en silence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La ligne de partage est celle des voutes. serveur_ops calcule et configure --
voute du tenant, SSH chez lui. serveur_ops_site materialise -- voute du SITE,
API de l'hyperviseur, jamais de SSH chez un tenant.
Un runner par tenant qui materialiserait mettrait la voute du SITE en N
exemplaires. Un runner unique qui ferait tout traverserait le default-deny
inter-tenant et rendrait l'emancipation impossible.
Le role depose la voute CHIFFREE et relit l'en-tete apres avoir ecrit : sans
`decrypt: false`, Ansible dechiffre la source quand il detient le mot de passe
-- constate le jour meme, 776 octets en clair au lieu de 3465. Controle negatif
fait, la garde mord.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un ecosysteme nait fils, peut rester mutualise par choix, et s'emancipe quand il
le decide. Le moteur avait rencontre ce motif trois fois sans le nommer :
client_artefacts_actif, serveur_ops_forge_externe, client_backup_cible.
serveur_ops_forge_externe etait citee par le registre des dependances ET par le
README, definie nulle part : l'exemption ne pouvait jamais s'appliquer. Elle
existe maintenant, avec un amont obligatoire.
Consequence pour les modeles : quatre n'ont pas de forge, ce ne sont pas des
lacunes mais des ecosystemes au premier age. Ils le declarent.
Reste a faire : l'instrument qui PROUVE qu'une emancipation a coupe le lien.
Sans lui, on croirait s'etre emancipe en restant dependant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
serveur_artefacts (apt-cacher-ng) + client_artefacts, integration universelle
qui s'eteint quand aucun hote ne porte le service et RETIRE la direction posee.
Chez patient 0 : colocalise sur forge-01 -- la forge est la source, du code et
des binaires.
La preuve est le mode hors ligne : paquet en cache servi en 0 o/s, paquet absent
refuse par un 503. Tant qu'internet repond, un apt update qui reussit ne dit pas
d'ou vient l'octet.
Trois lecons : apt fait heriter Acquire::https::Proxy de la valeur HTTP (d'ou
403 CONNECT denied sur les depots tiers, et smallstep injoignable) ; un service
ne doit pas dependre de lui-meme pour se reparer (l'apt update du role passait
par le cache hors ligne) ; et rediriger 2>/dev/null, c'est choisir de ne pas
voir.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le poste ne clonait que le moteur et son plan : il savait configurer des machines
existantes, pas en creer. Placer une VM demande de savoir sur quelle fabric la
poser.
Patient 0 n'a pas d'underlay a lui -- il est TENANT de SITE-Chezlepro. Son poste
porte donc les deux symlinks de D-80, et quatre depots : le moteur, son plan, la
fabric qui le porte, les modeles.
underlay.vault.yml reste hors du genome : le poste lit la CARTE du monde
physique, jamais ses cles. Mesure depuis ops-01 : `make instancier` rend un diff
vide sans aucun secret, `make underlay-plan` refuse faute de voute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Le plancher /etc/hosts ne savait nommer que ce que l'ecosysteme contient. Or un
ecosysteme enfant doit atteindre la forge dont il descend, et ce nom-la resout
vers l'adresse publique depuis l'overlay -- pas vers l'adresse du LAN, que la
frontiere bloque a juste titre.
hosts_statiques_externes accueille ces declarations. Le fichier etant regenere
integralement a chaque passage, une entree posee a la main ne tiendrait pas.
La poignee TCP disait "ouvert" sur l'adresse bloquee alors qu'aucune donnee ne
passait -- seule la livraison compte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La boucle est fermee. Set-OPS-public (avec son etiquette SIGNEE v2026.08.21),
SITE-Chezlepro, OPS-Chezlepro, OPS-Patient0 et Set-OPS-Modeles sont sur la forge de
l'ecosysteme que le moteur vient de fabriquer. La filiation reste donc verifiable depuis
l'enfant, sans rien demander au parent.
Le genome existe desormais en TROIS exemplaires vivants et independants : eregion, le poste
de l'exploitant, patient 0. C'est le seuil a partir duquel reecrire l'histoire suppose de
convaincre plusieurs temoins — la propriete recherchee, obtenue sans blockchain, comme
effet secondaire de la lignee.
LE CHEMIN A DU SE PLIER A LA POLITIQUE. Le port 3000 n'est pas ouvert depuis le poste, et
le SSH de forge-01 refuse le transfert de ports (durcissement). Versement par paquets git
deposes sur l'hote, puis pousses depuis lui a travers l'API locale — donc par les crochets
de la forge, comme n'importe quel push. Aucune regle assouplie pour la commodite.
LE COMPTE DE SECOURS NE SECOURAIT RIEN. Forgejo exige par defaut un changement de mot de
passe au premier acces et refuse toute requete d'API tant qu'il n'a pas eu lieu. Or ce role
DESACTIVE la connexion locale (SSO d'abord) : aucun chemin n'existait pour ce changement.
Le compte administrateur etait donc inutilisable des sa creation, sur toutes les forges
deployees. `--must-change-password=false` est pose ; le mot de passe vient de la voute et
tourne deja par empreinte.
Cinquieme defaut revele par le meme ecosysteme. Aucun n'etait visible sur une flotte
debout : il fallait en construire une AUTRE, et s'en servir.
make verifier 41 OK, 0 echec, 0 saute ; lint vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Quatre machines, zero echec, la forge repond (service actif, ecoute 3000, HTTP 200).
forge-01 121 taches, infra-pki-01 109, infra-edge-01 92, infra-dns-01 84.
1. UN TIERS INTERMITTENT ARRETAIT TOUT. `packages.smallstep.com` repond une fois sur deux ;
`get_url` abandonne a 10 s sans reprise. Mesure AVANT d'accuser le reseau : DNS
resolvait, la poignee TLS aboutissait, 138 Ko depuis deb.debian.org passaient en 0,09 s,
et le chemin acceptait 1450 octets en refusant 1500 — l'attendu exact en overlay. Le
reseau n'y etait pour rien. Reprises posees sur les trois telechargements du chemin
critique (serveur_step_ca, client_pki, serveur_forgejo). Une dizaine d'autres restent
sans reprise : liste dans le CHANGELOG.
2. UN `register` A ECRASE UN CHEMIN DE FICHIER. En posant la reprise, j'ai enregistre dans
`client_pki_cle`, nom que le role utilisait deja pour la cle privee. L'ecart s'est vu 70
taches plus loin : `step ca certificate` recevait un dict serialise a la place du
fichier et refusait « too many positional arguments ». Un register ecrit dans l'espace
de noms de TOUT le role.
3. LE SSO SE DECLARAIT AU LIEU DE SE DERIVER. `serveur_forgejo_oidc_actif: true` en dur
faisait cabler une source OAuth2 vers un Keycloak inexistant. Derive desormais de
l'inventaire. Quatrieme manifestation en deux jours de la meme hypothese — le moteur
supposait l'ecosysteme COMPLET — apres les intrants (P32), les bases (P35) et les
dependances causales.
AUCUN de ces defauts n'etait visible sur Chezlepro, qui porte tout et tournait deja. Ils ne
pouvaient apparaitre qu'au premier ecosysteme DIFFERENT.
make verifier 41 OK, 0 echec, 0 saute ; make test inchange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Chezlepro rase puis reconstruit SANS UNE SEULE INTERVENTION. Les sept correctifs
de la nuit tiennent sur une flotte entierement neuve. Sept devis CONFORME,
prouver 36/36, make test 0, neuf sauvegardes reussies et cinq sans objet.
LA BATAILLE CONTRE NodeName N'AVAIT QU'UNE CAUSE. icinga2 api setup ecrit
NodeName d'apres `hostname -f`. Tant que /etc/hosts placait le nom court en
premier, hostname -f mentait et le certificat devenait inverifiable. Depuis que
le FQDN est en tete, Icinga s'emet spontanement un certificat CN et SAN = FQDN.
Il n'y avait rien a forcer : il suffisait que la machine sache comment elle
s'appelle.
Le contournement par le nom court est RETIRE — il etait devenu faux des que la
cause reelle a ete corrigee.
Ce que ca enseigne : trois heures passees a corriger un symptome visible (le nom
du certificat) alors que la cause vivait deux couches plus bas et affectait toute
la flotte. Le signe qui aurait du alerter : CHAQUE correction etait effacee au
passage suivant. Un correctif qu'on doit reposer sans cesse ne corrige pas.
Dette notee : le clone de mon-01 a depasse proxmox_clone_timeout (600 s) pendant
la generation de son ISO cloud-init. Quatre clones complets simultanes saturent
le stockage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Chezlepro reconstruit depuis zero en 10.17.x.x. Sept defauts sont tombes, et
AUCUN n'etait detectable autrement — c'est tout l'argument de la reconstruction
comme preuve.
1. aucune route vers le tenant sur le poste (posee a la main, jamais persistee)
2. backup-01 exigeait l'AC d'Icinga — dependance non declaree
3. ma correction inversait les couches : REFUSEE PAR P08 avant d'entrer
4. base Forgejo a moitie initialisee (sequelle de l'arret du n°2)
5. /etc/hosts : nom court avant le FQDN -> hostname -f faux sur les 14 machines
6. API Icinga jamais activee (garde `creates:` d'api setup)
7. restic refuse tout le lot si un chemin declare manque
LE CINQUIEME DEPASSAIT ICINGA. hostname -f rend le PREMIER nom : toute la flotte
se croyait appelee « mon-01 ». Visible sur le certificat d'Icinga, mais le meme
piege attendait Postfix (myhostname), les journaux, les certificats. FQDN d'abord.
CE QU'ON NE CORRIGE PAS : icinga2 api setup REECRIT NodeName d'apres le nom court
et nomme ses certificats d'apres lui. Aligne avant, il est ecrase ; aligne apres,
les certificats portent le mauvais nom. On adopte sa convention : le depot appelle
l'API par le nom court, celui que le certificat porte.
serveur_icinga_node_name derive desormais de l'INVENTAIRE, pas d'ansible_fqdn —
un fait qui depend du resolveur et rendait « mon-01 » alors que le FQDN etait bon.
Le commentaire ecrit pour avertir qu'une sequence accolade-diese casse les
gabarits Jinja contenait cette sequence, et cassait le gabarit. Neuf hotes en
echec pour un avertissement mal redige.
Sept devis CONFORME, prouver 36/36, make test 0. Neuf sauvegardes reussies.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Objectif : supprimer le `curl -k` du rapporteur. Fait, et la maniere a ete
imposee par la mesure.
SERVIR UN CERTIFICAT STEP-CA SUR L'API EST IMPOSSIBLE. Icinga le dit lui-meme :
« Our certificate will expire soon, but we own the CA. Renewing. » Il renouvelle
tout certificat expirant sous 30 jours ; les notres vivent 24 h ; possedant une
AC, il re-emet avec la sienne et ecrase le notre a chaque demarrage. Collision
entre deux politiques de PKI, et la notre n'est pas negociable.
Deux decouvertes en chemin, toutes deux par le garde-fou `icinga2 daemon -C`
ajoute au role, qui a ARRETE le deploiement avant de redemarrer la supervision :
- cert_path/key_path/ca_path sont DEPRECIES depuis 2.8 ; les poser reveille un
chemin de code herite qui exige en plus un objet Endpoint ;
- l'identite de l'API est le CN du certificat. NodeName valait « mon-01 », donc
le SAN aussi, alors qu'on appelle par le FQDN : aucune verification n'aurait
pu reussir. NodeName est desormais aligne sur le FQDN.
A LA PLACE : Icinga garde son AC (domaine de confiance FERME, legitime) et
backup-01 verifie le pair contre CETTE AC, recuperee depuis mon-01 au
deploiement. Le pair est authentifie ; seule la racine differe.
Controle negatif — une verification qu'on ne teste pas est un ornement : avec la
mauvaise AC (step-ca), curl refuse (« unable to get local issuer certificate ») ;
avec la bonne, les neuf rapports passent.
Sous-AC step-ca ecartee : elle poserait sur l'hote de supervision une cle capable
d'EMETTRE pour n'importe quel nom, alors qu'on a choisi le sens du flux pour que
compromettre mon-01 ne donne rien.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Icinga ne surveillait rien : aucun objet Host ni Service de Set-OPS, seulement
la config Debian d'origine sur localhost.
Superviser setops-sauvegarde.service aurait reproduit le defaut du jour meme :
l'unite etait VERTE sur onze noeuds pendant qu'elle n'emportait rien. Le noeud
sait qu'il a LANCE sa sauvegarde, pas qu'elle est ARRIVEE. backup-01 evalue donc
ses depots et pousse un resultat passif par noeud vers l'API Icinga.
Trois criteres, parce qu'un seul suffit a mentir : l'instantane existe, il est
recent (26 h / 50 h), il contient au moins un fichier.
Le sens du flux est delibere : le depot parle a la supervision, jamais l'inverse
— compromettre mon-01 ne donne aucun acces aux sauvegardes.
Le ttl de 6 h fait la fraicheur : si le rapporteur se tait, Icinga perime les
services tout seul. C'est le silence qui a laisse le defaut vivre un mois.
Deux erreurs corrigees par la mesure :
- --data-urlencode refuse en Bad Request (l'API veut du JSON) ; le flux, le TLS
et l'auth marchaient, seule la charge etait perdue.
- le seuil « vide » en octets signalait a tort idm-01 (2363 o) : un export LDIF
d'un annuaire a un compte pese cela. « Vide » se mesure en FICHIERS. Et le
verdict est un AVERTISSEMENT : la machine ne distingue pas « les donnees ont
disparu » de « il n'y en a pas encore ».
Reserve assumee : curl -k — l'API presente le cert de sa propre AC, pas step-ca.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Correction : l'entree precedente attribuait le defaut a la reconstruction
from-zero. C'est faux — le commit fondateur 7476a54 (2026-07-03) disait lui-meme
« Reste : jobs Tier 1 (pg_dump/slapcat/vmail/forgejo) ». Ils n'ont jamais ete
ecrits, et infra-pki-01 a ensuite perdu son integration client_backup.
Le role qui POSSEDE la donnee dit comment la sortir : client_backup_jobs est
l'intersection du catalogue et des group_names du noeud. Un tenant qui deplace un
service emporte sa sauvegarde avec lui. On sauvegarde l'etat NON REGENERABLE :
ni zones PowerDNS ni tableaux Grafana, ils se redeploient.
L'unite qui ment est RETIREE, pas rendue bloquante : refuser le deploiement aurait
casse infra-edge-01, infra-dns-01 et mon-01, qui ne detiennent legitimement rien.
Le defaut etait le timer qui echouait chaque nuit en donnant l'apparence d'une
sauvegarde.
P36 (D-75) lit les groupes detenteurs dans le catalogue : ajouter un role au
catalogue etend la preuve du meme geste. Elle a attrape infra-pki-01 — les cles
de l'AC — corrige au plan.
Mesure hors-noeud : collab-01 64,0 MiB/272, edge-mta-01 4,4 MiB/139,
data-sql-01 1,0 MiB (pg_dumpall complet), forge-01 26,4 KiB/68,
infra-pki-01 20,1 KiB/21, idm-01 2,3 KiB/5 (slapcat). 9 hotes, 9 success.
Reste : rien ne surveille l'unite — c'est ce silence qui a laisse le defaut
vivre un mois.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La commande que j'avais donnee etait circulaire : `gpg --recv-keys <empreinte>`
demande la cle PAR son empreinte, et une empreinte est le condensat du materiel
de la cle. Le serveur ne peut rien renvoyer d'autre. Ca n'etablit rien.
Elle a tout de meme revele l'identite : Keycloak Bot <keycloak.bot@gmail.com>,
ed25519 2024-02-13, expire 2027-02-12. Mesure localement : cle AUTO-SIGNEE
uniquement, aucune certification tierce.
L'ancre reelle : keycloak.org/keys publie
861ab50e8cc6611fb6bc01a6b8f12ea26fd6eeba, identique a l'epinglage. Canal
DISTINCT de github.com qui livre l'archive — la propriete qu'avait Forgejo et
qui manquait ici.
Deux reserves consignees dans le role plutot que tues : la page decrit la cle
comme servant aux artefacts Maven, et l'ancrage vaut ce que vaut le controle de
keycloak.org (DNS + TLS).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
« 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>
Comme pour Nextcloud, ce n'est PAS une migration : `id-sso-01` reste a 26.0.7
jusqu'a sa prochaine reconstruction. L'epinglage decrit ce qu'on INSTALLE.
VERIFICATION PLUS FAIBLE QUE POUR NEXTCLOUD, ET IL FAUT LE DIRE. Keycloak ne
publie AUCUNE somme de controle a cote de son archive sur GitHub — ni .sha1
ni .sha256, verifie. On ne peut donc pas comparer a une reference de
l'editeur. Ce qui a ete controle a la place :
- flux gzip valide de bout en bout (`gzip -t`, pas seulement l'en-tete) ;
- l'archive contient bien bin/kc.sh, lib/quarkus-run.jar et version.txt ;
- version.txt declare « Keycloak - Version 26.7.1 » — le contenu dit la
meme chose que le nom du fichier.
C'est un controle d'integrite et de coherence, pas d'authenticite. La
difference merite d'etre nommee plutot que noyee dans un « verifie ».
`make versions-mesurer` : il ne reste que Forgejo, 10.0.0 contre v16.0.2 —
six majeures, qui demandent de lire les notes de version avant, meme pour une
construction de zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Set-OPS installe desormais 34.0.2 aux prochaines constructions. Ce n'est PAS
une migration : le modele du depot est de reconstruire, pas de mettre a jour
en place. `collab-01` reste donc a 34.0.1 jusqu'a sa prochaine reconstruction
— l'epinglage decrit ce qu'on INSTALLE, pas ce qui TOURNE.
Archive verifiee contre la somme sha256 officielle avant d'entrer au cache, et
l'ancienne retiree. `make versions-mesurer` rend maintenant « a jour » pour
Nextcloud ; restent Keycloak (26.0.7 -> 26.7.1) et Forgejo (10.0.0 -> v16.0.2),
qui meritent chacun leur propre decision — six majeures de Forgejo ne se
changent pas au passage.
Note pour plus tard : le role INSTALLE mais ne MET PAS A JOUR. L'extraction
porte `creates: {{ racine }}/occ`, donc sur un hote deja installe un changement
de version ne reextrait rien. C'est coherent avec la doctrine de
reconstruction, mais il faut le savoir : sur une flotte en place, l'epinglage
et le deploye divergent jusqu'au prochain rasage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Inventaire mesure de ce qu'une reconstruction de tenant telecharge — ~1,5 Gio :
Nextcloud 230 Mio, image collabora/code 471 Mio (Docker Hub), Keycloak 140,
Forgejo 101, oauth2-proxy 18, plus les paquets apt (Debian + Grafana +
smallstep + Icinga) sur 14 hotes.
Les quatre archives sont EPINGLEES EN VERSION et vont chacune sur UN SEUL
hote. Les retelecharger a chaque reconstruction est un gaspillage et une
dependance de plus sur le chemin critique — un serveur tiers lent a deja fait
tomber un deploiement le 2026-08-09, sur le binaire Forgejo precisement.
POURQUOI POUSSER PLUTOT QUE SERVIR UN CACHE. L'exploitant proposait son poste
comme cache HTTP ; l'intention est juste mais elle butait sur ce qu'on avait
ferme le matin meme : les regles sortantes visent !SETOPS_INTERNES, donc une
VM de tenant ne peut plus atteindre le poste. Servir un cache aurait exige de
ROUVRIR un flux vers le plan d'administration.
L'inversion evite le probleme entier : le controleur telecharge dans son cache
(~/.cache/setops, garde par un stat), puis pousse par le canal SSH qui existe
deja. Aucun port, aucun service, aucune regle, aucun couplage. Et ces
artefacts deviennent deployables HORS LIGNE une fois le cache rempli.
Ce que ca ne couvre pas, et qu'il faut nommer : l'image collabora/code, seule
entorse a la doctrine « zero Docker » du depot — elle merite sa propre
decision, pas un contournement discret ; et les paquets apt, dont le cache a
sa place cote HEBERGEUR, partage entre tenants.
Et une mesure qui a contredit mon hypothese : le .zip de Nextcloud pese
271 Mio contre 230 pour le .tar.bz2. Il telecharge PLUS pour decompresser
moins lentement. Le changement de format attend une mesure, pas une intuition.
Verifie : cache rempli (491 Mio, 4/4), ansible-lint production sur 79 fichiers,
prouver.py 35 OK, plus aucun get_url n'ecrit sur la cible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Technolibre remonte depuis zero une seconde fois — 14 hotes, 2588 taches ok,
0 failed. Deux defauts trouves en chemin.
LE JETON KEYCLOAK VIVAIT 60 SECONDES. Il etait pris dans politique-mdp.yml —
le 2e des neuf fichiers du role — et reutilise jusqu'au 8e. Entre les deux,
six fichiers de travail dont groupes-ldap.yml et ses reprises espacees de
15 s. Sur une construction NEUVE le temps depasse la minute : 401. Sur un
REJEU tout est converge, ca va vite, ca passe.
D'ou les deux echecs du matin, chaque fois suivis d'un succes au rejeu, qui
donnaient l'illusion d'une course au demarrage de Keycloak. J'avais ecrit
alors ne pas avoir de mesure qui le prouve — c'etait juste, et la cause etait
l'AGE du jeton. jeton-admin.yml en prend un frais la ou on s'en sert.
GRAFANA : UN ECHEC TRANSITOIRE RENDU ILLISIBLE. Premier demarrage, apres 67 s
de migrations : « failed to create admin user: no such column: uid », alors
que la migration qui ajoute cette colonne etait journalisee comme reussie.
Base neuve : tout remigre, service actif, colonne presente. L'incident ne
s'est pas reproduit et Chezlepro ne l'a jamais eu — je n'ai donc PAS corrige
la cause, faute de l'avoir reproduite. J'ai corrige ce qui la rendait
indechiffrable :
- Restart=on-failure venait du paquet SANS RestartSec, donc 100 ms : six
relances en une seconde, chacune rejouant les migrations sur la meme base
SQLite. Un echec unique se presentait comme un desastre. RestartSec=10 ;
- la rotation du compte de secours echouait cinq fois sous no_log en
annoncant « the output has been hidden », alors que la vraie cause etait
ailleurs et lisible : le serveur ne demarrait pas. Une attente explicite sur
le port precede desormais la CLI, avec un message qui renvoie a la PREMIERE
erreur du journal.
Troisieme fois dans la journee que no_log masque la cause au moment ou elle
sert : une garde qui protege un secret ne doit pas emporter le diagnostic.
Verifie : 7 devis sur Technolibre — MTU, identite, certificats, PostgreSQL,
courriel, frontiere CONFORME ; prouver.py 35 OK ; ansible-lint production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>