# CHANGELOG — Set-OPS ## 2026-09-10 (16) — La frontiere journalise a nouveau, et une garde veille cette fois Doctrine posee par l'exploitant : *des lors que le site a son Loki, la journalisation de la frontiere et des hyperviseurs doit y etre dirigee.* 46 681 lignes recues de la frontiere en 30 min site : 68 services, tous OK ### Ce qui etait casse, et depuis quand La destination syslog d'OPNsense pointait `10.17.20.11:3100` — une adresse de **TENANT**, sur le port de Loki, en **UDP**, ce que Loki ne sait pas lire. La VM a ete rasee ; une cible morte a arrete TOUTE la journalisation d'OPNsense pendant dix jours. La destination a ete ETEINTE pour reparer, et jamais remplacee. Deux fautes en une : la frontiere ne journalisait plus nulle part, **et** sa cible etait chez un locataire — ce que D-87 refuse dans l'autre sens. L'intention, elle, etait juste : les bonnes facilites, `info` et au-dessus. On l'a REPRISE telle quelle et change uniquement la destination — ces choix avaient ete faits, ce n'etait pas a nous de les redecider. ### Un traducteur, parce que Loki ne parle pas syslog `loki.source.syslog` dans l'Alloy qui tourne DEJA sur le collecteur — un second agent n'aurait servi qu'a tenir deux configurations en phase. Port 1514 et non 514 : au-dessus de 1024, donc ecoute sans privilege. *Un collecteur qui aurait besoin des droits du systeme pour entendre un equipement serait un mauvais echange.* ### Une regle de relabel sans garde n'ignore pas : elle EFFACE Trois quarts d'heure sur un symptome absurde — la configuration deposee portait `host = "bifrost-1"`, visible a l'oeil dans le fichier, et le flux arrivait sans etiquette. rule { source_labels = ["__syslog_connection_ip_address"] target_label = "host" } # sans regex Sans `regex`, la regle correspond **toujours** — meme quand sa source n'existe pas — et pose `host = ""`. Loki jette une etiquette vide, et celle de l'ecouteur disparaissait avec elle. Les deux autres regles etaient gardees ; celle-la ne l'etait pas. Et la verification finale a corrige une seconde erreur, la mienne : `label/host/values` rendait une liste en CACHE. La requete directe `{host="bifrost-1"}` rendait bien un flux. *Un index qui ne montre pas une chose ne prouve pas qu'elle n'existe pas.* ### La garde, qui manquait depuis l'incident `journaux-frontiere` demande a **Loki ce qu'il a RECU**, pas a la frontiere ce qu'elle croit avoir envoye — la destination est le seul juge, et un emetteur qui parle a un trou noir se porte tres bien. **La fenetre est DERIVEE du debit observe**, pas choisie : ~1 500 lignes/minute mesurees. Une frontiere qui filtre ne se tait jamais dix minutes. Trois etats, tous eprouves sur une copie : frontiere muette rc=2 « LA FRONTIERE NE JOURNALISE PLUS » Loki injoignable rc=3 « la sonde ne peut rien affirmer » debit normal rc=0 « 973 ligne(s) recue(s) en 10m » Le `3` compte autant que le `2` : *« je ne peux rien affirmer » n'est pas « c'est casse »*. Une sonde qui confondrait les deux accuserait la frontiere d'un silence qui serait le sien. ## 2026-09-10 (15) — Une source vide n'est pas « tout le monde » Les journaux de la fabric, et le defaut de classe que leur mise en place a revele. journaux 10 hotes dans Loki (7 VM du site + 3 hyperviseurs) site 67 services, tous OK (51 avant) ### Metriques TIREES, journaux POUSSES — et ce n'est pas un caprice Un scrape part du collecteur et doit REVENIR : il exige un chemin symetrique, que la route par defaut gelee (D-57) interdit. Un push part de la source et n'attend qu'un accuse : la route SPECIFIQUE vers les zones du site suffit — celle qu'on venait justement de completer. ### Trois trous dans les generateurs, du meme jour **Le devis de la frontiere ne connaissait `fabric` qu'en DESTINATION.** Un flux entrant depuis la fabric tombait dans « rien d'autre n'entre » et n'emettait AUCUNE regle, sans rien dire. Meme forme que le trou des integrations universelles, trouve le matin meme. **La regle etait sur la mauvaise patte.** D-61 fait raisonner le devis en ARRIVEE ; la regle etait posee sur l'interface de la DESTINATION. `opt7` — la patte face a la fabric, libellee PROXMOX — manquait a la table des zones, qui ne recensait que le site. Elle y est, et l'interface se derive desormais du reseau ou vivent les hyperviseurs. **Et le plus grave : `fabric` etait un mot RECONNU mais NON RESOLU.** Le generateur de pare-feu d'hote l'acceptait comme valide, ne trouvait aucune adresse, et une source vide produit une regle sans `saddr` : tcp dport 3100 accept # ouvert a tout le monde *Un mot reconnu mais non resolu est pire qu'un mot inconnu* : celui-ci serait refuse a la validation, celui-la produit une porte grande ouverte qui a l'air d'un flux precis. ### La classe entiere, refermee Le defaut n'etait pas propre a `fabric`. **Toute** paire nommant un ensemble et ne resolvant rien ouvrait le port. Trouve en lisant les fichiers generes : tcp dport 5665 accept # serveur_icinga, sur le mon-01 d'un TENANT L'API de supervision ouverte a tous, parce que `pair: serveur_backup` ne resout rien — cet ecosysteme depose son etat chez le site et n'a pas de depot a lui. `expositions` et `externe`, EUX, veulent bien dire « tout le monde » : ce sont des services publies, et leur ouverture est une INTENTION. Ailleurs : **pas de source, pas de regle** — et le generateur le DIT, parce qu'un flux tu en silence est une porte qu'on croit fermee. Trois regles se sont refermees. Chacune verifiee AVANT d'appliquer : | regle | verdict | |---|---| | `site-forge-01:3000` | rien n'ecoute — la forge sert 443 | | `mon-01:5665` (tenant) | deux autres regles couvrent les pousseurs | | `site-mon-01:3000` | **Grafana ecoute** — a corrige avant de fermer | ### Grafana : `edge` ET `admin` Chez un tenant, le nginx d'edge termine le TLS : `edge` resout, c'est le seul chemin. Le SITE n'a PAS d'edge — chaque service s'y sert lui-meme. `edge` n'y resolvait donc rien, et la console etait ouverte a l'Internet PAR ACCIDENT, sous un commentaire qui parlait d'un edge inexistant. Ajouter `admin` rend explicite ce qui etait accidentel. Et la mesure a montre autre chose : **Grafana etait deja injoignable depuis le poste** — la frontiere ne laisse pas passer le 3000. La regle ouverte ne servait a rien, sauf a rester ouverte si la frontiere s'ouvrait un jour. *Le site a donc une console deployee et sans chemin d'acces.* Ce n'est pas corrige ici, mais c'est dit. ## 2026-09-10 (14) — Le site surveille enfin sa fabric La supervision du site voyait ses sept VM et rien d'autre. Mesure de depart, depuis `site-mon-01` : les trois hyperviseurs, les neuf pattes de la frontiere et **sa propre passerelle par defaut** rendaient tous 100 % de perte au ping. 16 hotes UP / 16 dont 9 pattes de frontiere en controle ACTIF 11 cibles Prometheus dont 3 hyperviseurs, job `fabric` separe ### Le partage : ce qui peut porter un agent, et ce qui ne le peut pas **Les hyperviseurs** portent `node_exporter` — D-48 l'autorise. Chacun expose 6 600 a 7 900 lignes de metriques, dont **1 005 unites systemd avec leur etat** : soit exactement ce que la sonde `sante` mesurait, plus la charge, le disque et l'horloge. **Ils n'entrent PAS dans le socle**, et c'est le point delicat. Un hyperviseur Proxmox n'est pas une VM de la flotte : lui appliquer `serveur_durci` reecrirait son pare-feu, son SSH et ses sysctl — sur la machine qui tient tout le reste. Ils vivent dans leur propre groupe, hors de `GROUPE_SOCLE` et de `hotes_actifs`. **La frontiere** n'accueille aucun agent : controle ACTIF, une entree par PATTE. La frontiere est un seul boitier, mais chaque zone depend de SON interface — une interface eteinte coupe une zone pendant que les autres vont bien, et on en a deja vu (les routes creees `disabled` le 2026-09-02). Un ping vers une seule adresse dirait « la frontiere est debout » et manquerait ce cas. ### On TIRE, on ne pousse pas — et c'est la route gelee qui le decide J'avais propose du passif, et il avait ete valide. **La mesure a dit non** : asgard -> 10.0.36.11 via 192.168.11.254 (le routeur du site) route par defaut via 192.168.11.254 GELEE (D-57) Un hyperviseur envoie vers un routeur qui ne connait pas les reseaux du site : le porteur de sante y expirait en 20 s. Le remede evident — router 10.0.0.0/8 par la frontiere — touche la route par defaut d'une machine EN SERVICE, ce que D-57 interdit. On tire donc, dans le sens que la frontiere route deja. ### Le vrai defaut : une liste qui n'a pas suivi Le mecanisme de routage EXISTAIT sur `vmbr0`, avec un commentaire du 2026-08-26 tenant exactement le raisonnement qu'on venait de refaire. Sa liste s'arretait a `10.0.34.0/24` : le site declare 6 zones (31 -> 36) les hyperviseurs 4 routes (31 -> 34) Les zones `sauvegarde` (35) et `supervision` (36) sont nees, **les routes n'ont pas suivi**. Le symptome ne ressemblait pas a une route manquante : il ressemblait a un pare-feu, puis a un probleme de reseau chez l'exploitant. **`make routes-fabric-etat`** compare desormais TROIS choses : les zones declarees, les routes declarees dans `/etc/network/interfaces`, et les routes vivantes dans le noyau. Le cas le plus traitre est le troisieme — *vivante mais non declaree* : tout fonctionne, la supervision est verte, et la panne attend la prochaine maintenance. Une garde qui ne comparerait que le vivant ne le verrait jamais. Eprouvee dans les deux sens. ### Deux defauts trouves en construisant **`bifrost-2` est un nom reserve, pas un boitier.** L'underlay le disait — « pour que les noms soient reserves » — mais en PROSE, illisible par le moteur. Le surveiller aurait donne deux CRITICAL permanents pour un equipement absent. Il porte desormais `etat: reserve` ; le jour ou le second boitier arrive, retirer la cle le fait entrer dans la supervision. **Un service passif n'existe que pour un hote qui peut POUSSER.** Les hyperviseurs sont dans `client_metrique` — c'est par ce groupe qu'on leur deploie node_exporter — et la derivation en tirait `asgard!metriques`. Icinga refusait la configuration ENTIERE. Meme en definissant l'hote, le service serait reste UNKNOWN pour toujours. *L'appartenance a un groupe sert deux choses qui ne coincident pas toujours : a qui l'on deploie, et de qui l'on attend un rapport.* La seconde se lit sur `client_sante`, et nulle part ailleurs. ### Les commutateurs restent dehors Choix de l'exploitant, coherent avec D-48 : ils sont hors flotte, et rien ne les rend interrogeables sans leur ouvrir un acces qu'on ne veut pas leur ouvrir. ## 2026-09-10 (13) — D-88 : le noeud du gabarit, point unique de la REPRODUCTION Question de l'exploitant : *« le modele vit sur vishnu, les clones sont sur asgard — qu' arriverait-il si vishnu tombait ? »*. Mesuree, la reponse se coupe en deux. **Les donnees survivent.** pool CephNVMe size=3 min_size=2 OSD sur asgard, gandalf, vishnu base-9006-disk-0, base-9006-disk-1 repliquees L'image reste lisible avec un noeud en moins, et les quatorze VM d'un ecosysteme tournent ailleurs sur leurs propres disques Ceph : elles ne s'apercoivent de rien. **La reproduction, non.** La configuration du gabarit porte le nom du noeud dans son chemin meme — `/etc/pve/nodes/vishnu/qemu-server/9006.conf` — et le clonage appelle `nodes/vishnu/qemu/9006/clone`. Noeud eteint, API muette, **aucune VM nouvelle ne peut naitre**. Or la reproduction est ce que ce depot existe pour garantir. ### La decision est d'ASSUMER la dependance et de la rendre COURTE Pas de la supprimer. Depuis que le disque du gabarit vit sur un stockage partage (migration du matin), la remise en route est un **deplacement de fichier de configuration** — quelques minutes, aucun mouvement de donnees — suivi de la declaration `gabarit.noeud`. Sur un stockage local, il aurait fallu recopier 16 Go ou refabriquer le gabarit. *Un benefice de la migration sur Ceph qu'on n'avait pas cherche : elle a raccourci une panne qu'on n'avait pas encore nommee.* ### Trois endroits, parce qu'un seul ne suffit pas - **D-88** dans les decisions : la dependance est nommee et son perimetre borne ; - **`runbooks-exploitation.md` §7** : la manoeuvre, dans l'ordre, avec ce qui casse si on l'inverse — deplacer sans declarer laisse `gabarit_etat` en ecart, declarer sans deplacer fait echouer le clonage ; - **le plan du site**, dans le bloc `gabarit` lui-meme : c'est la que l'exploitant lit `noeud: vishnu`, et c'est donc la que l'avertissement doit vivre. ### Ce qui n'est PAS fait, et qui est dit **Rien ne MESURE cette dependance.** `gabarit_etat` compare le declare au reel ; il ne demande pas si le noeud du gabarit heberge autre chose que le gabarit. Deux remedes de fond restent ouverts : deplacer le gabarit la ou vivent deja les VM (ce qui ne supprime pas le point unique mais cesse d'en avoir DEUX), ou une garde qui refuse quand la reproduction depend d'un noeud qui ne porte rien d'autre. *Une dependance qu'on documente sans la mesurer reste une dependance qu'on decouvrira au mauvais moment.* ## 2026-09-10 (12) — Un fichier vide existe, et une sonde pour les correctifs ### La garde « fichier entier » — et pourquoi il a fallu DEUX corrections Un telechargement interrompu laisse un fichier de zero octet, **qui existe**. Toutes les gardes de ce depot demandaient *« ce fichier est-il la ? »* : - name: Cette ressource est-elle deja recuperee ? ansible.builtin.stat: ... - name: Telecharger la cle when: not ..._present.stat.exists `infra-mail-01` a garde une cle smallstep de **0 octet** apres l'epreuve hors ligne. Treize machines portaient 1022 octets, elle portait le vide — et `apt` refusait le depot. **La premiere correction n'a pas suffi.** Ajouter le controle de taille a bien fait s'executer la tache (`ok: [infra-mail-01]`) — et le fichier faisait **toujours 0 octet au passage suivant**. `get_url` sur une destination existante emet une requete CONDITIONNELLE : l'amont repond « non modifie », le module rend `ok`, la ruine reste. *Le play etait vert et ne reparait rien.* Il faut donc **effacer avant de redemander**. `state: absent` ne mord que sur un fichier vide : une cle valide n'est jamais retiree. Controle negatif : fichier vide volontairement, role rejoue → **0 → 1022 octets, 0 erreur apt**. Cinq roles portent le patron complet. *Cette etape intermediaire est la lecon de la journee en miniature : un `failed=0` ne dit pas que quelque chose a ete fait.* ### La sonde `correctifs` — 23e sonde Set-OPS **desarme** `unattended-upgrades` (masque sur les vingt et une machines) et applique les correctifs par `upgrade: full` au passage du socle. Choix defendable — un minuteur de fond qui se dispute le verrou `dpkg` avec un deploiement est un tirage au sort, et il a fait decrocher `infra-mail-01` d'une reconstruction entiere le matin meme. Mais rien ne disait **quand le geste etait du**. Vingt-deux sondes, aucune sur le retard de securite : une flotte pouvait deriver des mois en restant verte. Elle mesure les paquets en attente venant d'un depot de SECURITE, **et depuis quand** — le retard seul ne dit rien, c'est sa duree qui transforme un correctif publie en exposition acceptee. Trois etats eprouves sur une copie : `rc=0` a jour, `rc=1` des le premier correctif, `rc=2` au-dela du seuil. **Deux choix dits franchement.** Elle ne lance PAS `apt-get update` : une sonde qui rafraichit l'index toutes les quinze minutes deviendrait la cause de la panne qu'elle surveille. Et le seuil de 72 h est un CHOIX D'EXPLOITATION, pas une derivation — les autres seuils se deduisent d'un mecanisme (le certificat vit 24 h, donc on alerte a 6 h) ; ici le mecanisme est un geste humain, il n'y a rien a en deduire. ### P64 refusait une declaration correcte Declaree dans `common_packages`, la sonde etait **invisible d'Icinga** : la derivation croise les GROUPES, et ce role n'en est pas un. Pire, `client_sante` l'aurait retiree comme orpheline au passage suivant — la garde ecrite le matin meme. Mais la declarer dans `serveur_debian` faisait echouer P64, qui exigeait declaration et depot dans le MEME role. Or `serveur_debian` et `serveur_durci` sont des roles de **declaration pure** : ils portent `flux.yml`, `authentification.yml`, `supervision.yml`, et pas une tache. Le travail est fait par les roles que leur playbook applique. **Une garde qui force a contourner ce qu'elle protege est un defaut.** P64 suit desormais le playbook du groupe : ce qu'il applique compte comme depose. Elle ne s'affaiblit pas — elle apprend ou le depot a le droit de vivre. Controle negatif refait : depot desactive → refus, restaure → 65 OK. ### Etat mesure sonde `correctifs` 21 / 21 machines vertes prouver 65 preuves, 23 sondes, 0 echec ansible-lint 0 defaut ## 2026-09-10 (11) — Le trou reste ouvert derriere la porte qu'on croyait fermee Troisieme reconstruction complete de Chezlepro : **32 minutes, un seul echec — le mien**, introduit par le passage au cache et revele par la naissance. clonage 3 min 52 placement {'asgard': 14} `fatal:` dans le journal 0, pas meme un ignore servi par le cache +963 Mo tire de l'Internet +183 Mo -> 81 % servis localement ### `get_url` IGNORE la configuration d'apt Le mandataire pose dans `/etc/apt/apt.conf.d/` ne vaut **que pour apt**. Les six cles de signature sortaient donc TOUJOURS en direct, malgre tout le travail sur le remap. Et ce n'etait pas theorique. Depuis `collab-01` : en direct grafana 200 collabora TIMEOUT via le cache collabora 200 La route directe vers Collabora ne passe pas depuis cette zone. Trois cles sur quatre avaient reussi PAR CHANCE — parce que leurs fournisseurs, eux, etaient joignables. Le meme geste corrige les deux : plus rien ne sort, et la machine qui n'avait pas de route en trouve une. ### Pourquoi seule une NAISSANCE pouvait le montrer Mes deploiements de convergence rendaient `failed=0` — parce que les cles etaient **deja sur disque** et que la tache etait sautee. Le defaut existait depuis le premier commit du remap, invisible a tout deploiement sur une flotte existante. C'est l'argument de la reconstruction depuis zero, applique a moi-meme : *un correctif qu'on ne verifie que sur une machine deja construite n'est pas verifie.* ### La derive s'est effacee toute seule `apt-cacher-ng` tournait encore sur `forge-01`, que plus aucun plan ne declarait. Je proposais de l'arreter a la main. La reconstruction l'a fait : apt-cacher-ng : absent paquet : 0 sondes : les 4 legitimes **Ce qui n'est pas au plan n'existe pas apres une naissance.** La propriete centrale du depot, verifiee sur un cas qu'on n'avait pas provoque pour elle. ### Etat mesure 14 / 14 machines 0 source en HTTPS direct 14 / 14 machines 0 erreur `apt-get update` prouver 65 OK, 0 echec ansible-lint 0 defaut sur 79 fichiers ### Les trois reconstructions de la journee 1re (matin) 3 echecs 1 h 08 2e (confirmation) 0 echec 34 min 3e (avec le cache) 1 echec 32 min — le mien, corrige ## 2026-09-10 (10) — Plus rien ne sort chercher ses paquets, et un tenant de moins a nourrir Deux mouvements d'une seule doctrine : **le site fournit tout ce dont un tenant a besoin pour venir au monde.** ### 1. Les depots tiers passent enfin par le cache Set-OPS tire ses paquets applicatifs de fournisseurs qui ne publient **qu'en HTTPS** — Grafana, Icinga, Smallstep, Collabora. `apt-cacher-ng` ne relaie pas un tunnel, et le socle pose donc deliberement `Acquire::https::Proxy "DIRECT"` : chaque machine sortait elle-meme sur Internet. Mesure du matin : 58 references de depot, quatorze machines sortant chacune de son cote. Le remede tient en deux moities, et une seule ne sert a rien : - le cache **DECLARE** un `Remap-*` par fournisseur — le client demande en `http://`, le cache va chercher en `https://` ; - chaque role **DEMANDE** en `{{ ..._depot_schema }}://`, qui vaut `http` des qu'un cache d'amorcage est declare, `https` sinon. *Degrader, jamais deviner.* Le TLS n'est rompu nulle part : il est **termine au cache**, qui est notre machine. Et l'integrite ne vient pas du transport mais des signatures du depot — le raisonnement deja tenu pour `deb.debian.org` depuis toujours. **Trois choses que la mesure a apprises :** **Le remap appartient au cache qui SORT.** Pose aussi sur le cache du tenant — chaine vers celui du site — il tentait le HTTPS *a travers* son amont, ce qui exige un `CONNECT` que l'amont ne fait pas : remap sur le cache du tenant (chaine) -> 503 remap sur le seul cache du site -> 200 Un cache qui relaie n'a rien a remapper : il passe la demande a qui sort. Meme forme que la filiation elle-meme. **`apt_repository` AJOUTE, il ne remplace pas.** Passer une source de `https` a `http` y ecrivait une SECONDE ligne ; apt interrogeait les deux, et l'ancienne sortait toujours. Le symptome le disait sur les quatorze machines : *« La cible Packages est specifiee plusieurs fois dans grafana.list:1 et :2 »*. **La liste des fournisseurs ne se devine pas.** J'en avais recense trois ; l'audit des sources en a revele un quatrieme — Collabora, sur une seule machine. **P65** refuse desormais tout role visant un depot RELAYE en `https://` ecrit en dur. Sa limite est dite : elle empeche une regression sur ce qui est connu, elle ne decouvre pas l'inconnu. avant : 4 fournisseurs en HTTPS direct, 14 machines sortant seules apres : 0 source en HTTPS direct, 0 erreur apt sur 14 machines ### 2. Le cache d'artefacts quitte le plan du tenant La ligne portait son propre retrait depuis toujours : > *« c'est un service MUTUALISABLE — un ecosysteme au premier age peut aussi bien pointer > sur celui de son hote »* Retiree. `client_artefacts` le voit tout seul : sans hote portant `serveur_artefacts`, il n'ecrit rien et laisse en place le plancher d'amorcage qui vise `site-cache-01`. **Ce qu'on perd, dit franchement** : les quatorze machines interrogent desormais le cache du SITE a travers la frontiere, au lieu d'un cache local a la zone. Plus de trafic inter-zone, plus de charge sur `site-cache-01` — contre un service de moins a poser, superviser et reproduire dans chaque ecosysteme. ### 3. Un role qu'on retire doit pouvoir DEFAIRE ce qu'il a fait Retirer le role a revele que rien ne nettoie derriere lui. Deux fois : - **le fichier apt** `00-setops-artefacts` continuait de viser un cache eteint, en ecrasant le plancher qui, lui, fonctionnait — exactement le defaut deja paye (« quinze machines ont perdu apt d'un coup »). Le SOCLE le retire desormais, parce qu'il est le seul a tourner dans les deux cas ; - **les sondes** `cache-apt` et `cache-apt-volume` restaient sur la forge, et le porteur poussait pour des services qu'Icinga ne definit plus : ECHEC du rapport Icinga pour « cache-apt » : {"error":404,"status":"No objects found."} `client_sante` derive maintenant les sondes ATTENDUES sur chaque hote — ses groupes croises avec les `meta/supervision.yml` — et retire celles dont plus aucun groupe ne repond. Meme derivation que celle de `serveur_icinga`, du cote du porteur. ### Etat mesure sources apt en HTTPS direct 0 / 14 machines erreurs `apt-get update` 0 / 14 machines mandataire site-cache-01 seul, partout Icinga 87 OK | 9 UNKNOWN sur 96 services (les 9 = `sauvegarde`, minuteur nocturne) prouver 65 OK, 0 echec ansible-lint 0 defaut **Reste, et c'est dit** : `apt-cacher-ng` tourne toujours sur `forge-01`, que plus aucun plan ne declare. La prochaine reconstruction ne l'installera pas ; sur la machine actuelle, il subsiste. Un service qu'aucun plan ne reclame est une derive, meme benigne. ## 2026-09-10 (9) — La reconstruction de confirmation : 0 echec, 34 minutes Seconde reconstruction complete de Chezlepro dans la journee, cette fois pour EPROUVER les quatre correctifs livres entre les deux. Rien de nouveau n'a ete construit : c'est une mesure. rc=0 0 echec 0 injoignable 14 / 14 machines 34 min contre 68 le matin meme ### Les quatre points, et leur verdict | ce qui etait a eprouver | verdict | |---|---| | gabarit sur `CephNVMe` | phase de clonage **4 min 30** contre ~30 min | | placement declare (`noeud: asgard`) | `{'asgard': 14}` — les quatorze au bon endroit | | `hosts_statiques` conditionne a cloud-init | `changed` au 1er passage, `skipping` au 2e | | `monitoring-plugins-basic` au role | `ping4` **14/14 OK** sur une `mon-01` nee neuve | Le troisieme est le plus instructif : **les deux branches sont exercees dans la meme execution.** `infra-pki-01` recoit le gabarit maitre de cloud-init au premier passage (cloud-init est encore la), puis la tache est SAUTEE au second (le durcissement l'a retire). Aucune preuve statique ne pouvait montrer cela — il fallait une naissance. ### Le facteur 6,6, et pourquoi ce n'est pas 45 Un clone isole sur Ceph prend 8 s contre 352 a 480 s sur TrueNAS : facteur 45. En conditions reelles il tombe a **6,6** — quatre clones se disputent le meme pool, et le redimensionnement du disque s'ajoute a la copie. On retient la mesure en conditions reelles : celle qui compte est celle qu'on paie. ### Ce que les echecs du matin sont devenus - **`/etc/cloud`** — corrige, prouve dans ses deux branches. - **Verrou `dpkg`** — ne s'est pas reproduit. C'etait une course, pas un defaut de structure ; rien ne dit qu'elle ne reviendra pas, et le dire vaut mieux que la declarer reglee. - **Cache apt** — **n'etait pas un defaut**. Les deux seuls `fatal:` de cette execution sont suivis de `...ignoring` : ce sont les sondes de `client_artefacts`, qui cherchent le cache du tenant, ne le trouvent pas, et laissent la machine servie par le plancher du SITE. C'est la filiation en train de fonctionner. Le matin, j'avais compte ces deux lignes comme des echecs sans lire la suivante. - **`auditd` sans regles dans le gabarit** — toujours la, mais invisible : aucune machine n'ayant decroche avant le socle, le CRITICAL transitoire a ete repare partout avant d'etre mesure. *Un defaut qui ne se voit que lorsqu'autre chose casse reste un defaut.* ### Etat mesure Icinga 89 OK | 9 UNKNOWN sur 98 — aucun WARNING, aucun CRITICAL les 9 tous des `sauvegarde` : le minuteur nocturne n'a pas encore visite une flotte nee il y a vingt minutes La condition posee avant de retirer les disques `unused0`/`unused1` du gabarit sur TrueNAS est **levee** : une flotte complete est nee du nouveau gabarit, sans un echec. ## 2026-09-10 (8) — Une garde qui survit a ce qu'elle gardait Le defaut le plus couteux de la reconstruction, corrige. Il n'arretait rien de visible : il faisait **taire** la supervision de deux machines. `hosts_statiques` pose `/etc/cloud/templates/hosts.debian.tmpl` — le gabarit maitre dont cloud-init regenere `/etc/hosts` a chaque demarrage. Or **D-85 fait retirer cloud-init** par `serveur_durci`. Le repertoire part avec lui : Destination directory /etc/cloud/templates does not exist L'echec n'a aucun sens : ce gabarit ne sert qu'a survivre a une reecriture qui n'a plus lieu. Plus de cloud-init, plus de reecriture — le plancher tient tout seul, **ce qui est le resultat recherche par D-85**. ### Ce que ca a coute, et pourquoi c'etait invisible `_amorcer-socle` monte `infra-pki-01` et `infra-dns-01` EN ENTIER d'abord, durcissement compris. Cloud-init y est donc deja parti quand la flotte rejoue `hosts_statiques`. La tache echouait, **l'hote sortait du play — et tout ce qui suivait n'etait jamais pose**, dont `icinga-ca.crt`. Leur porteur de sante s'installait ensuite normalement, tournait, et chaque rapport echouait sur `curl: (77) error setting certificate file`. Deux machines ont supervise dans le vide sans que rien ne le dise. *Un echec bruyant au bon endroit avait produit une panne muette ailleurs.* ### Et le correctif evident aurait deplace l'echec d'un cran Sauter la tache ne suffisait pas : la garde qui SUIT compare le nombre d'entrees du plancher a celui du gabarit maitre. Sans gabarit, elle compare 21 a 0 et echoue — elle accuserait une divergence la ou il ne reste qu'un seul fichier. **Une garde qui survit a ce qu'elle gardait ne mesure plus rien : elle invente.** Les trois taches — pose, releve, assertion — suivent desormais la meme condition : le repertoire des gabarits maitres existe-t-il encore ? Verifie sur les deux machines memes qui echouaient : `failed=0`, cloud-init absent, plancher a 21 entrees. ## 2026-09-10 (7) — D-86 mis a l'epreuve : Chezlepro rasee et refaite depuis zero Quatorze machines detruites, quatorze refaites. **1 h 08**, 0 injoignable. La reconstruction a confirme D-86 — et revele quatre defauts qu'aucune preuve statique n'aurait pu voir, parce qu'ils n'existent QUE pendant une naissance. ### D-86 tient, et voici la mesure L'ordre declare est l'ordre reel : socle → `step_ca` → `client_pki` → **observabilite** (postgresql, prometheus, loki, grafana, icinga) → **agents** (metrique, journal, sante) → services → apps. Mais l'ordre ne prouve pas que quelqu'un REGARDAIT. Ce qui le prouve : 30 resultats de controle recus a 12:20:41 fin de la reconstruction 12:35:29 Trente services rapportaient leur etat pendant que Keycloak, Forgejo et Nextcloud montaient encore. Prometheus scrutait 14 cibles sur 15. **Ce qui se deploie ensuite l'est bien sous l'oeil de la supervision.** ### La limite, dite franchement : le plancher n'a pas ete eprouve D-86 affirme que le DNS peut rester en couche 6 *grace au plancher `/etc/hosts`*. Ce n'est pas ce qui s'est passe : `_amorcer-socle` monte `infra-dns-01` EN ENTIER d'abord — `serveur_powerdns` puis `serveur_resolveur`, bien avant l'observabilite. Le DNS etait debout ; le plancher n'a rien eu a porter. **La partie la plus audacieuse de la decision reste non testee**, et l'eprouver demanderait de retirer l'amorcage DNS — une autre decision. ### Le placement des VM n'etait declare NULLE PART Les quatorze machines vivaient sur `asgard` depuis toujours. `SETOPS_NOEUD` valait le VIDE : ce placement n'existait que dans l'etat d'execution de Proxmox. Sans cible, le clone reste sur le noeud du gabarit — `vishnu`, qui a **14 Go libres** et heberge `eregion` et `site-forge-01`. Quarante Go de VM neuves y auraient atterri. Le defaut avait survecu a la reconstruction du 2026-09-02 : les VM existaient deja, et le clone les sautait. **Un etat qui n'est declare nulle part ne se reproduit pas** — il faut un `from-zero` VRAI pour le voir. `noeud: asgard` est desormais au plan. ### Le clonage etait 45 fois trop lent, et la cause n'etait pas le reseau Mesure sur le vrai gabarit : disque sur `TrueNAS` (lvm sur iSCSI) 352 a 480 s par clone disque sur `CephNVMe` (rbd) 8 a 9 s par clone Source ET destination etaient sur le meme stockage : rien ne traversait d'hyperviseur a hyperviseur. La cause est que **le LVM epais interdit a Proxmox tout clone autre que complet** — 16 Go copies par machine, a travers iSCSI. Le clone LIE descend a 1 s, mais enchaine chaque VM a l'image de base pour toujours. Pour 7 s gagnees sur 480, l'echange ne vaut pas la dependance : **on garde les clones complets.** Gabarit deplace par `qm move_disk` sans `--delete` — les anciens disques restent attaches en `unused`, le retour arriere tient en une commande. ### Un champ `stockage:` au gabarit, branche en quatre points Declarer sans consommer, c'est decorer. Le champ vit donc partout ou il compte : | ou | quoi | |---|---| | `plan/10-intrants.yml` | la declaration, avec la mesure qui la justifie | | `underlay.py --gabarit` | l'expose | | `Makefile` | `STOCKAGE_PROXMOX` en derive — le clone ne l'HERITE plus en silence | | `gabarit_etat.py` | compare le declare au reel, eprouve dans les deux sens | Et le remede devient contextuel : le conseil *« NE PAS CONVERTIR… REFABRIQUER le gabarit »* s'imprimait pour TOUT ecart, y compris un deplacement de disque qui se repare en une commande. *Un remede plus lourd que le mal se fait ignorer, puis le vrai avec lui.* ### Icinga ne pouvait pas faire ses propres controles ping4 UNKNOWN execvpe(/usr/lib/nagios/plugins/check_ping) failed: No such file `icinga2` n'apporte AUCUN greffon. Toute la supervision de Set-OPS etant PASSIVE, le manque etait masque : les services qui rapportent d'eux-memes etaient verts, et personne ne regardait les autres. Quatorze `ping4` muets d'une seule cause. `monitoring-plugins-basic` entre au role — `-basic` et non le metapaquet : `-standard` ajoute des greffons LDAP/SMTP/PostgreSQL que Set-OPS n'appelle jamais. **Le site l'avait deja — pose A LA MAIN plus tot dans la journee, jamais declare.** Meme defaut que le placement, troisieme occurrence du jour : un etat qui ne vit que sur la machine ne survit pas a une reconstruction. ### Trois defauts trouves, laisses ouverts 1. **`infra-pki-01` et `infra-dns-01` attendent `forge-01:3142`** pendant l'amorçage — le cache apt naitra bien plus tard. Un ordre qui se mord la queue. 2. **Regression D-85** : `Destination directory /etc/cloud/templates does not exist`. Un role ecrit encore la ou cloud-init a ete supprime. **C'est ce defaut qui a fait decrocher deux hotes de la tache deposant `icinga-ca.crt`** — leur porteur de sante tournait, et chaque rapport echouait sur `curl: (77)`, en silence. 3. **`auditd` dans le gabarit, sans regles** : `audit-rules.service` echoue au premier demarrage de CHAQUE VM neuve, jusqu'au passage du socle. Le CRITICAL ressemble alors a un probleme de la machine alors qu'il vient du modele. ### Etat mesure, apres correction Icinga Chezlepro 93 OK | 1 WARNING | 1 CRITICAL | 3 UNKNOWN sur 98 Icinga du site 51 / 51 OK Prometheus 15 / 15 cibles prouver 64 OK, 0 echec ansible-lint 0 defaut, profil production Les quatre non-OK restants sont tous des `sauvegarde` : le minuteur nocturne n'a pas encore visite une flotte nee il y a deux heures. ## 2026-09-10 (6) — La frontiere passe, et trois sondes disaient faux Application de ce que l'entree precedente avait prepare, puis correction de ce que l'application a revele. ### La frontiere `make frontiere-appliquer` : **49 regles creees, 5 retirees.** Effet mesure immediatement : cibles Prometheus 2/8 -> 8/8 hotes dans Loki 1/7 -> 7/7 ### Trois defauts, tous de la meme famille : un reglage qui ne suit pas son interrupteur **1. La sonde de Loki interrogeait en HTTPS un Loki servant en clair.** `serveur_loki_sonde_url` disait `https` en dur alors que `serveur_loki_tls_actif` vaut `false` par defaut. Chez le tenant, qui l'active dans ses group_vars, l'accord etait fortuit ; au site, qui ne l'active pas, la sonde rapportait *« Loki ne repond pas »* sur un service en parfaite sante. **Une fausse alarme est pire qu'une sonde absente : elle apprend a ne plus lire la sonde.** Le schema se derive desormais du commutateur. C'est exactement le meme defaut que `GF_AUTH_DISABLE_LOGIN_FORM` chez Grafana, corrige la veille au soir. Deux occurrences en douze heures. **2. La sonde des journaux criait une perte deja reparee.** Elle comparait au passage precedent sans dire QUAND ce passage avait eu lieu. Elle a annonce *PERTE EN COURS* sur deux machines alors que le delta reel etait nul — les lignes avaient ete perdues avant la reparation. Mesure de controle, deux lectures a 90 s : jetees 11340 -> 11340 (delta 0) envoyees 68 -> 86 (delta +18) *Un ecart sans sa fenetre n'est pas une mesure, c'est un nombre.* L'etat retient maintenant l'horodatage, et le message porte la duree. **3. Et surtout : elle alertait sur la cicatrice, pas sur la plaie.** Le compteur d'Alloy est cumulatif — il ne redescend qu'au redemarrage. Les six machines qui avaient perdu des lignes pendant que la frontiere etait fermee les portaient donc pour toujours, condamnees a l'orange permanent. **Ce qui alerte est la CROISSANCE.** Le total reste dans le texte et dans les metriques, la ou il sert au diagnostic sans crier. En echange, un cas qui ne levait rien le fait desormais : `envoyees == 0` et `jetees == 0` n'est pas « tout va bien », c'est « Alloy ne fait rien du tout ». Controles negatifs, sur des COPIES des sondes : port d'Alloy inexistant -> CRITICAL rc=2 compteur de pertes en hausse -> CRITICAL rc=2 port de node_exporter ferme -> CRITICAL rc=2 ### Neuf services qu'Icinga attendait et que personne ne lui envoyait Le temoin declarait 51 services et n'en recevait que 42. Les neuf muets avaient une sortie **vide** : jamais un seul resultat. `setops-sondes.conf` derive les services de tous les `meta/supervision.yml` — Icinga savait donc les attendre bien avant que les roles n'aient ete rejoues pour deposer les scripts. *Une declaration suffit a creer l'attente ; il faut un deploiement pour creer la reponse.* Huit roles rejoues au site : `serveur_artefacts`, `serveur_resolveur`, `serveur_powerdns`, `serveur_forgejo`, `serveur_postgresql`, `serveur_postfix`, `serveur_step_ca`, `serveur_ops`. ### Grafana `vault_grafana_admin` depose dans la voute du site. Verifie sur la machine : formulaire local **offert**, zero ligne `GENERIC_OAUTH`. Sonde `tableaux` verte. *Note d'exploitation :* `ansible-vault` refuse de tourner quand stdout n'est pas bloquant — il faut le faire passer par un tube. Un premier essai a tronque la voute de 11,8 K a 873 o parce que le chiffrement s'est execute apres une lecture qui avait echoue. Restauree depuis la copie prise avant, identique a l'octet pres. **Le remede est dans l'ordre des gardes : lire, VERIFIER le nombre de cles, ecrire vers un fichier neuf, verifier ce fichier, et seulement alors remplacer.** ### Etat mesure | | | |---|---| | cibles Prometheus | **8/8** | | hotes dans Loki | **7/7** | | sonde `journaux` | **7/7 vert** | | sonde `metriques` | **7/7 vert** | | services Icinga | 51, dont 42 OK avant redeploiement des huit roles | | `make prouver` | 64 OK, 0 echec | | `ansible-lint` | 0 defaut, profil `production` | ### Suite : le runner ne pouvait plus cloner, et trois sondes de plus disaient faux Redeployer `serveur_ops` au site a echoue sur le clonage du genome. Mesure depuis les sept machines : 10.0.33.11:443 injoignable de PARTOUT sauf depuis la forge elle-meme Y compris depuis `site-cache-01`, qui est dans le MEME sous-reseau — ce qui excluait la frontiere et designait le pare-feu de l'hote. Le diff de `flux-genere/` l'a confirme : la ligne fautive etait la AVANT nos changements, qui n'ajoutaient que les regles des sondes. **Defaut preexistant, revele par le redeploiement, pas cause par lui.** **La cause : `generer_nftables(site=True)` lisait le plan du TENANT.** `ports_plan = _ports_du_plan()`, sans condition. `derive` se resolvait donc contre `OPS-Chezlepro/plan/applications.yml`, ou Forgejo vaut 3000 — le port qu'il ecoute DERRIERE un edge. Le plan du site dit 443, et le disait explicitement : # 443, ET NON 3000. [...] garder 3000 aurait grave `:3000` dans ROOT_URL La regle d'hote de la forge ouvrait donc un port que personne n'ecoute et laissait 443 ferme a toute la flotte. Un seul registre interroge pour deux verites opposees. **Trois sondes de plus corrigees, toutes de la meme famille** — un parametre qui ne suit pas l'interrupteur dont il depend : | sonde | ce qu'elle disait | la cause | |---|---|---| | `runner` | 2 depots divergent | comparait les 6 depots a UNE branche, quand le plan en declare une PAR depot (`master` pour deux d'entre eux) | | `forge` | reponse illisible | `http://` en dur sur un port 443 servant du TLS | | `forge` | ne repond pas | `curl -s` sans `-k` : cert step-ca emis pour le FQDN, interroge sur `127.0.0.1` | La sonde `runner` est l'exemple le plus net : **elle contredisait la declaration qu'elle etait censee verifier.** Elle n'accusait pas la machine, elle s'accusait elle-meme. Avec Grafana, Loki et Forgejo, cela fait **cinq occurrences du meme defaut en une journee**. Le motif merite d'etre nomme : *un reglage corrige a moitie ne dit rien tant que le deploiement ne change pas de camp.* Le tenant restait juste dans les cinq cas — c'est le site, qui deploie ces roles autrement, qui les a tous reveles d'un coup. ### Etat final mesure Icinga 51 / 51 services OK Prometheus 8 / 8 cibles up Loki 7 / 7 hotes presents prouver 64 OK, 0 echec lint 0 defaut sur 82 fichiers, profil production Controles negatifs, sur des COPIES : port d'Alloy, compteur de pertes, node_exporter, forge — les quatre rendent CRITICAL. ## 2026-09-10 (5) — Le site prend sa propre pile d'observabilite Suite directe de D-87. La decision disait : *l'hebergeur n'a pas le droit de voir les journaux de ses locataires.* Elle avait une face cachee — **a force de refuser de voir ceux des autres, le site s'etait prive des siens.** Ses sept machines n'expediaient nulle part. Le remede n'est pas d'assouplir la frontiere, c'est de donner au site **sa** pile : `prometheus`, `loki` et `grafana` sur `site-mon-01`, pour les machines du site, sans aucun lien avec ceux d'un tenant. ### Ce qui bloquait etait deja documente, dans le plan lui-meme `10-intrants.yml` exemptait toutes les machines du site de `client_metrique` et `client_journal`. La prose de l'exemption portait sa propre condition de levee : > *« Le jour ou le site prend un Loki et un Prometheus, on retire ces deux lignes. »* Ce jour-la etant venu, l'exemption s'est refermee toute seule. Elle aura tenu cinq jours. ### Grafana au site n'a pas de SSO, et le role l'ignorait Le site n'a ni Keycloak ni `domaines.yml` — l'identite ne monte pas dans le site, c'est D-87. `serveur_grafana` reclamait pourtant l'IdP **avant** de regarder s'il en voulait un : le deploiement echouait sur un registre absent, pour deriver une URL qu'aucun gabarit n'allait ecrire. L'interrupteur `serveur_grafana_oidc_actif` existait, il n'etait pas honore. Trois corrections, dont deux depassent le site : - `resoudre_idp` n'est appele que si le SSO est actif ; - le role **refuse** SSO eteint *et* formulaire local eteint — la combinaison deploie un Grafana en parfaite sante ou personne ne peut entrer ; - `GF_AUTH_DISABLE_LOGIN_FORM` sort du `{% if %}` du SSO. Il y disparaissait quand le SSO etait eteint, et c'est le defaut amont qui decidait en silence. *Un reglage d'authentification qu'aucun fichier n'ecrit est un reglage que personne ne peut relire.* ### Deux manques se cachaient l'un l'autre dans le devis de la frontiere Prometheus voyait **1 cible sur 7**. Les regles d'hote etaient justes ; c'est le devis OPNsense qui avait deux trous, et le second masquait le premier. **1. Le devis ne connaissait pas les integrations universelles.** Il derivait les groupes d'une machine du site de `applications.yml` seul — qui declare les *services*. Il ne voyait donc ni `client_metrique`, ni `client_journal`, ni `client_pki`, ni `client_sante`, alors que ces groupes portent des flux et **ouvrent des ports d'ecoute**. La source est desormais l'inventaire du site, seule autorite sur ce qu'une machine porte vraiment. **2. Une sortie vers un role du site visait « l'exterieur ».** Symetrique du correctif du 2026-09-02, qui n'avait traite que l'entree. `!SETOPS_INTERNES` est la bonne destination quand le pair est lointain ; quand il **nomme un role du site**, elle dit exactement l'inverse du flux declare — elle exclut la seule machine visee : client_journal -> !SETOPS_INTERNES port 3100 Le devis autorisait a expedier les journaux du site a n'importe quel Loki du monde, et a nul autre endroit qu'a celui-la. Neuf flux etaient dans ce cas : PKI, sante, resolveur, sauvegarde, courriel de la forge, base d'Icinga. **Et les deux declarations ne font qu'une regle.** `appliquer_opnsense` pose tout en `direction: in` (D-61) : deux regles qui ne different que par leur `sens` sont le meme filtre pose deux fois. Dedoublonnage sur ce que la frontiere applique vraiment — la forme `ingress` gagne, pour ne pas retirer-puis-recreer des regles deja justes. Devis : **49 regles a creer, 5 a retirer** — les cinq etant exactement les sorties trop larges dont la jumelle etroite existe deja. ### Les deux agents se supervisent enfin eux-memes `client_metrique` et `client_journal` etaient parmi les groupes sans sonde. Ils en ont une. `metriques` interroge **l'endroit que Prometheus interroge**, pas le gestionnaire de services : un node_exporter actif mais muet est vert pour systemd. Elle declare aussi les dix collecteurs qui cherchent du materiel qu'une VM n'a pas (`zfs`, `mdadm`, `infiniband`…) — ils echouent identiquement sur les sept machines, et *une sonde rouge partout est une sonde qu'on cesse de lire.* `journaux` ne demande pas si Alloy tourne : elle lit ce qu'il a du **jeter**, et le compare au passage precedent — ce qui augmente est une perte en cours, ce qui stagne est une cicatrice. Elle a fait ses preuves le jour meme. Sur six machines : Alloy: active (running) /-/ready: 200 dropped_entries_total: 50 Trois indicateurs verts, cinquante lignes perdues. La regle de frontiere manquait encore. ### Etat mesure | | | |---|---| | `metriques` | **7/7 vert** | | `journaux` | **1/7** — les six autres attendent la regle de frontiere, et le disent | | cibles Prometheus | 2/8 pour la meme raison | | `make prouver` | 64 OK, 0 echec | | `ansible-lint` | 0 defaut, profil `production` | **Reste a la main de l'exploitant** : `make frontiere-appliquer CONFIRMER=true`, et le secret `vault_grafana_admin` a deposer dans `underlay.vault.yml`. ## 2026-09-10 (4) — Ce que l'hebergeur n'a pas le droit de VOIR (D-87) Question posee : *« quels roles ne dois-je pas embarquer dans le site ? »* La table de mutualisation de `filiation-emancipation.md` existait deja et repondait presque. Mais mise a l'epreuve, **une de ses lignes contredisait ce qui tourne**. ### La ligne qui se contredisait | observabilite | oui | l'hebergeur surveille ses locataires | Or chaque ecosysteme a son propre Icinga, et celui du site ne voit que ses **sept** machines (mesure). L'implementation avait raison, la doctrine avait tort. Et surtout : cette case autorisait ce que la ligne du dessous interdit. **Les journaux contiennent du CONTENU** — un mot de passe dans un message d'erreur, une donnee metier dans une trace, qui a fait quoi et quand. Un hebergeur qui ingere les journaux de son locataire en sait **plus** que s'il detenait son annuaire : *l'annuaire dit qui existe, les journaux disent ce qu'ils font.* Decoupee en trois : disponibilite (est-ce debout ?) oui une VM tombee est un fait de la fabric metriques (charge, disque) oui, reserve disent quand et combien, pas quoi journaux NON contiennent le contenu ### Et la ligne PKI, « a trancher », est tranchee : non Elle notait qu'*une AC intermediaire signee par l'hote est possible*. Elle l'est techniquement — et c'est precisement ce qu'il ne faut pas faire. Une intermediaire signee par l'hote lui donne le pouvoir d'emettre des certificats **valides pour les noms du locataire**. Il peut alors se presenter comme n'importe lequel de ses services, devant les propres machines du locataire — qui les accepteront, puisque c'est exactement ce que la chaine de confiance leur demande de faire. Meme pouvoir que l'annuaire, sous une forme **moins visible** : rien dans la configuration du locataire, aucune trace de son cote, et la verification passe. **Une PKI par ecosysteme, jamais derivee de l'hote.** C'est deja ce qui tourne ; la ligne cesse de laisser la porte entrouverte. ### Le revers, mesure et assume Le site ne porte **ni `client_journal` ni `client_metrique`**, et aucun `loki` ni `prometheus`. Ses sept machines n'expedient rien nulle part : **l'hebergeur ne peut pas lire ses propres journaux**. C'est la consequence directe de la frontiere — a refuser de voir ceux des locataires, il s'est prive des siens. Le remede n'est pas d'assouplir la regle, c'est de lui donner **sa propre pile**, sans aucun lien avec celle d'un tenant. ### Ce que la mesure a confirme par ailleurs Rien d'intime n'est au site aujourd'hui. `openldap`, `keycloak`, `oauth2_proxy`, `loki`, `nextcloud`, `collabora`, `dovecot`, `rspamd`, `web_frontal`, `web_dorsal` : tous **tenant seulement**. La frontiere etait tenue en pratique avant d'etre ecrite au net. ## 2026-09-10 (3) — Les cinq sondes qui manquaient le plus **20 sondes sur 19 roles. 23 services distincts, 70 instances, 67 au vert.** ### D'abord une correction de compte J'avais annonce « cinq groupes sans sonde ». Mesure : **vingt-sept**. J'avais compte ceux que j'avais en tete, pas ceux que le depot contient. Il en reste vingt-deux. ### Les cinq posees, par ordre de degat silencieux **`autorite` (step_ca)** — la plus urgente de l'ecosysteme. Nos certificats vivent 24 h : une AC muette ne casse rien aujourd'hui, elle casse TOUT demain, d'un coup, sur les vingt-et-une machines a la fois. Elle surveille aussi **l'expiration de la RACINE**, que personne ne regarde jamais parce qu'elle vit des annees — releve : 3642 jours. Le jour ou elle expire, toute la confiance interne tombe d'un bloc, et aucun renouvellement de certificat d'hote n'y change rien. **`base` (postgresql)** — une VRAIE requete, pas `pg_isready`. Celui-ci ouvre une connexion et la ferme : il dit que le port repond, pas que la base sert. Une base en recuperation, en lecture seule ou a court de connexions le passe et refuse tout travail. La sonde compte aussi les connexions : a saturation, chaque application tombe en meme temps sans que la base ait l'air morte. **`annuaire` (openldap)** — elle COMPTE les entrees. Un annuaire vide repond `success` a tout, et plus personne ne s'authentifie nulle part : c'est exactement le mensonge des sauvegardes vides, vert et sans contenu. **`zones` (powerdns)** — un autoritatif sans zone repond NXDOMAIN a tout, ce qui se lit comme « ce nom n'existe pas ». La panne la plus trompeuse du DNS. **`edge` (nginx)** — elle valide la configuration SUR DISQUE. nginx garde la derniere configuration valide et continue de servir ; une configuration cassee ne se voit qu'au prochain demarrage, c'est-a-dire au pire moment, souvent des mois plus tard. Les cinq eprouvees vertes sur le sain, puis rouges PAR PARAMETRE — port ferme, seuil impossible, port qui n'ecoute pas — sans toucher a un seul service. ### Ce qui reste, et pourquoi ce n'est pas le meme genre Vingt-deux groupes. Ils ne sont pas de la meme nature : - **couverts ailleurs** : `client_metrique` (par `collecte` chez Prometheus), `client_backup` (par `sauvegarde`), `client_sante` (sa fraicheur EST son `ttl`) ; - **chemins de report** : `client_smtp`, `client_artefacts`, `client_journal`, `client_resolveur` — leur panne se voit deja par le silence de ce qu'ils portent ; - **du site** : `serveur_cache_site`, `serveur_forge_site`, `serveur_backup_site`, `serveur_resolveur_site`, `serveur_ops_site` — a poser depuis l'instance du site ; - **applications et socle** : `redis`, `rspamd`, `collabora`, `web_frontal`, `web_dorsal`, `oauth2_proxy`, `icingaweb2`, `backup`, `debian`, `durci`. ## 2026-09-10 (2) — `cache-apt` scindee, et le defaut que la scission a revele ### La scission `cache-apt` fondait deux causes dont les DELAIS different : « ne repond pas » arrete tout `apt` de l'ecosysteme — on agit dans la minute — tandis que « volume a 90 % » est un billet pour demain. Les fondre obligeait soit a reveiller quelqu'un pour un disque, soit a traiter une panne comme un billet. Deux sondes desormais, chacune avec son etat, son historique et son acquittement. Prouve qu'elles sont INDEPENDANTES, et c'etait tout l'enjeu : port ferme -> cache-apt [2] CRITIQUE cache-apt-volume [0] OK seuil impossible-> cache-apt [0] OK cache-apt-volume [1] AVERTISSEMENT ### Ce que la verification a revele : `moteur` aurait alarme PARCE QUE tout allait bien En verifiant que les deux services arrivaient bien dans Icinga, un resultat pousse et accepte (`code 200`) n'apparaissait pas en base. Hypothese testee et confirmee : **IcingaDB n'ECRIT `service_state` QUE SUR CHANGEMENT D'ETAT.** Un OK identique repete ne produit aucune ecriture ; un AVERTISSEMENT pousse ensuite est ecrit en 13 secondes. Or la sonde `moteur`, ecrite quelques heures plus tot, lisait exactement `max(last_update)` de `service_state` pour juger que « l'etat est frais ». Elle mesurait donc le CHANGEMENT, pas la fraicheur. **Consequence : sur un ecosysteme parfaitement stable — celui qu'on veut — plus rien ne change, `last_update` vieillit, et la sonde serait passee en avertissement a 15 minutes puis en critique a 90.** Une alarme qui se declenche PARCE QUE tout va bien, avec un delai qui l'aurait rendue difficile a rattacher a sa cause. C'est le piege que `docs/supervision-conception.md` interdit — *une alarme toujours allumee apprend a ne plus regarder* — sous sa forme la plus sournoise : differee. **La bonne source existait** : `icingadb_instance` porte le battement du synchroniseur, ecrit en continu qu'il y ait ou non des changements. Releve a **1 seconde** sur un systeme sain. Les seuils passent de 15/90 minutes a **1/5 minutes** : sur un battement, une minute de silence est deja anormale. Controle negatif rejoue par parametre. ### Etat Dix-huit services, tous verts sauf `sauvegarde` a 7/9 — deux noeuds sans donnee a emporter, deja connu et confirme. ## 2026-09-10 — Chaque role porte desormais sa sonde **Quatorze sondes**, declarees dans `meta/supervision.yml`, deposees par le role qui possede la verite, derivees en objets Icinga sans qu'une ligne soit ecrite a la main : boites (dovecot) certificat (client_pki, 14/14) collaboration (nextcloud) cache-apt (artefacts) collecte (prometheus) file-courriel (postfix) forge (forgejo) identite (keycloak) ingestion (loki) moteur (icinga) resolution (resolveur) runner (serveur_ops) tableaux (grafana) voute (ops_tenant) Les dix-neuf lignes `surveillance:` ecrites en prose et jamais executees commencent a devenir des mesures. ### Deux principes que la premiere sonde a imposes **Une sonde doit pouvoir etre mise en defaut PAR PARAMETRE.** Cible et seuils sont des variables du role : on prouve le rouge avec un port ferme ou un seuil impossible, sur une machine reelle, sans rien casser, aussi souvent qu'on veut. Une sonde qu'on ne peut prouver qu'en cassant un service ne sera prouvee qu'une fois. Eprouve sur `cache-apt` : vert, CRITIQUE par port ferme, AVERTISSEMENT par seuil, retour au vert. **La sonde vit la ou vit la verite.** *« Ce noeud est-il collecte ? »* est une sonde de `serveur_prometheus`, pas de `client_metrique` : une seule y voit les N noeuds, et surtout elle voit le cas **silencieux** — celui qui a cesse d'etre collecte ne peut pas s'en plaindre lui-meme. ### On demande au service ce qu'il pense de lui-meme Quand il sait le dire : `/api/healthz` de Forgejo (base + cache), `/api/health` de Grafana, `/ready` de Loki, `status.php` de Nextcloud, la decouverte OIDC de Keycloak. C'est plus juste que tout critere invente de l'exterieur — une forge dont la base est tombee sert encore ses pages et repond a `/api/v1/version`. Et quand il ne sait pas : on va chercher la verite de terrain. `moteur` ne regarde ni le service ni le port — il demande a la base **depuis combien de temps elle n'a pas ete rafraichie**. C'est la lecon des sauvegardes appliquee a la supervision elle-meme : un moteur vert dont la synchronisation est tombee affiche eternellement le dernier etat connu. ### Quatre fois j'ai ecrit la sonde avant de mesurer, quatre fois elle a eu tort **La forge** : port 443 et chemin des depots INVENTES. Elle ecoute en 3000 derriere l'edge et n'a legitimement AUCUN depot — elle est neuve. Deux cris sur un service sain. **Loki** : conclu « panne persistante » sur deux lectures prises a quelques secondes d'intervalle, juste apres un redemarrage. L'anneau etait `ACTIVE` et la reponse est passee a `ready` moins d'une minute plus tard. *Deux mesures rapprochees ne distinguent pas un etat d'un instant.* Le delai de stabilisation est desormais un AVERTISSEMENT nomme. **Keycloak** : vise en 8443, il ecoute en 8080 derriere l'edge. **Le runner** : `git` en root refuse les depots d'un autre proprietaire — la sonde aurait rendu un echec qui parle de git au lieu de parler du depot. À chaque fois le remede est le meme : **lire la verite du role, ne pas la supposer**. ### Et le meme piege Jinja, une seconde fois `${#tableau[@]}` contient `{#`. Le remede etait deja au depot depuis `client_sante` ; je l'ai reecrit au lieu de le chercher. La correction balaie desormais tous les gabarits de sonde. ### Ce qui reste `client_smtp`, `client_artefacts`, `client_journal`, `serveur_icingaweb2` et `serveur_ops_site` n'ont pas encore la leur. Le premier lot couvre les services dont la chute arrete l'ecosysteme ; ceux-la sont des chemins de report, dont la panne se voit deja par le silence des sondes qu'ils portent. ## 2026-09-09 (9) — La supervision passe juste apres la PKI (D-86) *On n'allume pas la lumiere une fois la maison finie.* L'observabilite et le moteur de supervision etaient en couches 4 et 5 sur six — donc deployes APRES presque tout ce qu'ils surveillent. Une reconstruction depuis zero est pourtant le moment ou l'on a le plus besoin de voir, et c'etait le seul moment ou l'on ne voyait rien. 1. socle serveur_debian, serveur_durci 2. pki_racine serveur_step_ca 3. pki_client client_pki 4. observabilite postgresql, prometheus, loki, grafana, icinga <- NOUVEAU 5. agents_supervision client_metrique, client_journal, client_sante <- NOUVEAU 6. services openldap, powerdns, resolveur, nginx, postfix... 7. apps keycloak, forgejo, icingaweb2, nextcloud... 8. agents client_smtp, client_backup, client_resolveur, client_artefacts ### Deplacer les serveurs sans les agents n'aurait rien change C'est le point qui a decide de la forme : ce sont les AGENTS qui rapportent, et ils etaient en derniere couche. Monter `prometheus` et `icinga` en laissant `client_metrique` et `client_sante` a la fin aurait produit une supervision allumee et aveugle. Les deux couches vont donc ensemble. Des la couche 5, chaque hote expedie ses metriques, ses journaux et l'etat de ses unites systemd. Tout ce qui se deploie ensuite est mesure PENDANT qu'on le construit. ### Ce que ca a coute, et ce qui reste tard a dessein `serveur_postgresql` monte aussi : `icinga` l'exige, et lui n'exige rien. C'est le seul entrainement. Restent en couche 7 : **`icingaweb2` et `oauth2_proxy`**, qui reclament LDAP et Keycloak. C'est la CONSOLE, pas la mesure — l'interface humaine peut attendre, l'etat non. ### Ce qui rend ce deplacement possible Le DNS (`powerdns`, `resolveur`) reste en couche 6, donc APRES la supervision. Or `icinga` joint sa base par un NOM, et `client_sante` pousse vers un NOM. Ca tient parce que le **plancher `/etc/hosts`**, pose des la couche 1 par `hosts_statiques`, porte deja les 34 entrees de l'ecosysteme — verifie sur `mon-01`. C'est exactement ce pour quoi il existe : *« il ne s'installe pas, il rend installable »*. Sans lui, cette decision serait impossible. ### La limite, dite franchement Les **notifications** dependent de `client_smtp`, encore en derniere couche. Pendant une reconstruction, l'etat est mesure et consultable — mais rien ne part par courriel avant la fin. Le deplacer demanderait de monter `postfix` et son relais, ce qui entrainerait bien plus que PostgreSQL. Et ceci : **l'ordre est valide, pas eprouve**. P08 accepte (aucune arete en arriere), le graphe des dependances accepte, `playbooks/site.yml` est regenere et passe le `--syntax-check` sur ses 782 lignes de plan. Mais la seule preuve reelle d'un ordre de reconstruction, c'est une RECONSTRUCTION — et celle-la se decide, elle ne se glisse pas dans une soiree. ## 2026-09-09 (8) — L'hote de supervision saturait par sa propre demonstration « mon-01 tape dans l'fond. » Il tapait, en effet. load average: 5,10 sur 4 coeurs 8 processus check_disk, 70 a 99 % de CPU chacun, jusqu'a 28 minutes de vie `check_disk` 2.4.0-3+deb13u1 ne rend jamais la main sur cet hote (etat `R`, il boucle). Icinga en relancait un a CHAQUE intervalle pour le service `disk` de son hote de DEMONSTRATION, et aucun ne mourait. **L'hote de supervision etait sature par la configuration d'exemple livree avec le paquet.** ### Ce qui a ete retire, et pourquoi l'hote plutot que les services `conf.d/hosts.conf` definit `object Host NodeName` — un `localhost` de demonstration avec `vars.disks`, `vars.http_vhosts`, `vars.os`. Les `apply Service` s'y accrochent : `disk`, `http`, `swap`, `apt`, `load`, `procs`, `users`, `ssh`, `icinga`. Aucun ne decrit cet ecosysteme, et trois etaient **rouges en permanence** — `swap` sur une VM sans swap, `http` sur un port ou rien n'ecoute, `apt` pour un paquet a mettre a jour. On retire l'HOTE, pas les services : sans lui les `apply` ne s'accrochent a rien, et on ne touche pas a un fichier que le paquet remplacera a la prochaine mise a jour. **Et ca garde ce qui servait** : `apply Service "ping4"` vise tout hote ayant une adresse, donc les notres. Il est vert 14/14 au tenant. Retirer `services.conf` l'aurait emporte avec le reste. avant : load 5,10 — 8 check_disk — 3 alarmes rouges permanentes apres : load 0,77 — 0 check_disk — certificat 14/14, sante 14/14, ping4 14/14 ### Trouve en verifiant : la supervision du SITE croyait tout mort hotes UP : 1/7 (site) 14/14 (tenant) Nos `object Host` sont verifies par `hostalive`, c'est-a-dire un ping. Le site est decoupe en zones separees ; l'ICMP inter-zones n'etait declare nulle part, donc la frontiere l'avalait — 100 % de perte, mesure. Et **Icinga SUPPRIME les notifications des services d'un hote DOWN** : une supervision qui croit tout mort n'alerte plus de rien, tout en ayant l'air de fonctionner. Le flux est desormais declare des DEUX cotes (`serveur_icinga` en egress, `serveur_debian` en ingress) — et le registre a refuse la premiere moitie seule, ce qui est exactement sa raison d'etre. `CODES_ICMP` apprend `echo-request` (un TYPE, pas un code de destination-unreachable). Les regles d'hote sont posees sur les 21 machines. ### Le generateur de la frontiere : corrige, et il cachait un second manque **La cause exacte.** `devis_opnsense.py` traitait tout `port` non numerique comme SYMBOLIQUE — a resoudre par le plan. C'est vrai pour `derive`, le port d'un service que seul le plan connait. C'est FAUX pour l'ICMP : `echo-request` et `frag-needed` sont des litteraux, pas des inconnues. Ils tombaient dans la branche « le plan ne resout pas », et aucune regle n'etait emise. Une ligne de condition, et le silence de toute une flotte. **Ce que la correction a revele en plus.** Huit regles a creer, zero a retirer — et SIX d'entre elles sont du **PMTUD**, absent lui aussi pour la meme raison. Le depot dit de ces deux messages ICMP : *« Bloques, la connexion s'etablit, les petites requetes passent et les grosses reponses restent suspendues — la panne la plus couteuse a diagnostiquer. »* **CORRECTION (meme jour, apres mesure).** La premiere redaction de ce paragraphe disait que cette panne « etait la, silencieuse, sur les sept machines de l'hebergeur ». **C'est faux, et la mesure ne le soutient pas** : site-mon-01 -> autre zone du site : MTU de chemin 1500 site-mon-01 -> Internet : MTU de chemin 1500 ops-01 (tenant, overlay EVPN) : MTU 1450, chemin vers Internet 1450 Le site est a **1500 de bout en bout**. Rien n'y etait donc casse entre zones : aucun lien du chemin ne plafonne plus bas, donc aucun message « fragmentation necessaire » n'avait lieu d'etre emis. La contrainte a 1450 vit chez les TENANTS, sur l'overlay — pas chez l'hebergeur. Les six regles restent un vrai manque : elles couvrent le site parlant vers l'EXTERIEUR, ou un lien distant peut tres bien plafonner plus bas, et elles seraient necessaires le jour ou une zone du site passerait sur l'overlay. Mais c'etait une **protection absente**, pas une panne active — et decrire l'un comme l'autre est exactement le genre d'exageration que ce fichier ne doit pas contenir. Une correction qu'on ne mesure pas se raconte toujours plus grande qu'elle n'est. **Ce que la regle emise autorise, exactement — et pourquoi on ne pretend pas mieux.** `appliquer_opnsense` n'envoie `destination_port` que pour TCP et UDP : une regle ICMP ouvre le PROTOCOLE entre deux pairs, pas le seul type declare. Le type reste porte par la regle d'HOTE, que `resoudre_flux` emet precisement (`icmp type echo-request accept`). La frontiere dit qui peut parler a qui ; l'hote dit ce qu'il accepte d'entendre. Emettre un `icmptype` a la frontiere aurait ete plus fin — et aurait demande de traduire `frag-needed` (un CODE de `destination-unreachable`) dans le vocabulaire de pf, au risque de casser le PMTUD pour gagner de la precision sur une barriere qui n'est pas la derniere. **Mesure apres application :** ping site-mon-01 -> les six autres zones : 6/6 repondent hotes UP : 1/7 -> 7/7 ping4 : 1/7 -> 7/7 certificat 7/7, sante 7/7 ## 2026-09-09 (7) — Le contrat des sondes ETAIT celui de Nagios, sans le savoir Question posee : « tu connais le paquet `monitoring-plugins` ? » Oui — et le contrat que `docs/supervision-conception.md` decrivait quelques heures plus tot EST le sien, mot pour mot : une ligne sur stdout, `0/1/2` en code de sortie. Je l'avais donc **reinvente sans le nommer**, alors que `positionnement.md` dit exactement l'inverse : *adopter aux seuils, ne pas reimplementer*. Le document nomme desormais l'**API des greffons Nagios**, ajoute `3 = INCONNU` et la partie `| metriques` qui manquaient, et dit la consequence : **un greffon standard EST une sonde valide, sans la moindre colle**. Le paquet en fournit 54 — `check_disk`, `check_load`, `check_procs`, `check_ntp_time`, `check_smtp`, `check_pgsql`... On n'ecrit du shell que lorsque la verite a mesurer est propre a Set-OPS, comme `client_pki/certificat`, qui compare l'empreinte SERVIE a celle du disque : aucun greffon ne sait ca. Le porteur separe maintenant le texte des metriques (`performance_data`), parce que c'est ce que le contrat porte et qu'Icinga sait les tracer. ### Essayer un vrai greffon a revele DEUX defauts du porteur **1. Un envoi refuse faisait taire toutes les sondes suivantes.** `rapporter` sortait en `exit 1` : une sonde deposee mais non declaree — donc refusee par le filtre d'API — supprimait le rapport de toutes les autres, `sante` comprise. Le tableau ne devenait pas rouge, il devenait VIDE, et le `ttl` le perimait des heures plus tard sans dire pourquoi. *Un rapporteur qui ne peut pas dire UNE chose doit quand meme dire les autres.* **2. Aucun delai de garde sur les sondes.** Et ce n'est pas theorique : check_disk 2.4.0-3+deb13u1 sur mon-01 : etat R, ne rend jamais la main quels que soient ses arguments (-p /, sans -p, seuils en % ou en G) Un greffon STANDARD, sur une machine saine, qui **boucle**. Sans delai de garde il figeait le rapport entier, toutes les quinze minutes, indefiniment. Chaque sonde tourne desormais sous `timeout 20` ; au-dela, on rapporte INCONNU en le disant. *La lecon n'est pas « les greffons standards sont mauvais ».* C'est qu'adopter un standard ne dispense pas de l'eprouver — et que le porteur doit survivre a une sonde qui se comporte mal, standard ou maison. ### Trois voyants rouges permanents, sur l'hote de supervision Constate en cherchant : la configuration d'exemple livree par Icinga surveille `localhost` et crie en permanence sur `mon-01` — swap : SWAP CRITICAL - 0% free (une VM sans swap) http : connect to 127.0.0.1:80 (rien n'ecoute la) apt : 1 package upgradable Trois alarmes qui ne peuvent que rester rouges, dans le seul endroit qui doit rester lisible. **Non corrige** — c'est une decision d'exploitation, pas une correction de code. ## 2026-09-09 (6) — La supervision se DECLARE dans le role, comme les flux **64 preuves (P01-P64).** Nouveau document : `docs/supervision-conception.md`. Premiere sonde : `client_pki/certificat`, **14/14 OK** sur la flotte. ### Le constat qui a decide Mesure du depot sur lui-meme, au 2026-09-09 : 39 roles declarent leurs flux -> nftables + OPNsense, derives 32 declarent leur empreinte -> ressources des VM, derivees 32 declarent leur authentification -> habilitations, derivees 19 groupes declarent `surveillance:` -> RIEN Les dix-neuf lignes `surveillance:` de `docs/dependances-groupes.yml` sont ecrites, versionnees, relues — et **aucune n'etait executee**. Icinga surveillait deux choses. C'est la classe d'echec de ce depot, en version documentaire : la carte dit ce qui est surveille, et personne ne surveille. *Une intention ecrite n'est pas une mesure.* ### Le mecanisme Un role declare ses sondes dans `meta/supervision.yml` (nom, `ttl`, raison) et **depose lui-meme** son script dans `/usr/local/lib/setops/sondes/` — il connait ses chemins, ses seuils, son consommateur. Le porteur (`client_sante`) fait tourner tout ce qui vit dans ce repertoire et pousse un resultat passif par sonde ; il ne sait pas ce qu'elles mesurent, et c'est le but. `serveur_icinga` derive les `object Service` ET **le filtre de permission d'API** des memes declarations. Ajouter une sonde ne demande de toucher ni au porteur, ni a Icinga. Comme un flux declare devient une regle nftables. ### La sonde du certificat, et ce qu'elle mesure VRAIMENT Nos certificats vivent **24 heures**. Une machine dont le renouvellement s'arrete ne casse pas tout de suite : elle casse le LENDEMAIN, et tout ce qui parle TLS avec elle tombe ensemble. Ce qu'on ne mesure pas, et c'est delibere : *« le minuteur est-il actif ? »* — un minuteur vert qui echoue chaque nuit est exactement le mensonge deja paye avec les sauvegardes. *« le certificat existe-t-il ? »* — un certificat perime existe. Ce qu'on mesure : les heures restantes sur le certificat REELLEMENT pose, la chaine verifiee contre notre racine, et — quand un service le consomme — l'empreinte SERVIE comparee a celle du disque. ### Trois obstacles, et le premier est le plus instructif **La sonde a rendu 14 machines sur 14 en CRITIQUE — sur une PKI qui se portait tres bien.** Elle utilisait `openssl verify -CAfile racine` ; or nos certificats d'hote sont signes par un INTERMEDIAIRE, et seule la racine vit sur le disque. `step certificate verify`, l'outil que le role installe deja, repond VALIDE. *Une alarme toujours allumee ne vaut pas mieux qu'une alarme jamais allumee : elle apprend a ne plus regarder.* Une sonde se prouve donc DEUX FOIS — verte sur le sain, rouge sur le casse. Le document de conception porte la regle. **Le filtre d'API etait ecrit avant la lecture des declarations.** Les services existaient, et Icinga aurait refuse leurs resultats avec un « 404 No objects found » sur des objets bien presents — une heure perdue sur ce message le 2026-09-02. La lecture est remontee avant le compte d'API, avec le pourquoi ecrit la ou l'on serait tente de la redescendre. **Mon controle negatif a casse un service reel.** Substituer le certificat d'hote par un auto-signe a fait virer la sonde au rouge, comme voulu — et le script de synchronisation a propage ce certificat vers `node_exporter`, dont la clef etait restee l'ancienne : `tls: private key does not match public key`. Un service tombe pour eprouver une sonde. La regle qui en decoule est au document : **un controle negatif se fait sur une copie**, jamais en substituant l'artefact que d'autres consomment. ### Les quatre etats, tous eprouves certificat absent -> [2] CRITIQUE signe par une autre autorite -> [2] CRITIQUE (24 h restantes : c'est la CHAINE qui parle) sous le seuil d'avertissement-> [1] AVERTISSEMENT etat sain -> [0] OK Puis relu dans IcingaDB : `certificat : 14/14 OK`, `sante : 14/14 OK`. ### P64 tient les deux bouts Une sonde DECLAREE dont le script n'est pas depose donnerait un service qui n'a jamais de resultat. Un script DEPOSE que rien ne declare serait refuse par le filtre d'API. Les deux se rompent seuls, la preuve tient les deux — plus la presence d'une `raison` et d'un `ttl`. Trois controles negatifs rejoues. Ce qu'elle ne fait pas, et qu'aucune lecture statique ne fera : juger qu'une sonde MESURE quelque chose. Ca reste au controle negatif de chacune, exige par le document et trace ici. ### Et une QUATRIEME fois la meme lecon : les seuils criaient trop tot Deployee sur le site, la sonde a rendu **5/7** — deux machines en AVERTISSEMENT, sur des certificats parfaitement sains. Le seuil etait a 12 h. Or `cert-renewer@.service` porte `ExecCondition=step certificate needs-renewal`, qui dit oui au TIERS RESTANT : **8 h** pour un certificat de 24 h, reessaye toutes les 15 minutes. La sonde criait donc AVANT que le mecanisme ne soit cense agir. Les seuils sont desormais SOUS le point de renouvellement — 6 h (le renouvellement est du depuis deux heures, huit fenetres manquees : ce n'est plus un hoquet) et 3 h. *Un seuil au-dessus du point de renouvellement ne previent pas : il ment.* Un seuil ne se choisit pas a l'intuition : il se DERIVE du moment ou le mecanisme surveille est cense agir. C'est la meme faute que `openssl verify` quelques heures plus tot, sous une autre forme — et c'est encore la flotte reelle qui l'a dite. **Etat final : 14/14 au tenant, 7/7 au site**, sur `certificat` comme sur `sante`. ### Reste Dix-huit groupes attendent encore leur sonde. Le mecanisme, lui, ne demande plus rien. ## 2026-09-09 (5) — Retrait d'une garde qui ne pouvait pas se declencher `client_sante` portait une branche « aucun `serveur_icinga` : rien a poser », et un `when` sur tout son bloc. **Ni l'une ni l'autre ne pouvait s'executer.** Eprouve sur le modele public, dans une copie jetable : `instancier` ne pose une integration UNIVERSELLE que si son serveur existe dans l'ecosysteme. Sans Icinga au plan, le groupe `client_sante` est **absent** de l'inventaire genere — exactement comme `client_metrique` et `client_journal` le sont deja. Le role n'est donc jamais appele sans destinataire, et sa branche defensive etait du decor. *Une garde qui ne peut pas se declencher n'est pas une garde : elle rassure sans rien tenir.* Et elle coute deux fois — a la lecture, puis le jour ou l'on cherche pourquoi rien n'a alerte. Ce qui la remplace se declenche vraiment, et les deux cotes sont eprouves : -e client_sante_icinga_hote='' -> FAILED (assertion sur l'hote) -e client_sante_icinga_motdepasse='' -> FAILED (assertion sur le secret) L'echec dit LEQUEL des deux manque. Le bloc, prive de sa condition, est aplati : douze taches a plat au lieu de onze indentees sous un `when` toujours vrai. Rejoue sur les deux flottes : **zero changement sur zero hote**. `make prouver` : CONFORME, 63 OK, 0 echec, 0 saute. ## 2026-09-09 (4) — `systemctl --failed` entre dans la supervision **63 preuves. `make prouver` : CONFORME, 63 OK, 0 echec, 0 saute.** Nouveau role `client_sante`, integration **universelle** (67 roles). ### Ce qu'il ferme Le defaut trouve quelques heures plus tot : `openipmi.service` echouait a CHAQUE demarrage sur les quatorze machines depuis le 2026-09-02, et `systemctl --failed` rendait ZERO partout — non parce qu'elles allaient bien, mais parce qu'AUCUNE n'avait redemarre depuis. Il a fallu qu'un humain redemarre une machine pour que le defaut existe aux yeux de quelqu'un. *Un controle qui ne peut echouer qu'au demarrage ne mesure rien tant que rien ne demarre.* ### PASSIF, et a duree de vie Un controle ACTIF ne voit pas la machine MUETTE : si elle ne repond plus, la sonde echoue et on met ca sur le compte du reseau. Ici c'est le NOEUD qui parle, et le `ttl` de son envoi fait la fraicheur — sans nouvelle, Icinga perime le service tout seul. **Le silence alerte autant que l'echec**, et le silence est precisement ce qui n'a alerte personne. Le minuteur declenche **au demarrage** (`OnBootSec=2min`) autant que toutes les 15 min : les echecs de cette famille NAISSENT au boot, et attendre le premier quart d'heure laisserait une fenetre pendant laquelle la machine est en panne et le tableau au vert. **CRITIQUE des la premiere unite**, jamais un seuil. Une unite en echec est soit un vrai probleme, soit du bruit a retirer : dans les deux cas il faut agir. Un seuil ferait vivre le bruit indefiniment — exactement ce qu'on vient de corriger. Les tolerances se nomment **une par une** (`client_sante_unites_tolerees`, vide par defaut). Jamais un motif large : un filtre qui cache une unite en cache d'autres, et on ne s'en apercoit que le jour ou l'on cherche pourquoi rien n'a alerte. ### Le controle negatif Une garantie qu'on n'a jamais vue echouer n'est pas une garantie. Unite factice posee sur `obs-01`, rapport rejoue, etat relu dans IcingaDB : obs-01 sante CRITICAL 1 unite(s) systemd en echec : setops-controle-negatif.service (13 autres) sante OK Aucune unite systemd en echec. Le verdict NOMME l'unite. Apres nettoyage : **14/14 OK**. ### Un conflit evite de justesse `setops-sauvegardes.conf` definissait les `object Host`. Un second fichier de controle aurait redefini les memes, et **Icinga refuse un objet en double** : la configuration entiere aurait ete rejetee, donc AUCUNE supervision — en voulant en ajouter. Les hotes vivent desormais dans `setops-hotes.conf`, definis UNE fois ; les fichiers de controle n'attachent que des services. Verifie : 14 hotes, 14 services, zero doublon, `icinga2 daemon -C` valide. ### Trois obstacles, et ce qu'ils apprennent **`${#tableau[@]}` contient `{#`**, que Jinja lit comme un debut de commentaire — le rendu echouait sur « Missing end of comment tag ». Le shell a besoin de `${#...}` ; c'est donc a Jinja de s'ecarter (`#jinja2: comment_start_string:...` en tete du gabarit). **Ma premiere sonde a traduit un refus en « 0 service ».** Le compte `setops-depot` ne peut que POSER un resultat, pas lire — moindre privilege voulu. L'API rendait `{"error":403,"status":"Missing permission: objects/query/service"}`, et mon script, qui cherchait une clef `results`, a affiche « 0 service(s) sante ». *Encore un « echec » qui ecrasait « permission ».* L'etat se lit dans IcingaDB, pas par ce compte. **Et un echec `apt` transitoire** sur `mon-01` : `503 unexpected range` puis « Message has been manipulated » sur la signature de `debian-security`. Alarmant a la lecture, local a une machine (13 autres propres), et disparu au second essai — un hoquet du cache apt-cacher-ng, pas un incident de signature. Mesure avant conclusion : les deux mandataires servaient le meme contenu, au meme condensat. ### Le meme compte d'API, elargi de deux noms a trois Un second compte serait plus pur — un secret par usage. Il exigerait une clef de voute de plus dans CHAQUE ecosysteme, donc un geste manuel a chaque nouvel ecosysteme, pour une portee identique : ce compte ne peut deja que poser un resultat passif, sur des services NOMMES. Le filtre passe de `sauvegarde: *` / `sauvegarde` a ces deux-la plus `sante`. Aucun pouvoir nouveau. ### Le SITE : fait, et il a fallu deux corrections pour que ca MARCHE 7 machines, 0 echec au deploiement — et **cinq rapports sur sept en `TimeoutError`**. *Un playbook vert ne prouve pas qu'une chose fonctionne.* `make site-appliquer GROUPE=client_sante` rendait 7/7, 0 echec, sur un flux qui ne passait pas. Seul l'essai de bout en bout l'a dit. **1. Le pair de la frontiere.** Le role client declarait son `egress` 5665, la politique de sortie etait `accept`, la regle d'entree de l'hote autorisait bien la source — et les paquets mouraient ENTRE les deux. La frontiere filtre le trafic inter-zones du site et ne connait que ce que le registre lui dit ; or elle ne resout que les roles que les machines PORTENT AU PLAN. Une integration universelle n'y figure pas : elle est derivee, pas declaree. `pair: client_sante` produisait donc une regle est-ouest correcte et AUCUNE regle a la frontiere. Le pair est desormais `serveur_debian` — le vocabulaire du depot pour « tout noeud », que le generateur de frontiere traite deja comme tel, et qui est exact au sens strict : tout noeud rapporte sa sante. Six regles creees, zero retiree, une par patte de zone. Le meme piege explique pourquoi le flux `client_backup` juste au-dessus ne suffisait pas seul : c'est `serveur_backup`, role REEL, qui ouvrait la porte pour le depot. **2. Le rapporteur s'accusait lui-meme.** Pendant l'heure ou le pare-feu bloquait, `setops-sante.service` a echoue — et une fois debloque, cinq machines ont rapporte CRITIQUE en citant... leur propre rapporteur. Le blocage corrige, l'accusation restait : systemd garde l'etat `failed` jusqu'a un `reset-failed`. Un rapporteur qui trebuche une fois s'accuserait indefiniment. Sa propre unite est donc exclue du compte, et ce n'est pas se menager : **sa sante est deja mesuree, et mieux, par la FRAICHEUR de ses envois**. S'il ne peut plus parler, le `ttl` perime le service — ce qui se voit precisement quand il ne peut PAS ecrire, alors que sa propre unite en echec ne se voit que quand il le peut. Se compter soi-meme, c'est mesurer deux fois la meme chose, dont une fois mal. **Etat final** : **7/7 au site, 14/14 au tenant**, tous OK. Controle negatif rejoue sur les deux flottes. ## 2026-09-09 (3) — Le redemarrage a tenu, et il a montre autre chose ### Ce que le redemarrage prouve `obs-01` a redemarre **avec son disque cloud-init retire de Proxmox** — donc sans paquet ET sans source de donnees. Elle est revenue avec son adresse (`10.17.20.11/24`), sa passerelle (`10.17.20.1`) et son resolveur (`10.0.34.11`). C'est la confirmation que les mesures annoncaient : `/etc/network/interfaces.d/50-cloud-init` n'appartient a aucun paquet, il survit, et `ifupdown` le relit au demarrage. Le retrait de cloud-init est desormais **prouve par un demarrage reel**, pas seulement par inference. ### Ce que le redemarrage a REVELE, et qui n'a rien a voir Une unite en echec : `openipmi.service`. openipmi[655]: Starting ipmi drivers ipmi failed! systemd[1]: Failed to start openipmi.service La chaine : `prometheus-node-exporter` **recommande** `prometheus-node-exporter-collectors`, qui **recommande** `ipmitool`, qui **recommande** `openipmi`. Trois recommandations en cascade, chacune raisonnable sur du metal. Dans une VM il n'y a pas de BMC — `/dev/ipmi*` n'existe pas — et le script d'init echoue a chaque demarrage. **LE POINT N'EST PAS LE PAQUET, C'EST POURQUOI PERSONNE NE L'AVAIT VU.** `openipmi` est arrive le **2026-09-02**, avec la reconstruction. `systemctl --failed` rendait pourtant ZERO sur les quatorze machines — non parce qu'elles allaient bien, mais parce qu'aucune n'avait **redemarre** depuis. Six jours et vingt heures pour `obs-01` (`last -x reboot` : boot du 2026-09-02 19:24, jusqu'a aujourd'hui 16:10). *Un controle qui ne peut echouer qu'au demarrage ne mesure rien tant que rien ne demarre.* C'est la meme famille que le cheque vert sur un perimetre vide, avec le temps comme perimetre. Et le degat n'est pas cosmetique : une unite en echec **permanent** use le seul signal qui devrait alerter. Le jour ou une vraie unite tombe, `systemctl --failed` rend « 2 » au lieu de « 1 », et personne ne fait la difference. ### La correction, a la source et conditionnelle `client_metrique` retire `ipmitool` et `openipmi` — mais **seulement dans une VM** (`ansible_facts.virtualization_role == 'guest'`). Sur du metal le collecteur IPMI est legitime : c'est la raison meme de la recommandation. Le role s'appuie sur ce qu'Ansible SAIT de la machine, pas sur une supposition. Ils ne sont que RECOMMANDES, donc les retirer n'emporte pas `prometheus-node-exporter-collectors`, dont les collecteurs textfile servent vraiment. Un handler efface l'etat `failed` laisse derriere : retirer le paquet ne l'efface pas, systemd le garde jusqu'au prochain demarrage. Sur une flotte qui ne redemarre pas, ce serait conserver par inadvertance exactement le bruit qu'on vient de supprimer. Applique aux quatorze : `ipmi=0`, `collectors=1`, `node_exporter=active`, `echecs=0` partout. Second passage : **zero changement sur zero hote** — idempotent. ### Ce qui reste ouvert Rien dans le harnais ne regarde `systemctl --failed` sur la flotte. Ce defaut-ci a ete trouve **parce qu'un humain a redemarre une machine**, pas parce qu'une mesure l'a dit. Les treize autres portaient la meme unite condamnee, silencieuses. ## 2026-09-09 (2) — Le wiki d'`origin` : une garde qui bloquait au lieu de proteger **Les deux forges servent desormais le meme wiki**, 26 pages, contenu identique au fichier pres (`diff -rq` : aucune difference). ### Ce que je disais, et qui etait faux « Il n'y a rien sur `origin` » — non. **Le depot y est, et a jour** : `git ls-remote` rend exactement notre HEAD. C'est le WIKI qui etait vide. L'ecart entre « le depot est absent » et « son wiki n'a pas de branche » est tout l'ecart entre une panne et une formalite, et je l'avais recopie d'une session precedente sans le remesurer. ### La garde avait raison de refuser, et tort de s'arreter la `make wiki-publier` refuse un wiki VIDE : sans commit il n'a aucune branche, et publier en inventerait une — `master` sur ce poste, alors que le wiki en service vit sur `main`. Le message disait « creer une premiere page dans l'interface Forgejo ». Or l'interface n'est pas joignable depuis ce poste, et ce n'est pas une panne : la forge du site n'accepte le 443 que des machines qui DECLARENT le flux. **Forgejo declare pourtant la reponse.** `GET /api/v1/repos/genome/set-ops-public` rend `wiki_branch`. Interroge depuis `ops-01` — une machine qui a le flux — il repond `main`. On ne devinait donc pas : on ne demandait pas. `WIKI_BRANCHE=` a ete ajoute a la recette. Le refus reste le DEFAUT ; qui a la reponse peut la donner. Les deux cotes sont eprouves : un wiki vide sans la variable est refuse, avec `WIKI_BRANCHE=main` il est initialise sur `main` exactement. ### Au passage, une lecon d'instrument Trois sondes fausses avant la bonne. Un `connect()` direct sur `10.0.33.11:443` depuis le poste rend `TimeoutError` — route presente, paquets avales : une POLITIQUE, pas une route manquante. Un tunnel par la frontiere ne repondait pas davantage. Et pendant ce temps `git ls-remote` fonctionnait tres bien, parce que `~/.ssh/config` passe par un `ProxyJump ansible@10.17.0.1` que la sonde ignorait. L'instrument juste etait `ops-01` : la machine dont le flux vers la forge est DECLARE. Elle repond `HTTP 200` en 443, et 22 muet — l'exact inverse du poste. *Verifier d'ou l'instrument mesure*, encore une fois. ## 2026-09-09 — cloud-init nait avec la VM et ne lui survit pas **63 preuves (P01-P63). `make prouver` : CONFORME, 62 OK, 0 echec, 1 saute.** ### Le second maitre cloud-init n'est pas un logiciel d'installation : c'est une **source de verite externe**. Il ne s'arrete pas apres la premiere seconde — il se reveille a CHAQUE demarrage et relit le lecteur cloud-init attache par l'hyperviseur, lequel peut redefinir les comptes, les cles SSH autorisees, les mots de passe et le reseau. Sur une machine que le plan possede desormais, c'est un second maitre : le plan ne le decrit pas, `make valider` ne le mesure pas, et il parle en premier. Sa tache est pourtant finie a la premiere seconde. C'est precisement parce qu'il a REUSSI a poser l'adresse, le nom et les cles d'hote qu'Ansible a pu entrer. ### Trois moities, et elles se defont separement 1. le **gabarit** le garde. Sans lui, un clone n'a ni adresse ni nom : il ne nait pas. 2. le **socle** ne l'installe plus. Le garder produisait un va-et-vient a chaque deploiement — le socle installe, le durcissement retire, deux `changed` par passage, l'idempotence perdue et `make valider` bruyant pour rien. 3. le **durcissement** le retire (`roles/cloud_init_retrait`, dernier role de `serveur_durci`, apres que tout le reste soit pose). Une seule des trois qui bouge et la decision devient son contraire en silence : un gabarit sans cloud-init donne des VM mortes ; un socle qui le reinstalle rend le pouvoir a chaque passage ; un durcissement qui ne le retire plus laisse le second maitre en place. **P63** garde les trois, plus le contenu du role — un role vide passerait les trois premiers controles sans rien fermer. Les quatre controles negatifs ont ete rejoues. ### Ce qui rend le retrait sur est MESURE, pas suppose L'adresse d'une VM vit dans `/etc/network/interfaces.d/50-cloud-init`. Le risque evident etait que le retrait l'emporte — une machine qui perd ce fichier ne se plaint pas : elle repart au prochain demarrage sans adresse, et plus personne ne peut entrer pour le constater. Mesure du 2026-09-09 sur `obs-01` : dpkg -S /etc/network/interfaces.d/50-cloud-init -> aucun paquet ne le possede /var/lib/dpkg/info/cloud-init.postrm -> ne nomme jamais interfaces.d Le fichier survit donc, meme en `purge`, et `interfaces` fait toujours `source interfaces.d/*`. Son en-tete annonce que les modifications « ne persistent pas au redemarrage » : c'etait vrai TANT QUE cloud-init pouvait le reecrire. Le paquet parti, plus personne ne le reecrit. **Le role le verifie quand meme**, avant et apres, et n'accuse que si le retrait l'a emporte — une machine qui n'a jamais eu ce fichier ne doit pas faire echouer le durcissement. Essai a blanc sur `obs-01` : `cloud-init*` a retirer, configuration reseau intacte, drapeau pose. ### Deux choix dits franchement **`cloud-guest-utils` reste.** C'est `growpart` : ni service, ni port, ni source de donnees. Le retirer ne fermerait aucune surface, et il faudrait le reinstaller au premier agrandissement de disque. **Les orphelins ne sont pas retires par defaut.** Le retrait laisse ~29 paquets qui n'etaient la que pour cloud-init (`python3-jsonschema`, `python3-jinja2`, `python3-oauthlib`...). Ce sont des bibliotheques : elles pesent, elles n'ouvrent rien. `autoremove` calculerait exactement quoi retirer — a partir des drapeaux « installe manuellement » de dpkg, et une machine dont ces drapeaux ont derive y perdrait autre chose. Un durcissement ne doit pas pouvoir surprendre. `cloud_init_retrait_autoremove` existe pour qui veut, en connaissance de cause. ### Et un drapeau, pour le retour par dependance `cloud-init` peut revenir en recommandation d'un autre paquet. `/etc/cloud/cloud-init.disabled` est lu par cloud-init lui-meme au demarrage et l'arrete avant qu'il ne lise la moindre source de donnees. ### Ce que cette decision NE ferme PAS Ajoute le meme jour, apres la question « cloud-init est-il vraiment une valeur ajoutee ici ? ». La premiere redaction de D-85 se lisait comme une emancipation. Elle n'en est pas une, et il valait mieux le dire que de laisser un lecteur presse en conclure trop. **Retirer cloud-init n'ote AUCUN pouvoir a l'hebergeur.** `qemu-guest-agent` est au gabarit — il doit y etre (P56 : c'est par lui que `creer-vm` confirme la materialisation sans entrer chez le tenant). Releve du 2026-09-09 sur `edge-mta-01`, avec le jeton d'API du site, les sous-chemins que l'API expose sur son dos : exec · exec-status · file-read · file-write · set-user-password · shutdown · ... C'est un pouvoir STRICTEMENT PLUS GRAND que le lecteur cloud-init, et il est deja la, sur chaque VM vivante de la flotte. Ce que D-85 ferme est donc precis et etroit : 1. une reapplication AUTOMATIQUE, a chaque demarrage, depuis un support que le plan ne possede pas et qu'aucune preuve ne lit ; 2. le code de cloud-init lui-meme — un interpreteur Python complet execute en root au demarrage, et ses ~29 dependances. La mainmise d'un hyperviseur sur ses invites est une propriete de la VIRTUALISATION, pas de cloud-init. Elle appelle sa propre decision, qui n'est pas prise. ### Le seuil ou le remplacer deviendrait juste L'alternative existe et sa piece est deja au gabarit : ecrire l'adresse et la cle par `agent/file-write` + `agent/exec` depuis l'hyperviseur, sans reseau. Aujourd'hui ce serait REIMPLEMENTER UN STANDARD — ce que `positionnement.md` interdit — et echanger un mecanisme eprouve contre du code maison au moment le plus fragile, dont le mode de panne est le pire : une VM injoignable. Le jour ou une premiere seconde ne pourra plus etre amorcee par Proxmox — autre hyperviseur, metal nu, hebergeur sans API — ce chemin cesse d'etre une reimplementation et devient LE CHEMIN PORTABLE. C'est l'axe emancipation que le depot suit deja. Le seuil est nomme ici pour ne pas etre franchi sans le voir. ### Deploye le 2026-09-09 — 14/14 Applique a la flotte de Chezlepro apres confirmation explicite. `serveur_durci` : **14 hotes, 0 echec, 0 injoignable**. Ordre suivi : `obs-01` seul d'abord, verifie, puis les treize autres. Releve sur les quatorze machines apres coup : paquet=0 unites=0 reseau=oui drapeau=oui networking=enabled echecs=0 et, sur chacune, `ifquery` rend EXACTEMENT l'adresse vive (`10.17.20.11`, `10.17.21.11`...). C'est le point : `ifquery` ne lit pas la memoire du systeme, il **relit le fichier** que le retrait aurait pu emporter — donc il repond ce que le prochain demarrage fera. Second passage sur `obs-01` : `changed=1`, et ce seul changement est la tache `nftables_baseline` qui se declare toujours modifiee, anterieure a ce chantier. Le retrait est idempotent. `make valider` apres deploiement : **0 echec, 0 injoignable** sur les quatorze. **CE QUI N'A PAS ETE PROUVE, ET IL FAUT LE DIRE.** Aucune machine n'a ete REDEMARREE. Le redemarrage etait le controle que je voulais faire — la garde de securite du poste l'a refuse, et on ne contourne pas une garde. Le chemin de demarrage a donc ete prouve AUTREMENT, sans redemarrer : `networking.service` (ifupdown) est le service actif, `systemd-networkd` est desactive, NetworkManager absent ; `ifquery` relit le fichier et rend la bonne adresse ; zero unite cloud-init subsiste dans `systemctl list-dependencies multi-user.target` ; zero unite en echec ; le nom d'hote et les trois cles d'hote SSH persistent hors de cloud-init. C'est fort, ce n'est pas un redemarrage. La confirmation finale tient en une ligne, a lancer par un humain sur une machine de son choix. ### Le SITE : fait le 2026-09-09 — 7/7 `make site-appliquer GROUPE=serveur_durci` : **7 hotes, 0 echec, 0 injoignable**. Configuration reseau intacte sur les sept, `ifquery` rend l'adresse attendue sur chacune (`10.0.31.11`, `10.0.33.11`...), zero unite cloud-init restante, zero unite en echec. Le site HEBERGE — c'est ce qui rendait l'operation plus lourde que chez le tenant. Verifie apres coup, depuis les machines qui ont le flux : la forge repond `HTTP 200` et sert toujours ce depot au bon commit ; le cache APT repond `HTTP 200` ; le DNS resout `forge.genese.internal` ; step-ca rend `{"status":"ok"}`. Le site ne portait pas le piege `openipmi` : il n'applique pas `client_metrique`. *(Ce paragraphe remplace celui qui disait le changement « arme, pas applique » — et qui disait aussi, a tort, qu'il fallait passer par `site-ops-01`. `make site-appliquer` fonctionne depuis le poste.)* ### Trouve en verifiant : `site-backup-01` n'a jamais recu `client_pki` Elle est DANS le groupe `client_pki` — et elle n'a ni `/etc/step`, ni minuterie de renouvellement, ni meme la racine de l'AC dans `/usr/local/share/ca-certificates/`. Les six autres machines du site ont leur certificat et leur minuterie quotidienne, qui tourne. L'appartenance au groupe dit « couverte ». La machine dit le contraire. C'est la meme famille que le reste : *un perimetre declare n'est pas un perimetre mesure.* Sans rapport avec le retrait de cloud-init — trouve en le verifiant. **Corrige le meme jour** : `make site-appliquer GROUPE=client_pki` — 7 hotes, 0 echec. `site-backup-01` a recu quatorze changements : depot apt Smallstep, `step-cli`, STEPPATH, mot de passe du provisioner, **etablissement de la confiance dans l'AC**, certificat d'hote emis, unite et minuteur de renouvellement, actives. Verifie apres coup sur les sept : chaine = VALIDE (`step certificate verify` contre la racine locale) racine = /etc/step/certs/root_ca.crt presente partout timer = cert-renewer@.timer [enabled/active], prochaine passe ce soir `site-pki-01` est la seule sans ancre dans `/usr/local/share/ca-certificates/`, et c'est juste : elle PORTE l'autorite, elle ne s'enrole pas aupres d'elle-meme. *Trois de mes sondes ont menti avant la bonne* : `is-enabled` sur un nom d'unite devine, puis la colonne « service active » lue a la place de la minuterie — un service declenche par minuteur est normalement `disabled`, ce qui se lit comme une panne quand on regarde la mauvaise ligne. La question etait pourtant serieuse : sans minuteur, les certificats du site expiraient dans 24 h. Et le `changed=2` que les six autres machines rapportaient n'etait pas une non-idempotence : c'etait le depot initial de `step-cli`. Rejoue sur `site-forge-01` : **`changed=0`**. ### Ce qui restait a faire (historique) `SITE-Chezlepro` est **une autre instance de ce depot** — meme `playbooks/groupes/serveur_durci.yml`, memes roles. Ses sept machines actives (`site-ops-01`, `site-cache-01`, `site-forge-01`, `site-pki-01`, `site-dns-01`, `site-backup-01`, `site-mon-01`) sont durcies depuis le 2026-09-02 : leur prochain passage de `serveur_durci` retirera cloud-init chez elles aussi, sans que personne ne le decide a nouveau. Ce n'est pas un oubli, c'est un fait a connaitre : le site se deploie depuis SON runner (`site-ops-01`), pas depuis ce poste, et son inventaire n'est meme pas genere ici. Y aller est un acte distinct, sur une machine distincte, et le site porte la forge qui sert ce depot et le cache qui nourrit `apt` — un rayon d'action different. ## 2026-09-08 (4) — Les SIX registres ont un formulaire genere **62 preuves. `make prouver` : CONFORME, 61 OK, 0 echec, 1 saute.** `CHAMPS_ECRITS_A_LA_MAIN` est **vide** : plus un seul champ recopie a la main. ### L'epreuve qui compte Ouvrir chaque vue et enregistrer **sans rien toucher** doit renvoyer au serveur exactement le plan qu'on vient de lire. C'est ce qui separe un formulaire genere d'un formulaire qui a l'air genere : un champ visible a l'ecran et perdu en silence a l'enregistrement serait le pire des deux mondes. /api/serveurs 14 entite(s) IDENTIQUE /api/applications 25 entite(s) IDENTIQUE /api/domaines 2 entite(s) IDENTIQUE /api/bases 4 entite(s) IDENTIQUE `test_rendu_gui.py` le mesure desormais a chaque `make prouver`. ### Trois defauts trouves en chemin **Le formulaire annoncait des defauts inventes.** « 2048 » pour la memoire, « 2 » pour les coeurs, « 16G » pour le disque. Il n'existe aucun defaut fixe : `deriver_ressources` calcule la taille depuis les ROLES que l'hote porte — 1024 Mo et 1 coeur pour `infra-pki-01`, 5632 et 4 pour `collab-01`. Un repere faux est pire qu'aucun : il fait croire qu'on connait la valeur. Le schema nomme maintenant le champ derive (`x-defaut-derive`), et le formulaire affiche la valeur REELLE de cet hote. De meme, l'option vide d'un `` sans hôte fantôme) ; la flotte et la bascule. - **Glossaire** — 24 concepts en une phrase (était « à venir »). Raccordées dans `Home.md` (deux unités-pilotes : services **et** méthode) et `_Sidebar.md` (section « Flotte & preuve »). Fidèle à la doctrine : le wiki pointe vers `docs/`, ne recopie pas. Publié via `make wiki-publier`. Wiki : 855 → 1573 lignes, 22 pages, aucun lien mort. ## 2026-07-23 (suite 4) ### Ajouté — créer un MODÈLE (`make model-creer`, dépôt privé) Symétrique de `instance-creer`, mais produit un modèle réutilisable dans le dépôt PRIVÉ (`Set-OPS-Modeles/`, jamais `exemples/modeles/`). `scripts/model_creer.py`, deux modes : - **`MODE=base BASE= NOM=`** — copie un modèle déjà générique (copier + éditer). - **`MODE=instance SOURCE=OPS- NOM=`** — **promeut une instance éprouvée en modèle** : généralise l'identité (`domaine → exemple.internal`, organisation → `Exemple`, realm → `exemple`), fixe `index → 1`, `setops_production → false`, `nftables_admin_ssh → []`, vide la clé publique de sauvegarde, générique `proxmox.yml` (host/nœud/stockage/VMID vidés, golden template → `modele-debian13`). Sûreté : **aucun secret** ne sort (`vault.yml`/`proxmox.vault.yml` jamais copiés ; refus si l'un subsiste ; le `.example` est conservé). Copie **ciblée** (plan/ + inventories/ seulement, symlinks résolus) — robuste aux dépôts imbriqués et boucles de symlinks. Le modèle produit **doit valider** (`modeles.py verifier`), sinon il est annulé. `make model-creer`. Validé : les deux modes testés en isolement (destination tmp, dépôt privé jamais touché) — promotion du labo (cohérent) réussie + validée, aucun `chezlepro` résiduel, aucun secret, `proxmox.yml` génériqué ; copie base (socle) OK ; refus d'une instance INVALIDE (le garde-fou a détecté un hôte fantôme dans une instance en cours d'édition). `node --check`, `make verifier` rc=0 CONFORME 21/21. ## 2026-07-23 (suite 3) ### Ajouté — créer une instance depuis un modèle (CLI + GUI) Le moteur est indépendant des instances (le symlink `instance/` est gitignoré, aucun artefact d'instance n'est committé). Créer une instance existait seulement à la main (`cp -r` + `ln -s`, QUICKSTART). C'est désormais une capacité de premier ordre. - **`scripts/instance_creer.py`** — copie un modèle (`exemples/modeles/*` + `SETOPS_MODELES`) vers un dépôt frère `../`, y fixe l'`index` (le seed), et **refuse** : un nom déjà existant (rien n'est écrasé), un modèle inconnu, un **index en collision** avec une instance fédérée (le garde-fou vérifie AVANT toute copie). Le `.git` et `hosts.genere.yml` du modèle ne sont pas copiés. - **`make instance-creer NOM=OPS-X MODELE=socle [INDEX=N]`** et **`make instance-modeles`** (liste les modèles + les index déjà pris). - **GUI, vue Réseau** — formulaire « Créer une instance depuis un modèle » sous la flotte : modèle (liste), nom, index. `POST /api/instance-creer` ; `/api/instances` renvoie aussi `modeles` et `index_pris`. Ne bascule pas l'active (geste explicite). ### Corrigé - **Gabarit de voûte du labo complété.** P18 (voûte) a échoué en passant l'active sur le labo : son `vault.yml.example` était resté à l'ancienne version (17 clés, sans `vault_restic_password`, `vault_oauth2_cookie`, etc.). Aligné sur le gabarit complet (23 secrets). P18 fait exactement son travail — attraper un gabarit incomplet, quelle que soit l'instance active. Validé : garde-fous de création testés en isolement (collision d'index refusée avant copie, écrasement refusé, modèle inconnu refusé, aucune pollution des dépôts frères) ; `node --check` du GUI ; `make verifier` rc=0 **CONFORME 21/21** (active = labo). ## 2026-07-23 (suite 2) ### Ajouté — bascule d'instance depuis le GUI (vraiment multi-instance) Demande explicite et répétée de l'opérateur : gérer les instances **depuis le GUI**, pas seulement en CLI. Le plan de contrôle reste maison, mais cette capacité y entre. - **Inventaire résolu dynamiquement.** Le serveur GUI figeait l'inventaire au démarrage (`args.inventaire.resolve()`). Il est désormais relu **à chaque requête** depuis le symlink `instance/` (propriété `Gestionnaire.inventaire`) — la bascule prend effet sans redémarrer. Les chemins de plan (`FICHIER_*`) étaient déjà relatifs au symlink et suivent de même. `SETOPS_INVENTAIRE` force encore un inventaire fixe (CI). - **`POST /api/instance-utiliser`** + `basculer_instance(nom)` — repointe le symlink avec les garde-fous de `make instance-utiliser`, plus une **validation stricte** : `nom` doit être une instance **découverte** (dossier frère), ce qui interdit toute traversée de chemin (testé : `../etc` refusé). - **GUI, vue Réseau** — la table de flotte gagne un bouton **« Activer »** par instance (l'active affiche « active »). Il confirme (avec avertissement renforcé si l'instance est en **production**), bascule, puis recharge la page : toutes les vues et les déploiements visent la nouvelle instance. L'édition du drapeau `federe` reste au CLI (rarement changé). La bascule, elle, est maintenant CLI **et** GUI. Validé : `node --check` du GUI ; bascule testée de bout en bout (repoint → l'inventaire dynamique suit → traversée refusée → restauration) ; `make verifier` rc=0 CONFORME 21/21. ## 2026-07-23 (suite) ### Ajouté — gestion multi-instances : vue d'ensemble + garde-fou de collision Le mécanisme de bascule existait déjà (`make instance-utiliser`, `make instance-courante` : repointer le symlink `instance`). Ce qui manquait : le regard d'ensemble et le filet. - **`make instances`** (`scripts/instances.py`) — liste toutes les instances de la fédération (dépôts frères avec `plan/nomenclature.yml`), marque l'active (`*`), et montre pour chacune index, plage VLAN dérivée, statut fédéré/local (`federe`) et production (`setops_production`). Lecture seule. - **Détection de collision d'index** — signale toute paire d'instances **fédérées** partageant un `index` (donc mêmes VLAN/VMID sur le trunk convergé). C'est exactement le piège vécu (Chezlepro-prod et le labo tous deux à l'index 1) : désormais crié, pas découvert par hasard. Le mode `--verifier` sort en erreur (rc=2) sur collision. - **Preuve P21** (`make prouver`/`make verifier`) — câble ce garde-fou dans le harnais : la cohérence de la fédération est vérifiée à chaque passage. No-op quand moins de deux instances fédérées sont présentes (comme P17 sans `SETOPS_MODELES`). Validé : `make instances` liste les 3 instances (labo LOCAL, Technolibre et Chezlepro fédérées, index 2 et 13) ; détection testée en synthétique (deux fédérées au même index → collision levée ; labo `federe: false` → cohérent) ; `make verifier` rc=0 **CONFORME 21/21**. ## 2026-07-23 ### Changé — l'adressage se dérive du seul seed `index` (RUPTURE, mode compact retiré) Principe posé par l'utilisateur : les valeurs de configuration doivent se dériver des intrants, pas se réécrire à la main. La nomenclature dupliquait ce que `index` détermine déjà (supernet, sous-réseaux, passerelles, VLAN). C'est corrigé, en rupture nette. - **`inventory_rules`** : nouvelles fonctions de dérivation, **source unique** — `supernet_de`, `base3_de`, `sous_reseau_de`, `passerelle_de`, `vlan_de`. Le modèle à 6 zones est encodé une fois : 2ᵉ octet = `10+index`, 3ᵉ octet de zone = `15+catégorie`, VLAN trunk = `1000+index×10+zone`. `deriver_nomenclature` ne lit plus **aucun** adressage stocké ; le mode `compact` est supprimé (tout est ip-miroir dérivé). - **`devis_reseau`** importe ces helpers (plus de duplication) ; découverte des tenants sur `index` présent (le filtre `vmid_schema` disparaît). - **Les 10 nomenclatures** (3 instances + 7 modèles) passent au **format maigre** : `index`, `cidr_hote`, `reservations`, libellés de zones et `fonctions` seulement. Supprimés : `supernet`, `vmid_schema`, et par zone `sous_reseau`/`passerelle`/`vlan`. `presence-web`, encore en `compact`, gagne `index: 1`. - **GUI** : `index` devient un **intrant** (section « Réseau » du panneau Intrants). Il vit dans la nomenclature (le plan réseau, uniforme partout — contrairement aux intrants des modèles, hétérogènes) et le GUI l'écrit **chirurgicalement** (une ligne, sans reformater le fichier). Le miroir JS dérive le VLAN du seed (fin de la lecture de `c.vlan` stocké). ### Ajouté - **Preuve P20** (`preuve_nomenclature_derivee`) : aucune nomenclature ne stocke d'adressage — garde-fou permanent contre une rechute vers l'écriture manuelle. Testée en négatif (une rechute simulée est bien rejetée). Validé : **DIFF VIDE sur les 3 instances** (la dérivation reproduit exactement l'adressage qui était stocké), les **7 modèles valident**, `devis_reseau` génère les mêmes VLAN (1011-1016 dérivés), `make verifier` rc=0 **CONFORME 20/20**, `node --check` du GUI OK, aller-retour d'écriture de `index` : une seule ligne touchée. ## 2026-07-22 (suite) ### Ajouté — trois preuves qui ferment les angles morts du harnais Le constat : `make prouver` ne vérifiait qu'*une* instance et le seul modèle `socle`. Tout ce qui vit à côté du moteur dérivait en silence — d'où l'hôte fantôme d'`integral`, les neuf secrets absents du gabarit Chezlepro, les champs de plan que le GUI ne sait pas écrire. - **P17 — tous les modèles valident** (`scripts/modeles.py`). Rejoue les 4 validateurs de registres sur chaque modèle découvert (les `exemples/modeles/*` du dépôt, plus les chemins de `SETOPS_MODELES`, ex. le dépôt privé). **A immédiatement trouvé 6 modèles invalides sur 7** : cinq portaient `autorite: interne` (valeur périmée jamais propagée depuis la correction de `socle` en phase 5), `presence-web` manquait son domaine interne. Corrigés. - **P18 — gabarit de voûte complet** (`scripts/voute.py`). Confronte `vault.yml.example` aux secrets réellement exigés par le plan (champ `secret` des bases + références `vault_*` des rôles actifs et des group_vars). Ne déchiffre jamais la vraie voûte : compare des noms. `--strict` signale aussi les clés devenues inutiles. - **P19 — le GUI couvre le plan** (`scripts/couverture_gui.py`). Croise les champs présents dans les plans réels (instance + modèles) avec `CHAMPS_ECRITS_PAR_GUI`, nouveau tableau de `inventory_gui.py` déclarant ce que les fonctions de sauvegarde écrivent. Signale tout champ non éditable par le GUI (**a trouvé `applications.websocket`**, comblé — case à cocher ajoutée), et toute dérive entre le tableau et le source du GUI. La nomenclature reste un trou connu (lecture seule), tolérable via `--tolerer nomenclature`. ### Corrigé - **Garde-fou contre l'hôte fantôme** : `valider_applications` accepte désormais le registre des serveurs et **refuse une application posée sur un hôte non déclaré** — la faute exacte qu'`integral` portait. Câblé dans `scripts/applications.py` (les 4 appels), au POST du GUI, et vérifié : re-teste le bug d'origine → rejeté avec un message actionnable. - **6 modèles de `Set-OPS-Modeles`** : `autorite: interne` → `auto-heberge` (collaboration, forge, identite, integral, observabilite) ; `presence-web` gagne son domaine interne pour ses expositions `site.*`/`app.*`. - **Champ `websocket` dans le GUI** (inspecteur d'application) — Collabora en a besoin. Validé : `make verifier` → **rc=0, CONFORME 19/16→19** (P01–P19), `ansible-lint` 0 échec, les 7 modèles valident (`SETOPS_MODELES` posé), gabarit de voûte complet (23/23), couverture GUI 27/27 champs (nomenclature tolérée), DIFF VIDE conservé, JS du GUI valide. ## 2026-07-22 ### Ajouté - **Champ « Liens (bindings) » dans le GUI** (`scripts/inventory_gui.py`). L'inspecteur d'application porte une section dédiée : une ligne par lien, `rôle → cible`, les deux en listes déroulantes, avec bouton d'ajout et de retrait. Les **rôles proposés sont exactement ceux que le rôle porteur déclare accepter** (`roles//meta/liens.yml`), et les cibles sont les autres applications du plan. Chaque ligne affiche les **variables que le lien injectera**. Changer le groupe d'une application vide les rôles de lien devenus inacceptés. Comble un manque relevé à l'audit du GUI : les bindings ne pouvaient se déclarer qu'en éditant `plan/applications.yml` à la main, ce qui rendait la configuration du courriel (Postfix → Dovecot, Postfix → rspamd) inaccessible sans éditeur. - **`liens_acceptes(groupe)` et `catalogue_liens()`** dans `scripts/inventory_rules.py` — source **unique** de « quels liens un rôle accepte », partagée par le validateur, le GUI et `instancier.py`. Nouvelle clé `liens_acceptes` dans la charge utile de `/api/inventaire`. ### Modifié - **`valider_applications` valide désormais les `liens`** : liste de tables `{vers, role}`, champs non vides, cible connue, pas de lien vers soi-même, et **rôle accepté par le rôle porteur** (avec la liste des rôles acceptés dans le message d'erreur). Un rôle sans `meta/liens.yml` reste toléré au validateur — `instancier.py` tranche avec le même message. Bénéficie aussi à `make inventaire-verifier` et à `scripts/applications.py verifier`. - **`scripts/instancier.py`** — sa copie locale `_liens_acceptes()` est retirée au profit de la fonction partagée. Un seul endroit lit `meta/liens.yml`. - **`docs/bindings-conception.md`** — la Phase 4 passe à 🟡 : l'éditeur est fait, le **graphe des liens reste à faire**. Validé : `make verifier` → **rc=0**, `ansible-lint` 0 échec sur 485 fichiers, `prouver.py --verifier` → CONFORME 16/16, `verifier_gui.py` (node --check) OK, **DIFF VIDE** conservé et les deux variables de binding du courriel toujours générées (`serveur_postfix_mailstore_hote`, `serveur_postfix_rspamd_milter`). Aller-retour d'écriture testé : les liens survivent au cycle chargement → sauvegarde → relecture. Sept garde-fous du validateur éprouvés (rôle inconnu, cible inexistante, lien vers soi-même, champ manquant, types invalides). ## 2026-07-21 (suite) ### Modifié - **Palette aurore enrichie sur les quatre pages `promo/`** — ajout de deux teintes, `--rose:#ff6fc4` (frange magenta) et `--vert:#5cff9d` (vert fluo), présentes uniquement dans les **dégradés foncés** : fond fixe du corps, nappe `.aurora` dérivante, et filets de séparation `.rule`. Opacités de 5,5 % à 8,5 % — une teinte, pas un motif. Les dégradés de texte et de boutons (`--aurora`) sont **inchangés** : l'identité de marque ne bouge pas. Appliqué identiquement aux quatre fichiers (0 conflit CSS après coup). - **Thème Forgejo étendu au wiki** (`roles/serveur_forgejo/files/custom/public/assets/css/alliance.css`, 29 → 118 lignes). La feuille étant injectée par `templates/custom/header.tmpl` sur toutes les pages, le wiki est couvert **sans feuille distincte**. Réorganisée en deux sections de risque explicite : **§1 Variables** (surcharge des variables de couleur officielles, sûr, résiste aux mises à jour) et **§2 Décor** (fond aurore, filet sous les titres, citations, tableaux et code en ligne de `.markup` — sélecteurs internes, à revérifier à chaque montée de version majeure). Supprimer §2 ramène au thème sobre d'origine. Le ciel nocturne ne s'applique qu'aux thèmes sombres, pour ne pas casser le thème clair. `roles/serveur_forgejo/README.md` documente le tout. ### Ajouté - **`docs/theme-forgejo-hors-flotte.md`** — pose **manuelle** du thème sur une instance Forgejo **non gérée par Set-OPS** (la forge historique qui héberge ce dépôt et son wiki). Forgejo n'ayant aucun réglage web pour le CSS, il faut déposer la feuille et référencer `header.tmpl` sur le serveur. La procédure **ne duplique aucun fichier** : elle pointe vers ceux de `roles/serveur_forgejo/files/custom/`. Comprend la détection du répertoire `custom`, un garde-fou pour ne pas écraser un `header.tmpl` existant (ajout de ligne, pas remplacement), la vérification par `curl`, et la marche arrière. ### Corrigé - **État de forge-01 élucidé — ce n'était pas une contradiction.** L'hôte a été **créé, puis supprimé**. Le plan dit donc `etat: planifie` (état **courant**) et le CHANGELOG du 2026-07-03 dit le branding « prouvé sur forge-01 » (état **passé**) : les deux disent vrai. `roles/serveur_forgejo/README.md` l'énonce désormais ainsi ; la preuve reste valable, simplement non rejouable tant que l'hôte n'est pas recréé. (La note précédente, qui présentait l'écart comme une contradiction à trancher, était erronée.) - **Teintes rose et verte trop concentrées dans les coins** — nappes élargies d'environ ×2 (780×540 → 1500×1020 px pour le rose, 840×580 → 1620×1120 px pour le vert), ramenées vers l'intérieur (97 % → 76 %, 3 % → 20 % ; nappe dérivante 95 % → 79 % et 6 % → 22 %), extinction repoussée de 62 % à 82 % avec un **arrêt intermédiaire** pour une rampe douce, et flou de `.aurora` porté de 64 px à 86 px. La couleur est répartie au lieu d'être tassée aux bords. - **Opacités des teintes réduites d'environ 40 %** dans la foulée — l'étalement les rendait trop présentes. Rose `.075 → .044`, vert `.058 → .034`, violet `.062 → .036` (arrêts intermédiaires abaissés dans la même proportion), et opacité de la nappe `.aurora` `.44 → .32`. Géométrie inchangée : seule l'intensité baisse. - **Statut du thème précisé, section par section** : §1 (variables) est **éprouvée** sur forge-01 ; §2 (décor), ajoutée aujourd'hui, n'a **jamais été rendue** par un Forgejo réel. L'affirmation précédente (« non rendu par un Forgejo réel », tous les cas confondus) était fausse pour §1. Validé : équilibre des accolades et parenthèses du CSS, 0 conflit CSS entre les quatre pages promo, `ansible-lint` 0 échec, `prouver.py --verifier` → CONFORME 16/16. ## 2026-07-21 ### Ajouté - **Protocole d'épreuve de l'opérateur indépendant** (`docs/audit/protocole-operateur-independant.md`). Met **AFF-002** (« Set-OPS s'exploite entièrement à la main, sans aucune IA ») à l'épreuve d'un sysadmin qui n'est pas l'auteur, sur sa propre grappe Proxmox, à froid depuis le modèle public `socle`. Définit : la **règle du silence** (l'observateur journalise, n'aide pas ; trois niveaux N0/N1/N2), les **interdits** (aucune IA, aucun accès aux dépôts privés, aucune lecture de `docs/audit/` qui divulguerait les pièges connus), les **critères de réussite R1→R6 fixés d'avance**, le périmètre matériel et la sécurité, et le **gabarit de rapport** (`operateur-independant-AAAA-MM-JJ.md`). Le registre peut **perdre** : le protocole prévoit explicitement la redescente d'AFF-002 en 🟡 ou ❌ selon le verdict. ### Modifié - **`docs/audit/affirmations.md`** — nouvelle limite consignée : **AFF-002 est ✅ *par inspection*, pas par démonstration**. Ses écarts bloquants sont soldés, mais aucun opérateur indépendant ne l'a exécutée ; ✅ y signifie « plus aucun défaut connu ». Renvoi vers le protocole. - **`docs/audit/README.md`** — « Les trois pièces » → « Les pièces » : ajout du protocole comme quatrième pièce du dispositif (l'épreuve humaine, hors harnais automatisable). Validé : `python3 scripts/prouver.py --verifier` → **CONFORME 16/16** (voûte exportée) ; équilibre des blocs de code du nouveau document vérifié. Aucun code touché. ## 2026-07-20 ### Modifié - **`make verifier` inclut désormais les preuves (`make prouver`).** `make verifier` se termine par `python3 scripts/prouver.py --verifier` : nouveau mode qui exécute toutes les preuves du registre (verdict + code de sortie) **sans écrire de rapport**, pour ne pas écraser la pièce justificative committée `docs/audit/preuve-.md`. `make verifier` échoue donc si une preuve échoue. `make prouver` seul (sans `--verifier`) continue d'écrire le rapport horodaté. Vérifié : `make verifier` → CONFORME 16/16 (voûte exportée), aucun churn du rapport committé ; `make prouver` écrit toujours. Doc mise à jour (`docs/audit/README.md` § « Rapport avec `make verifier` »). - **Conformité Phase 2 — 3 incohérences internes corrigées (documentation d'autorité).** Aucun code Ansible touché ; le code SSH était déjà conforme à `AGENTS.md`. - **`CLAUDE.md` réduit à un pointeur mince** : autorité unique d'`AGENTS.md` + les cinq règles absolues (agent unique ; `hosts.yml` généré jamais édité ; aucun secret ; confirmation des actions destructives ; rien n'est prêt sans validation). Toute la doctrine dupliquée (template, SSH, pare-feu, cloud-init, handlers, Makefile…) retirée. - **Contradiction SSH levée** : `CLAUDE.md` prescrivait `PasswordAuthentication yes` pendant la construction ; le code applique en réalité `PasswordAuthentication no` + `AuthenticationMethods publickey` dès le départ (prouvé, cf. `docs/audit/affirmations.md` AFF-037/050). La doctrine SSH périmée de `CLAUDE.md` est supprimée — la duplication en était la cause racine. - **`AGENTS.md` : « Comportement attendu de Codex » → « …des agents IA »** (la section vaut pour tout agent IA, Claude inclus). - Suivi dans `docs/audit/affirmations.md` (§ Journal des traitements) : AFF-040, AFF-050, AFF-052 résolus. ### Corrigé - **Conformité Phase 3 — lot A « doc de démarrage » (le parcours QUICKSTART marche seul).** - **Modèle absent (AFF-020/021).** `QUICKSTART.md` proposait `cp -r exemples/modeles/presence-web …` — modèle **inexistant** dans le dépôt public (seul `socle` l'est). Étape 2 réécrite sur `socle` ; les modèles assemblés renvoyés au dépôt privé `Set-OPS-modeles`, cohérent avec `exemples/modeles/README.md`. - **Chemin de voûte faux + piège de shadowing (AFF-023).** Le chemin `lab/` (QUICKSTART, `exemples/vault.exemple.yml`, `docs/config-proxmox.md`) est corrigé en `production/` (le modèle `socle` n'a qu'un inventaire `production/` ; `make config` résout `lab > principal > production`). **Piège corrigé** : le socle livrait ses intrants en `production/group_vars/all.yml` (forme fichier) ; y ajouter `all/vault.yml` (forme dossier) fait **ignorer silencieusement** `all.yml` par Ansible (le dossier masque le fichier — vérifié empiriquement). Le modèle est converti en forme dossier (`group_vars/all/10-intrants.yml`), alignée sur l'instance prouvée. Validé : `ansible-inventory --host` charge `domaine_interne` depuis la nouvelle disposition. - **Commandes `make` périmées (AFF-080/081/082).** `make help` → `make` ; `make syntax-template` → `make syntaxe-modele` (dans `docs/config-proxmox.md`, `docs/modeles_vm/debian13-proxmox.md`, `docs/MISE-A-JOUR-CODEX-CLAUDE.md`). - **Découvert et consigné (AFF-097, non traité ici) :** `lab/` codé en dur restant dans les docs template/clone (`vm-lifecycle`, `procedure-template…`, `proxmox/README`, message de `cloner_vm_debian.yml`) — lot séparé à prévoir. - **Conformité Phase 3 — lot B « doc (prose) ».** - **Prérequis Vault rappelé (AFF-026).** `QUICKSTART.md` : note que `make inventaire-verifier` / `make verifier` chargent l'inventaire complet et exigent `ANSIBLE_VAULT_PASSWORD_FILE`, sinon « no vault secrets found ». - **Sous-dossiers `playbooks/` (AFF-039).** `AGENTS.md` : précisé que seuls `groupes/`, `maintenance/`, `modeles_vm/`, `proxmox/` sont peuplés ; les autres sont prospectifs. - **`SOLUTION.md` remis au présent (AFF-060/061).** Bannière « document historique » (supplanté par README/QUICKSTART/`make`) ; arborescence complétée ; commande de construction `--ask-pass` (mot de passe) → `make preparer-modele` (accès par clé). - **Découvert et consigné (AFF-098, traitement B, non traité ici) :** contradiction fonctionnelle — `make config` écrit le token Proxmox dans `all/vault.yml`, mais `cloner_vm_debian.yml` ne le lit que depuis `proxmox.vault.yml`/env. Couplé à AFF-097 dans un futur lot « voûte Proxmox ». ### Corrigé - **Conformité Phase 3 — lot « voûte Proxmox » (AFF-098 bug fonctionnel + AFF-097).** - **Le clonage lit la voûte unifiée (AFF-098, option a).** `make config` écrit le token Proxmox dans `instance/inventories//group_vars/all/vault.yml`, mais `playbooks/proxmox/cloner_vm_debian.yml` (lancé `-i localhost,`) ne le chargeait que depuis `proxmox.vault.yml` → `make creer-vm` échouait l'assert `proxmox_api_token_secret` pour qui suivait la voûte unifiée. Corrigé : `all/vault.yml` ajouté aux sources de secrets du playbook (autoritaire) ; la détection de voûte chiffrée du `Makefile` (`cloner-vm`) cherche d'abord `all/vault.yml` puis `proxmox.vault.yml`. `proxmox.vault.yml` reste accepté en compatibilité. **Validé** : `--syntax-check` OK, `ansible-lint` 0 échec, test fonctionnel (token chargé depuis `all/vault.yml`, assert vert). - **Docs Proxmox/template alignées (AFF-097).** `lab/` codé en dur → `production/` + voûte unifiée dans `playbooks/proxmox/README.md`, `docs/procedure-template-debian13-proxmox.md`, `docs/vm-lifecycle.md`, `docs/modeles_vm/debian13-proxmox.md`. Plus aucune référence `inventories/lab/group_vars` dans les fichiers suivis. - **Conformité Phase 3 — lot C « `make verifier` vert » (AFF-006).** `ansible-lint` passe de **33 échecs à 0** (profil `min` → `production`), donc `make lint` **rc=0**. Trois causes : - **`site.yml` généré lint-propre** : `scripts/orchestrer.py` émet un `name:` avant chaque `import_playbook` (30× `name[play]`) ; `site.yml` régénéré. Orchestration inchangée. - **`risky-shell-pipe`** : `set -o pipefail` + `executable: /bin/bash` sur les deux tâches shell à pipe de `playbooks/valider.yml`. - **`name[template]`** : Jinja déplacé en fin de `name` dans `supprimer_vm_debian.yml`. **Validé** : `make lint` rc=0 ; toutes les étapes de `make verifier` vertes (lint, test, site-verifier, flux-verifier, syntaxe) — seule `inventaire-verifier` requiert la voûte de l'opérateur (prérequis documenté, AFF-026). Débloque AFF-002 (« exploitable sans IA » → ✅). ### Corrigé - **Conformité Phase 5 — boucle documentaire (le parcours QUICKSTART marche seul, prouvé).** Relecture de cohérence bout-en-bout (README/QUICKSTART/docs/wiki ↔ code final). Deux bogues du modèle public/outillage débusqués et corrigés, en plus des alignements de prose : - **Modèle `socle` invalide (AFF-099).** `exemples/modeles/socle/plan/domaines.yml` : `autorite: interne` (périmé, rejeté par le validateur) → `auto-heberge`. Le modèle valide. - **Split-brain d'inventaire (AFF-100).** `scripts/instancier.py` et `scripts/inventory_gui.py` retombaient sur `principal/` quand aucun `hosts.yml` n'existe encore ; or le socle est en `production/` → la 1ʳᵉ génération écrivait dans `principal/`, à côté des `group_vars` restés en `production/`. Corrigé : le repli vise le **répertoire d'inventaire déjà présent** (comme `config_proxmox.py`). Instances existantes (avec `hosts.yml`) **inchangées** (non-régression vérifiée sur `principal`). - **Alignements de prose.** `docs/intrants-communs.md` (`group_vars/all.yml` → `all/10-intrants.yml`, forme dossier que le GUI écrit déjà) ; `QUICKSTART.md` étape 8 (`serveur_postgresql` → `serveur_powerdns`, groupe présent dans le socle). - **Preuve** : parcours QUICKSTART rejoué **hors-ligne** sur une copie du socle (`instancier generer`→`comparer`→`appliquer`) — écrit `production/hosts.yml`, diff **vide** ensuite, 4 validateurs verts. Nouvelle preuve récurrente **P15** dans `make prouver` (« modèle public socle valide ») : `make prouver` = **15 OK, 0 échec, 1 sautée**. ### Ajouté - **Harnais de preuve `make prouver` (Phase 4).** Nouveau `scripts/prouver.py` — un **orchestrateur mince** qui rejoue les preuves automatisables du registre en appelant l'outillage **existant** (les mêmes scripts que `make verifier` : lint, tests, diff-vide, validateurs de registres, cohérence groupes/playbooks, handlers, orchestration, flux, syntaxe, existence des runbooks, invariants structurels) — **aucune validation réimplémentée**. Produit `docs/audit/preuve-AAAA-MM-JJ.md` : rapport **horodaté, rejouable**, reliant chaque preuve aux affirmations couvertes, listant à part les déclarations d'intention (⚪). Sort en erreur si une preuve échoue ; la preuve `P15` (inventaire Ansible complet) est **sautée** proprement sans mot de passe Vault (prérequis AFF-026). Documenté dans `README.md` (une phrase) et `docs/audit/README.md` (mode d'emploi complet + comment ajouter une preuve). `affirmations.md` : section « Couverture par `make prouver` » reliant chaque ✅ à sa preuve. **1re exécution : 14 preuves OK, 0 échec, 1 sautée → CONFORME.** - **Registre des affirmations (audit de conformité, Phase 1).** Nouveau `docs/audit/affirmations.md` : chaque affirmation publique vérifiable du dépôt (`README`, `AGENTS`, `CLAUDE`, `QUICKSTART`, `SOLUTION`, `docs/`, `wiki/`, aide du `Makefile`, GUI) est tracée vers une **commande de preuve reproductible** et un statut (✅ prouvée / 🟡 partielle / ❌ fausse / ⚪ invérifiable localement). **54 affirmations enregistrées : 30 ✅, 13 🟡, 8 ❌, 3 ⚪.** Audit **sans aucun correctif** (les traitements relèvent des phases suivantes). Preuves exécutées localement, hors production : diff-vide du plan, recoupement notify↔handlers, validateurs de registres (serveurs/applications/bases/domaines/GUI/orchestrateur/flux), `--syntax-check` de tous les playbooks, test unitaire. Dix écarts majeurs classés par risque pour un opérateur suivant la doc à la lettre — dont : `QUICKSTART` renvoie à un modèle absent (`presence-web`), `make verifier` échoue (ansible-lint : 33 failures), contradiction SSH `CLAUDE.md` ↔ code/`AGENTS.md`, chemin de voûte faux, commandes `make` périmées dans `docs/`. ## 2026-07-07 (soir) ### Modifié - **Nomenclature `ip-miroir` : longueurs UNIFORMES.** Le VLAN dérivé passe à `1000 + index×10 + zone` → **toujours 4 chiffres** (1011..4094), donc le VMID (`VLAN·octet·seq`) fait **toujours 9 chiffres**. Longueurs uniformes et mnémotechniques (retirer 1000 redonne `index×10+zone`). Les deux tenants sont **convergés** sur le réseau fédéré 10/8 : Chezlepro (idx 1) → VLANs 1011-1016 / 10.11.x ; Technolibre (idx 2) → VLANs 1021-1026 / 10.12.x. Chezlepro quitte le bac-à-sable plat 192.168.15/VLAN 15. ### Ajouté - **`make devis-reseau` : config switch dérivée du plan.** Nouveau `scripts/devis_reseau.py` qui découvre les instances fédérées (`../*/plan/nomenclature.yml`, schéma `ip-miroir`) et émet la config Cisco-like du réseau convergé — **VLANs + SVIs (passerelles) + ACLs d'isolation inter-tenant** — dérivée des nomenclatures, jamais saisie à la main (toujours synchrone avec le plan). One-shot précédemment ; maintenant reproductible. - **GUI : vue « Réseau » (devis switch) + bouton Copier.** Nouvel onglet lecture seule qui affiche le devis `make devis-reseau` (VLANs + SVIs + ACLs), via `GET /api/devis-reseau`, avec un bouton **Copier** (prêt à coller sur le switch). Un opérateur génère et transmet la config sans CLI. - **GUI : erreurs de déploiement en LANGAGE CLAIR.** Quand une action (créer/vérifier/déployer) échoue, le GUI n'oblige plus à lire le dump Ansible : un encadré résume les tâches en erreur — **étape · hôte · type · message clair** — avec un bouton **« Copier »** (pour transmettre le résumé à un humain / au mainteneur). Le backend (`_extraire_echec`) parse les `fatal:`/`UNREACHABLE!`, privilégie la vraie cause (`stderr` plutôt que le générique « non-zero return code »), gère `no_log` (« sortie masquée — secret ») et les injoignables. Éprouvé sur les 4 échecs réels du from-zero du jour. `node --check` OK. - **GUI : éditeur « Domaines » (6ᵉ onglet éditable).** Le registre des domaines publics — le seul sans édition dans le GUI — a désormais son onglet : ajouter/retirer une zone DNS, **autorité** (sélecteur `primaire-cache`/`auto-heberge`/`delegue`), **edge**, **DNSSEC**, **mail**, secondaires, et affichage des **expositions (FQDN) sous la zone**. Backend : route `/api/domaines`, `ecrire_domaines`, validation `valider_domaines` (rejette une autorité inconnue). Éprouvé : round-trip backend OK, `node --check` OK, smoke serveur OK. (Le `autorite: interne` périmé des instances Chezlepro/Technolibre a été corrigé en `auto-heberge` au passage.) - **GUI : persistance des journaux d'exécution.** Chaque action lancée depuis l'interface (créer-vm / vérifier / déployer) écrit désormais, **en plus du streaming live**, un journal horodaté `/logs/--.log` (gitignoré). Bonus : si le navigateur se déconnecte, l'exécution **continue** et le journal est capturé jusqu'au bout (au lieu d'être interrompue) — utile pour un déploiement qu'on ne veut pas voir avorter à la fermeture d'un onglet. - **GUI : vues « Flux » et « Couches » (lecture seule).** Deux nouveaux onglets rendent visibles dans l'interface deux registres jusque-là en ligne de commande seulement : **Flux** = la matrice d'audit réseau (rôle · sens · port · pair · chiffrement coloré · raison, depuis les `meta/flux.yml`), et **Couches** = l'ordre de reconstruction (socle → pki → services → apps → agents, avec les groupes triés topologiquement par couche). Alimentées par `resoudre_flux`/`orchestrer` via l'API. Éprouvé : `node --check` OK, smoke test serveur (63 flux, 6 couches servis). ### Modifié - **GUI : intégration des intrants de session.** Le panneau « Intrants de base » expose désormais **`nftables_admin_ssh`** (type liste, section « Sécurité ») — la garde anti-lockout du pare-feu était éditable en fichier mais absente du GUI. Retrait de `vault_step_ca_fingerprint` des secrets attendus (empreinte du root CA désormais dérivée dynamiquement, plus un secret). `node --check` OK. ## 2026-07-07 — Reconstruction from-zero PROUVÉE (preuve de portabilité « sans réserve ») Les 14 VM du lab (+ sauvegardes + AC) supprimées, puis `make myDay` a reconstruit l'écosystème POC **de rien** : 13 hôtes déployés (0 échec), et `make valider` **entièrement vert** (Prometheus 6/6 UP, 6 vhosts HTTPS, courriel bout-en-bout remis, 6 dépôts restic restaurés) — le tout sous pare-feu actif. 4 bugs de portabilité débusqués et corrigés dans le moteur : ### Corrigé - **client_pki : empreinte du root CA dérivée dynamiquement** (au lieu d'une valeur figée en Vault). Une AC régénérée (from-zero) a une empreinte neuve ; le rôle la lit désormais de l'autorité elle-même (`step certificate fingerprint`, délégué au nœud step-ca), source de vérité. `client_pki_ca_fingerprint_override` permet un épinglage explicite. - **serveur_keycloak : assignation de rôle tolérante aux utilisateurs absents.** Sur un annuaire vide (from-zero), assigner un rôle à un user inexistant échouait ; on vérifie désormais son existence (Keycloak fédère LDAP à la demande) et on saute proprement sinon. - **serveur_prometheus : ne scrute que les hôtes ACTIFS** (`client_metrique ∩ hotes_actifs`). Un hôte planifié (non déployé) n'est plus une cible morte. - **Pare-feu nftables compatible Docker.** Le ruleset résolu (a) remplace **uniquement la table `setops_flux`** au lieu de `flush ruleset` (préserve les tables Docker : DNAT/forward des conteneurs) et (b) autorise `docker0` + `ct established,related` dans la chaîne `forward`. Sans ça, `forward policy drop` coupait Collabora (conteneur). Diagnostic prouvé au niveau paquet. ### Ajouté - **`playbooks/proxmox/supprimer_vm_debian.yml`** — suppression de VM par VMID (from-zero), avec garde-fous : n'agit que sur les VMID présents dans le cluster, refuse si le modèle est ciblé, secrets `no_log`. ## 2026-07-07 ### Ajouté - **`make valider` — recette d'acceptation fonctionnelle (phase 4, v1).** Vérifie que les services *fonctionnent*, pas juste qu'ils sont déployés (complète les `*-verifier` statiques). `playbooks/valider.yml`, lecture seule : **cibles Prometheus toutes UP** (API `/targets`) + **vhosts HTTPS exposés répondent** (dérivés des `server_name` réels de l'edge, filtrés sur `domaine_interne` — générique) + **courriel bout-en-bout** (envoi via le MTA → LDAP → LMTP → Maildir, remise vérifiée par `doveadm` sur le compte `testmail`, message de test nettoyé) + **restauration de sauvegarde** (pour chaque nœud `client_backup` : restic restaure le dernier snapshot dans un dossier temporaire — lecture seule sur le dépôt — et vérifie que des fichiers en sortent). Éprouvé sur la flotte vivante sous pare-feu actif : 7 cibles UP, 8 vhosts OK, courriel remis, 6 dépôts restaurables. **La recette a trouvé un vrai trou** (collab-01/Nextcloud sans `client_backup_jobs` → service de sauvegarde en échec, fichiers non protégés), corrigé côté instance. - **Pare-feu nftables activé sur TOUTE la flotte (14 nœuds, activation prudente).** Les 14 hôtes actifs tournent sous `policy drop` avec leur ruleset **résolu moindre-privilège** (flux est-ouest déclarés autorisés par source, reste refusé). Vérifié en conditions réelles : flux déclarés OPEN (keycloak→pg, postfix→dovecot LMTP, prometheus→node_exporter…), flux non déclaré DROP (forge→redis), 14/14 `active`+`enabled`+`policy drop`, contrôleur toujours joignable. - **Intrant `nftables_admin_ssh`** (garde anti-lockout) — CIDR d'administration TOUJOURS autorisés en SSH, indépendamment des flux. Le résolveur (`resoudre_flux.py`) l'injecte en tête de chaque ruleset. **Bug de conception rattrapé avant activation** : le contrôleur Ansible arrive par VPN (`192.168.255.2`, hors sous-réseau flotte) — sans cette règle, activer = lockout immédiat. - **Rollout prudent** : d'abord `infra-pki-01` seul (test dead-man switch `systemd-run`, SSH re-vérifié sous drop, puis permanent), puis les 13 autres par lot (dead-man 5 min + sonde des flux est-ouest avant de persister). Activation pilotée par `nftables_baseline_enabled: true` (group_vars `hotes_actifs` ; le golden template n'y est pas → reste sans pare-feu, voulu). - Le déploiement dépose `instance/flux-genere/.nft` dans `/etc/nftables.conf` + service `enabled` (survit reboot ET futurs `make myDay`). ### Corrigé - **Détection du coffre Vault : `production/` codé en dur → inventaire réel.** `deployer`, `deployer-tout` et `verifier-deploiement` cherchaient le coffre chiffré dans `inventories/production/group_vars`, alors qu'une instance en `principal/` (cas courant) n'a pas ce chemin → l'invite du mot de passe Vault ne se déclenchait jamais et le déploiement échouait au déchiffrement. Corrigé : la garde vise désormais le `group_vars` de l'inventaire **résolu** (`$(dir $(INVENTAIRE_PRODUCTION))group_vars`). Vérifié : le coffre de `principal/` est bien détecté. ### Modifié - **`nftables_baseline` branché sur les flux résolus (reconstruction, phase 0).** Le rôle déploie désormais le ruleset **résolu** généré par `make flux` (`instance/flux-genere/.nft` — règles par source, `ip saddr` = moindre privilège) quand il est présent ; sinon repli sur le gabarit plat. **Toujours `nftables_baseline_enabled: false` par défaut → aucune activation** (l'activation reste un geste dédié, testé par nœud). Nouveau var `nftables_baseline_ruleset_genere`. Syntax-check OK. ### Ajouté - **Reconstruction from-zero en une commande (reconstruction, phase 3 — outillage).** La création de VM était unitaire (`creer-vm HOTE=…`) ; on comble le trou entre *créer* (2a) et *configurer* (2b) : - **`make flotte-creer CONFIRMER=true`** — boucle `creer-vm` sur tous les hôtes actifs du plan (clone Proxmox). Nouvelle sous-commande `inventory_host.py lister-actifs`. - **`make reconstruire CONFIRMER=true`** — enchaîne **flotte-creer → attente SSH de la flotte (`_attendre-flotte`, `ATTENTE_MAX` réglable) → `deployer-tout`**. La reconstruction complète en une commande, **idempotente de bout en bout** : le clone (`proxmox_kvm`) saute une VM déjà présente (par nom), le réseau/disque sont `present`/`resized` (grow-only), le déploiement Ansible converge. Re-lançable sans risque, qu'il reste des VM ou non. - **`make myDay`** repointé sur `reconstruire` (le vrai « bouton rouge » ; n'était qu'un alias de `deployer-tout`). Distinction assumée : `deployer-tout` = **converger** la config d'une flotte existante (2b, avec `MODE_CHECK=1`) ; `reconstruire`/`myDay` = **créer les VM manquantes puis déployer** (2a+2b). Gardes `CONFIRMER=true` sur les trois. Non testé contre Proxmox/lab (validé : énumération des 14 hôtes actifs, refus sans `CONFIRMER`, enchaînement `make -n`). ## 2026-07-06 ### Ajouté - **`make wiki-publier` — fin du dernier geste manuel (reconstruction, phase 0).** Le wiki pédagogique (`wiki/`, source versionnée) se publie désormais dans le wiki Forgejo par `make wiki-publier WIKI_REMOTE=….wiki.git` : clone superficiel du wiki, synchronisation des pages (`wiki/*.md` sauf `README.md` ; suppressions propagées), commit + push seulement s'il y a du changement. Refuse sans `WIKI_REMOTE`. Éprouvé de bout en bout contre un dépôt bare local (17 pages publiées = 17 source, diff vide, README exclu, `_Sidebar` inclus, idempotent au 2e passage). - **Registre des flux réseau complété (reconstruction, phase 0).** Transcription du travail zéro-confiance est-ouest dans `meta/flux.yml` : **16 rôles remplis** (step_ca, openldap, powerdns, prometheus, loki, redis, rspamd, backup, dovecot, postfix, keycloak, forgejo, grafana, icinga, icingaweb2, nextcloud, oauth2_proxy + le socle `serveur_debian` pour le plan de gestion SSH, + les 5 clients pki/journal/smtp/backup/unbound). Le registre couvre désormais **29 rôles, 63 flux** (qui-parle-à-qui : port, sens, pair, chiffrement, raison) — la base de génération nftables/OPNsense et la matrice d'audit. Schéma enrichi (`docs/flux-conception.md`) : valeurs `ssh` (transport SSH, restic/backup + SSH de gestion) et `tls-cible` (TLS visé, feuille de route edge→backends). **Point critique traité** : SSH (22) déclaré au socle, sinon les nftables générés couperaient l'accès Ansible. Validé : schéma conforme (0 erreur) et **matrice cohérente** (tout egress vers un service a l'ingress correspondant en face). Reste phase 0 : la cible `make wiki-publier`. - **Résolveur de flux (reconstruction, phase 0 — §Séquence 2).** `scripts/resoudre_flux.py` agrège les `meta/flux.yml`, résout les `pair`, et produit **deux artefacts, hors-ligne, sans activation** : - **`docs/registre-flux.md`** (généré) — la matrice d'audit source→destination (rôle, sens, port, chiffrement, raison) + synthèse chiffrement. Artefact du label de certification. - **aperçus nftables par hôte** (`instance/flux-genere/.nft`, gitignorés) — règles résolues avec IP réelles, `ip saddr` = **moindre privilège** (ex. LMTP 24 sur le mail n'accepte que l'IP du nœud Postfix), `policy drop`. **Aperçus inspectables, NON activés** (l'activation reste un geste dédié testé par nœud, cf. flux-conception §Activation prudente). - Cibles **`make flux`** (registre + aperçus) et **`make flux-verifier`** (schéma + matrice, branché dans `make verifier`). Validé : 29 rôles / 63 flux cohérents, 14 aperçus générés. Reste : brancher `nftables_baseline` (modèle plat aujourd'hui) sur ces aperçus, et le test lab. - **Orchestrateur ordonné (reconstruction, phase 2).** `playbooks/site.yml` n'est plus un stub : c'est désormais un **point d'entrée ordonné généré**, qui déploie l'écosystème **couche par couche, dans l'ordre de reconstruction**, sans intervention manuelle. Nouveautés : - **`docs/couches-deploiement.yml`** — registre central des couches ordonnées (`socle → pki_racine → pki_client → services → apps → agents`), les 30 groupes déployables classés. - **`scripts/orchestrer.py`** — trie les groupes par couche (clé primaire) puis **topologiquement intra-couche** via `dependances-groupes.yml` (ex. dovecot avant postfix, icingaweb2 après icinga, nextcloud en dernier). Génère `site.yml` comme une séquence d'`import_playbook`. Artefact du moteur (déterministe, sans donnée d'instance ; Ansible saute les groupes sans hôte actif). **Deux gardes anti-dérive** (refus si violé) : bijection univers↔couches (un nouveau rôle non classé casse la génération) et aucune arête « en arrière » (un prérequis dans une couche plus tardive = classification fausse). Éprouvées par test négatif. - **`make site`** (régénère + syntax-check), **`make site-verifier`** (cohérence, branché dans `make verifier`), **`make deployer-tout CONFIRMER=true`** (déploiement orchestré de la flotte, limité à `hotes_actifs` ; garde `CONFIRMER` car action impactante ; `MODE_CHECK=1` pour l'essai idempotent à blanc). Validé : `verifier` OK (30 groupes, aucun cycle/arête arrière), `--syntax-check` du `site.yml` généré OK, refus `deployer-tout` sans `CONFIRMER` (rc=2). ### Modifié - **Audit exhaustif du codé-en-dur (reconstruction, phase 1b).** Balayage complet `tasks + templates + defaults` de tous les rôles (noms de tenant, IP, domaines, emails, orgs). Résultat : le moteur ne porte plus **aucun** nom de tenant en dur. Corrigé — les labels/slug OIDC dérivent désormais de l'intrant `organisation` : `serveur_forgejo_oidc_nom` (slug de callback, `organisation | lower | replace(' ','-')`), `serveur_grafana_oidc_nom`, `serveur_nextcloud_oidc_nom`, `serveur_nextcloud_theme_nom` (labels d'affichage). Commentaires « Se connecter avec Chezlepro » → génériques. Non-régression SSO : `organisation: Chezlepro` → slug `chezlepro`, **identique** à l'URI de redirection Keycloak de l'instance (pas de casse). Conservés intentionnellement : realm `default('chezlepro')` (décision `identite_realm` actée) et le thème visuel **Alliance Boréale** (identité par défaut assumée du réseau, pas un tenant). Validé : re-balayage vide, rendu Jinja du slug testé (Chezlepro/Alliance Boréale/Ma Coop), `--syntax-check` OK (playbook forgejo via inventaire `principal`). - **Audit du graphe de dépendances (reconstruction, phase 1a).** `docs/dependances-groupes.yml` gagne les prérequis inter-groupes confirmés dans le code, en vue de l'orchestrateur trié en topologie. Ajouts : `serveur_keycloak` → **`serveur_openldap`** (fédération LDAP via `resoudre_annuaire_uri`, en plus de PostgreSQL) ; **`serveur_dovecot`** → `serveur_openldap` (userdb/passdb LDAP) ; **`serveur_postfix`** → `serveur_dovecot` (remise LMTP au mailstore) ; **`serveur_icingaweb2`** → `serveur_icinga` + `serveur_postgresql` + `serveur_openldap` (IcingaDB + auth LDAP) ; **`serveur_nextcloud`** → `serveur_postgresql` + `serveur_keycloak` (OIDC) + `serveur_collabora` (validation WOPI). Réconciliation `meta/liens.yml` : le seul lien structurel (`serveur_postfix` mailstore → Dovecot) coïncide avec le graphe. **Conclusion d'archi :** la règle « TLS vérifié ⇒ `client_pki` aux deux bouts » ne devient PAS des arêtes par-groupe (client_pki est quasi universel) — c'est une **couche** de l'ordre de reconstruction (`socle → step_ca → client_pki → services → apps → agents`) ; `dependances-groupes.yml` ne capture que le fin ordonnancement intra-couche. Validé : YAML conforme, **aucun cycle**, tri-topo réussi (19 nœuds), chargeur `charger_dependances` accepte (12 groupes, `est_groupe_operationnel` OK), tous les groupes ont un rôle. ## 2026-07-05 ### Modifié - **VLAN dérivé du tenant (réseau convergé).** `deriver_nomenclature` (schéma `ip-miroir`) dérive désormais le VLAN = `index × 10 + zone` — **unique globalement** sur un trunk convergé (chaque tenant son bloc de 10 ; 1-9 réservés à l'infra partagée). Le VLAN contenant déjà le tenant (1er chiffre = index), le VMID **mène avec le VLAN** (`VLAN·octet·seq`, ≤ 9 chiffres Proxmox). Ex. Technolibre (index 2) → VLANs 21-26, VMID 2101101. Le schéma `compact` (lab, sandbox) reste inchangé. Champs `vlan:` codés en dur retirés des catégories ip-miroir (désormais dérivés). ### Ajouté - **Zéro-confiance est-ouest — flux Métriques, Logs et Courriel chiffrés.** Suite du chantier (après PostgreSQL) : **métriques** (node_exporter sert en HTTPS via cert step-ca + `--web.config.file` + cert-sync owned prometheus ; Prometheus scrape `scheme: https` + `tls_config`), **logs** (Loki `http_tls_config` + cert-sync owned loki ; Alloy push `https` + `tls_config`), **courriel** (LMTP `edge-mta→infra-mail:24` en `lmtp_tls_security_level=verify` + `lmtp_tls_CAfile` ; `client_smtp` en STARTTLS vérifié). Chacun prouvé de bout en bout (200 HTTPS, cibles UP, livraison `status=sent`, HTTP rejeté). Motif cert-sync `.path` industrialisé. Reste : edge→backends + DNS (DoT). - **VMID 9 chiffres mnémotechnique (schéma `ip-miroir`, opt-in).** `vmid_schema: ip-miroir` dans la nomenclature → VMID `I·VVV·HHH·NN` (index·VLAN·octet-hôte·séquence) : le VMID *contient* l'IP (`10.(10+index).VLAN.hôte`) + le tenant, lisible d'un coup d'œil. Défaut `compact` rétro-compatible (instances déployées inchangées). - **Instance partenaire Technolibre.** Écosystème complet (12 VM, `etat: planifie`) dans `10.12.16.0/20`, **6 zones de sécurité** (Frontière/Identité/Données/Services-infra/Observabilité/ Applications, un /24 + VLAN chacune), index de fédération 2, VMID ip-miroir. Preuve de portabilité d'un tenant. ### Modifié - **Références par FQDN partout (fin des IP codées en dur).** Décision d'archi : FQDN pour toute référence inter-services (non ambigu en fédération, canonique pour TLS ; nom court = hostname OS). `resoudre_base` renvoie le FQDN (→ keycloak/forgejo/icinga) ; `client_journal_loki_url` dérivé du groupe `serveur_loki` ; defaults db_host IP morts nettoyés. **Aucune IP littérale dans les defaults.** - **Découplage du tenant d'origine.** Realm SSO centralisé sur l'intrant **`identite_realm`** (défaut `chezlepro` ; les 4 rôles keycloak/forgejo/grafana/oauth2_proxy en dérivent ; exposé dans la GUI). Vars brandées renommées génériques : `chezlepro_timezone→fuseau_horaire`, `chezlepro_organisation→organisation`. Le moteur ne porte plus le nom d'un tenant. - **Modèles d'instance rafraîchis.** `integral` régénéré depuis le cas prouvé (6 zones, fonctions éprouvées `data-sql`/`id-ldap`/`id-sso`/`sup`, ip-miroir, nouveaux intrants) ; `socle`/`identite`/ `observabilite`/`forge` réalignés sur le même moule (prouvés : dérivation + `valider_serveurs`). `presence-web` marqué **aspirationnel** (rôles web-frontal/dorsal absents) plutôt que faussement prêt. ## 2026-07-04 ### Ajouté - **Zéro-confiance : flux PostgreSQL entièrement chiffré et vérifié.** PG sert désormais son **cert step-ca** (vérifiable contre root_ca) au lieu du snakeoil, et **refuse** toute connexion non-TLS du réseau (`hostssl` dans pg_hba). Les 3 clients passent en **verify-full** : keycloak (`db-url-properties sslmode=verify-full`), forgejo (`SSL_MODE=verify-full` + `PGSSLROOTCERT`), IcingaDB (`tls: true` + `ca`). Prérequis posés : `client_pki` sur data-sql-01 (cert), et **`root_ca.crt` en 0644** (cert public, requis par les clients TLS non-root). cert-sync PG (motif `.path`, owned postgres) + reload de l'instance `postgresql@NN-main`. Vars : `serveur_postgresql_tls_actif`/`_tls_force`, `serveur_*_db_sslmode`/`_ca`. Prouvé de bout en bout (cert Set-OPS CA servi, apps 200, non-TLS rejeté « aucun chiffrement », TLS accepté). ### Corrigé - **PG : détection de version robuste (collision avec un répertoire non-numérique).** Placer le `tls_dir` sous `/etc/postgresql/` faisait choisir `tls` comme « version » de cluster (`find | sort | last`) → configs déployées au mauvais endroit (verrou hostssl inopérant). Corrigé : détection filtrée aux dossiers **numériques** (`^[0-9]+$`) + `tls_dir` déplacé sous `/var/lib/postgresql/tls`. - **Renouvellement de cert : recharger le VRAI consommateur (bug latent de flotte).** Le `cert-renewer@.service` (client_pki) renouvelait le cert sur disque mais son `ExecStartPost` rechargeait un service nommé *d'après le cert* (`%i` = FQDN), inexistant → **nginx (et postfix, dovecot, slapd) n'étaient jamais rechargés** et servaient l'**ancien cert jusqu'à expiration**. Symptôme vécu : cert edge expiré en mémoire (renouvelé sur disque), échec TLS de l'échange code→jeton OIDC → **login Grafana/SSO cassé** (tous les services derrière l'edge). Correctif : `client_pki_reload_services` (liste des vrais consommateurs), câblée par groupe (edge→nginx, mail→postfix/dovecot, annuaire→slapd). Appliqué + vérifié sur les 4 hôtes. Fix immédiat de l'incident : `systemctl reload nginx` sur l'edge. ### Ajouté - **Doc à jour : unité wiki « Autorisation & RBAC », leçon renouvellement, runbooks.** Fermeture des dettes de doc : nouvelle **unité wiki authZ/RBAC** (pendant d'*Identité & SSO*, avec l'exemple Grafana), **section « le renouvellement est un système »** versée dans l'unité *PKI* (comparer cert servi vs fichier ; recharger le consommateur), et **`docs/runbooks-exploitation.md`** (cert expiré, RBAC Grafana, branding Forgejo). 15 unités wiki désormais. - **UI des logs Loki : dashboard Grafana provisionné.** Loki n'a pas d'UI ; son UI est Grafana. Ajout d'un **dashboard « Journaux de la flotte »** (dossier Set-OPS) : sélecteur d'hôte multi + filtre regex insensible à la casse + panneau logs + débit par hôte. Référence Loki par une **variable de datasource** (`ds_loki`), pas un UID codé en dur (leçon : ajouter un uid explicite à une datasource déjà provisionnée casse le démarrage de Grafana). **Visible par les Viewers** (dont testmail) sans accès Explore. Prouvé sur obs-01 (dashboard chargé, Grafana actif). - **RBAC via SSO : rôle de realm → niveau Grafana.** Machinerie *additive et idempotente* dans `serveur_keycloak` (`rbac-oidc.yml`) : rôles de realm (`serveur_keycloak_realm_roles`), **mapper `roles`** sur les clients choisis (`_role_mapper_clients`, rôles de realm → claim `roles` dans ID token + userinfo), **assignations** rôle→utilisateur (`_role_assignments`). Côté Grafana, `role_attribute_path` (`grafana-admin→Admin`, `grafana-editor→Editor`, sinon Viewer). kcadm **à chaud, zéro coupure SSO**. Prouvé (idempotence `changed=0`) sur id-sso-01 : rôles créés, mapper présent, `testmail` = `grafana-editor` (→ Explore). Illustre l'**authZ** (vs authN du SSO). ## 2026-07-03 ### Ajouté - **Identité visuelle Alliance Boréale sur Forgejo (léger, officiel).** Branding via le dossier **`custom/`** de Forgejo (mécanisme *officiel* — pas de fork, résistant aux MAJ) : accent **aurore par variables CSS** (`--color-primary`…, aucune classe interne touchée), **logo/favicon étoile** (réutilisés du thème Keycloak), **page d'accueil brandée** (`home.tmpl` : hero aurore + accroche), **thème sombre** par défaut, **nom + méta**. Codifié dans `serveur_forgejo` (`serveur_forgejo_branding`, `_app_name`, `_theme`), déployé dans `{{ data }}/custom/`. **Prouvé** sur forge-01 : accueil rend (200, « Forge Chezlepro »), `alliance.css` servi (cyan aurore), lint OK. - **Wiki pédagogique Forgejo — 14 unités d'apprentissage.** Set-OPS comme *compagnon pédagogique* : chaque service = une lentille sur un fondamental TIC, méthodes génériques (on apprend OIDC, pas Keycloak). Source versionnée dans `wiki/`, publiée dans le wiki Forgejo (`eregion`). Moule à 4 temps (concept → Set-OPS → transférable → à toi de jouer, avec *casse-répare*). - **GUI : boutons cohérents.** « Pousser » (surchargé : clonait *et* déployait) → « 🖥 Créer la VM » pour le clone, « Déployer » partout pour le déploiement, dry-run obligatoire partout. GUI 100 % française. - **Agents d'observabilité/ops éprouvés — observabilité *flotte-complète*.** Les 3 intégrations « agent » (liaisons nœud × optionnelles) déployées sur 4 nœuds (obs-01, data-sql-01, id-ldap-01, forge-01) et **prouvées** : `client_metrique` (node_exporter → **Prometheus scrape les 4 cibles, toutes UP**) ; `client_journal` (journald → **Loki reçoit les logs des 4 nœuds**) ; `client_smtp` (msmtp → **courriel système d'un nœud relayé par l'edge-MTA et livré**). Comble le trou : les *serveurs* d'observabilité (Grafana/Prometheus/Loki) étaient prouvés, mais pas la collecte fleet-wide — désormais Grafana voit toute la flotte. Cibles corrigées (bac à sable) : Loki→obs-01, relais→edge-mta-01 (pas infra-mail-01, qui est le store Dovecot sans SMTP :25). - **Thème de connexion Keycloak à l'identité Alliance Boréale.** Thème de login `alliance-boreale` (`roles/serveur_keycloak/files/themes/`, `parent=keycloak` + overlay CSS) reprenant l'identité du site de l'Alliance (extraite de `site-alliance-boreale`) : **ciel nocturne aurore** (`#05060f`/ `#0a0d24` + dégradés), **carte glassmorphism**, **logo étoile aurore** (le `favicon.svg` du site), **bouton dégradé aurore** (teal→cyan, pilule), liens cyan, **police système** (souveraineté, zéro dépendance externe). Déployé dans `{{ keycloak_home }}/themes/`, appliqué au realm via `kcadm ... -s loginTheme` (var `serveur_keycloak_login_theme`, idempotent), Keycloak rechargé (`flush_handlers` avant la config realm). **Prouvé** : la page de login charge `alliance.css` (HTTP 200) + le `logo.svg` (200), `loginTheme=alliance-boreale` actif sur `chezlepro`. **Constellation animée en fond** (`scripts=js/constellation.js`) : le JS crée son propre ciel (canvas + aurore, le template n'en ayant pas) — étoiles scintillantes qui dérivent, liens de constellation cyan, blob d'aurore ondulant ; respecte `prefers-reduced-motion`. Prouvé : `constellation.js` référencé + servi (200). **Console de compte thémée aussi** (thème `account`, `parent=keycloak.v3`) : overlay CSS surchargeant les variables PatternFly 5 (fond aurore, cartes en verre, accent aurore) + la même constellation animée. Var `serveur_keycloak_account_theme` via `kcadm -s accountTheme`. Prouvé : console charge (HTTP 200, `keycloak.v3` intact), `account.css` servi (200). - **Soumission courriel `:587` interne (authentifiée) — la boucle souveraine est bouclée.** Postfix (`edge-mta`) sert la **soumission `:587`** (bloc `master.cf` : STARTTLS requis, `SMTP AUTH`, seuls les authentifiés relaient) ; l'auth SASL est **déléguée à Dovecot** (`infra-mail`, passdb LDAP prouvé) via un **auth-listener réseau** (`service auth { inet_listener sasl }`, port 12345). Aucune sortie internet : interne→interne uniquement (l'externe = déliverabilité, Étape B). **Prouvé** (swaks) : `testmail` s'authentifie (`235 Authentication successful`), Postfix accepte (`250 queued`), et le courriel est **livré dans la boîte** (LMTP→Dovecot). Le courriel souverain fait maintenant **recevoir ET envoyer**. Vars : `serveur_dovecot_sasl_reseau`, `serveur_postfix_submission_actif`. Pièges : Dovecot 2.4 exige un **nom** de section `inet_listener` ; ajouter un service `master.cf` (nouveau listener) → handler **restart** (pas reload) ; et une config cassée peut bloquer un redéploiement si la synchro cert/restart précède le template (corriger la config à la main pour débloquer). - **Sauvegardes applicatives (logiques) — `serveur_backup` + `client_backup` (restic), Tier 0 prouvé.** Choix : sauvegarder la **donnée d'état** (non régénérable) plutôt que les VM (reconstructibles par le code + le template). Outil **restic** (chiffrement côté client, déduplication, rétention). `serveur_backup` (nœud `backup-01`) = cible SFTP/SSH (utilisateur `restic`, clé autorisée, dépôts sous `/srv/restic/`). `client_backup` (intégration par nœud) = restic + **jobs déclaratifs** (`client_backup_jobs` : `{nom, commande?, chemins}`), clé SSH + mot de passe restic en voûte, script + **timer systemd** (quotidien) + rétention `forget --prune`. **Prouvé de bout en bout** sur le **Tier 0** (`infra-pki-01` → `/etc/step-ca`, l'ancre de confiance) : sauvegarde **hors-nœud** vers `backup-01`, puis **restauration byte-identique** des clés CA (`root_ca_key`, `intermediate_ca_key`, `ca.json`). Piège corrigé : le plancher `/etc/hosts` d'un nœud existant ignore un nœud nouvellement ajouté → rafraîchir le socle. - **Sauvegardes Tier 1 généralisées — 5 nœuds, restauration prouvée.** `client_backup` étendu (jobs déclaratifs en host_vars) à : `data-sql-01` (`pg_dumpall` — keycloak/forgejo/icingadb), `id-ldap-01` (`slapcat` LDIF — les identités), `infra-mail-01` (`/var/vmail` — les boîtes), `forge-01` (`/var/lib/forgejo` + `/etc/forgejo` — dépôts Git ; la BD est déjà couverte par PG). **Prouvé par restauration** : dump PostgreSQL restauré contient bien les 3 bases (`CREATE DATABASE forgejo/icingadb/keycloak`) ; LDIF restauré contient `testmail`. Les 5 dépôts restic (pki, sql, ldap, mail, forge) sont hors-nœud sur `backup-01`, chiffrés. Ajouter un service à sauvegarder = déclarer un job. Reste : cible **offsite** (3-2-1, Étape B — le dépôt n'est qu'une URL swappable). - **Consolidation — binding `annuaire` (`resoudre_annuaire`) + retrait de la cruft.** *Cruft* : supprimés les 8 dossiers-catégories inertes (`roles/{applications,backup,database, identity,monitoring,proxmox,storage,web}/`, README seuls) et les 5 playbooks-échafaudages `debug` sans rôle (nextcloud, collabora, client_supervision, web_frontal, web_dorsal) ; l'intention reste documentée dans `docs/catalogue-services.md`. *Binding annuaire* : nouveau rôle utilitaire partagé `resoudre_annuaire` (comme `resoudre_base`) qui **dérive** la connexion OpenLDAP du `domaine_interne` + un hôte d'annuaire surchargeable — LE seul endroit où le nom d'hôte de l'annuaire est fixé, au lieu d'être répété. Facts `resoudre_annuaire_{uri,port,base_dn,users_dn,bind_dn,bind_password}` (secret déréférencé, `no_log`). **Migrés + prouvés (config neutre, `changed=0`)** : `serveur_keycloak` (fédération LDAP — testmail token HTTP 200), `serveur_dovecot` + `serveur_postfix` (flux courriel Postfix→LDAP→LMTP→Dovecot **livré de bout en bout**). Piège appris : les *defaults* d'un rôle inclus ne persistent pas hors de son exécution — publier via `set_fact`. - **Binding annuaire complété — `icingaweb2` + `client_ldap` migrés vers `resoudre_annuaire`.** Fin des 2 loose ends : `serveur_icingaweb2` (connexion LDAP dormante en mode SSO) résout via `resoudre_annuaire` (redéploiement `changed=0`, SSO intact) ; `client_ldap` (SSSD, dormant) ne pointe plus sur un `idm-01` périmé. **Plus AUCUN rôle ne code en dur l'hôte d'annuaire** — un seul point de vérité (`resoudre_annuaire`). - **Rôle `serveur_oauth2_proxy` — passerelle SSO OIDC générique (Keycloak) + Icinga Web 2 au SSO.** oauth2-proxy (v7.15.3, binaire GitHub) place Keycloak **devant** n'importe quelle app sans OIDC natif : elle reçoit l'utilisateur authentifié via en-tête, en auth `external`. Rôle paramétrable (client, secret voûte, redirect, upstream, cookie voûte) — **réutilisable** pour toute app OIDC-less. Éprouvé sur `sup-01` **devant Icinga Web 2** : client Keycloak `icingaweb2`, oauth2-proxy `:4180` (exposé par l'edge) → upstream nginx local `:8080` → icingaweb2 `backend = external` (REMOTE_USER depuis `X-Forwarded-Preferred-Username`). **Prouvé** (flux authorization code headless) : `testmail` → oauth2-proxy → Keycloak → **icingaweb2 `/dashboard`, connecté** (« Se connecter avec Chezlepro », comme Grafana/Forgejo). Ferme le gap LDAP-direct d'icingaweb2. **Réglages appris** : `insecure_oidc_allow_unverified_email` (les users LDAP n'ont pas `email_verified` ; IdP interne de confiance) ; en reverse-proxy oauth2-proxy passe `X-Forwarded-*` (pas `X-Auth-Request-*`) ; **handler nginx en `restart` (pas `reload`)** car un changement d'adresse d'écoute n'est pas pris par un reload gracieux. - **Rôle `serveur_icingaweb2` — Icinga Web 2 (UI native) + module IcingaDB : éprouvé.** App PHP (php8.4-fpm) servie par un **nginx local**, exposée par l'edge (`icinga.lab.chezlepro.internal`, auto-dérivé : vhost + cert SAN + A PowerDNS + alias plancher). Config **par fichiers `.ini`** (config/resources/authentication/roles + module `icingadb`), pas d'assistant de setup. Base IcingaDB via `resoudre_base` (registre). **Auth LDAP direct** vers OpenLDAP (LDAPS, `client_pki` sur `sup-01`) — icingaweb2 n'a pas d'OIDC natif ; SSO-par-proxy = raffinement futur. **Prouvé** : `testmail` (LDAP) se connecte (`/dashboard`), et le **module IcingaDB affiche la supervision** (hôte `icinga`). Déploiement `failed=0` (le rôle est bon ; les frictions étaient dans le simulateur de login curl : contrôle de cookie `_checkCookie`, champs `uid`/`submit_login`, valeur CSRF avant `name`). - **Module BPM (Business Process) éprouvé + codifié — pile Icinga complète.** `serveur_icingaweb2` installe + active `icingaweb2-module-businessprocess` (`serveur_icingaweb2_modules`), crée le répertoire des processus (éditable via l'UI, groupe `icingaweb2`, setgid) et **sème des processus métier en IaC** (`serveur_icingaweb2_bpm_processes`, nom → contenu `.conf`). Format des feuilles `host;service` (éprouvé via les fixtures du module). **Prouvé** : un processus « Supervision Chezlepro » (agrège load/procs/swap/ping4/ssh du host `icinga` en logique ET) **rend un état** dans l'UI (`testmail` connecté), **avec le backend IcingaDB** (pas d'IDO). Rôle re-prouvé (reset → recrée le processus, idempotent). BPM n'est **pas remplaçable par Grafana** (roll-up d'impact métier). Pile Icinga = **moteur + Web 2 + BPM**, complète. - **Cœur Icinga éprouvé (supervision active).** `serveur_icinga` (cœur : `icinga2` + `icingadb` + `icingadb-redis`) déployé sur `sup-01` (🔧→⭐), base `icingadb` PostgreSQL via le registre. **Prouvé** : 3 services actifs, et le moteur **supervise** — IcingaDB peuplée (1 hôte, 12 services, résultats de checks persistés en base). Zéro bug de déploiement (rôle bien bâti). **Icinga Web 2 + module BPM restent différés** (phases dédiées : UI native + vues d'impact métier ; le BPM n'est pas remplaçable par Grafana). Confirme aussi le **DRY `resoudre_base`** sur icinga (les 3 rôles consommateurs validés). - **Forgejo branché au SSO OIDC (« Se connecter avec Chezlepro ») — 2e app SSO, prouvée.** `serveur_forgejo` (10.0.0) éprouvé sur `forge-01` (🔧→⭐), adossé à PostgreSQL (base `forgejo` auto-provisionnée depuis le registre), exposé par l'edge (vhost + cert SAN + A PowerDNS + alias plancher, tout auto-dérivé de `expose`). **Source OAuth2** vers Keycloak (`forgejo admin auth add-oauth`, idempotent, realm `chezlepro`), client OIDC `forgejo` enregistré via `serveur_keycloak_clients`. Auto-enregistrement OIDC (`[oauth2_client] ENABLE_AUTO_REGISTRATION` + `ALLOW_ONLY_EXTERNAL_REGISTRATION` : identités depuis l'annuaire seulement). **Prouvé** (flux authorization code headless) : `testmail` (LDAP) se connecte, **compte auto-créé** (`testmail@lab.chezlepro.internal`), atterrit sur le tableau de bord. **5 bugs de 1er déploiement corrigés** : dépendance périmée `serveur_sendmail`→`serveur_postfix` (le vrai MTA) ; `app.ini` doit appartenir au user `git` (Forgejo persiste des secrets générés) ; ordre admin/migrations (`flush_handlers` + `wait_for` avant `admin user create`) ; `HTTP_ADDR` `127.0.0.1`→`0.0.0.0` (l'edge nginx est sur un autre hôte, 502 sinon) ; auto-enregistrement OIDC. - **DRY : rôle utilitaire partagé `resoudre_base` (résolution BD depuis le registre).** Le bloc copié-collé dans `serveur_keycloak`, `serveur_forgejo` et `serveur_icinga` (charger le registre, filtrer par consommateur, déréférencer le secret via `lookup('vars', ...)`, résoudre hôte/port) est extrait dans `roles/resoudre_base` (facts `resoudre_base_entree/db_password/db_host/db_port`, `no_log`). Les 3 rôles l'incluent (`include_role`) et adoptent les facts. Le secret **ne quitte toujours pas le rôle** (déréférencé au déploiement). **Fait « sur la preuve »** : re-déploiement keycloak + forgejo `failed=0`, idempotent, `testmail` token Keycloak HTTP 200. Ferme le reste noté de la Phase 2 des bindings (cf. `docs/bindings-conception.md`). - **PowerDNS — A d'exposition auto-dérivés (le DNS de la Phase 3 des bindings).** `serveur_powerdns` génère désormais, dans la zone interne, un enregistrement A pour chaque FQDN d'exposition (champ `expose` des applications) vers l'**edge qui le sert** (`domaines.edge`) — via `expositions_des_applications` (même source que les vhosts nginx et les SANs du cert edge). Déclarer `expose` produit maintenant **vhost + SAN de cert + enregistrement DNS**, tout dérivé. **Prouvé** : `dig @infra-dns-01 grafana.lab.chezlepro.internal` et `keycloak.…` → `192.168.15.21` (edge). Option `serveur_powerdns_publier_expositions` (défaut true). **Limite / reste** : PowerDNS est **autoritatif, pas récursif** — pour que les nœuds *utilisent* ces A sans casser la résolution Internet, il faut un **récursif** (pdns-recursor : forward de la zone interne + récursion du reste) ou garder le plancher `/etc/hosts`. Ne PAS repointer naïvement `client_dns` vers l'autoritatif. - **`hosts_statiques` — alias d'exposition dans le plancher `/etc/hosts` (résolution client, sûre).** Le plancher pose désormais, sur **chaque nœud**, ` ` pour chaque `expose` (dérivé de `domaines.edge`, même source que nginx/PowerDNS). Indépendant du DNS, aucun risque de couper la résolution (choix retenu vs pdns-recursor). Chargement du plan **best-effort** (`stat` **`delegate_to: localhost`** + `become: false` — les registres vivent sur le nœud de contrôle ; ignoré si le plan est absent, ex. préparation du template). **Prouvé** : `/etc/hosts` d'obs-01 régénéré avec `keycloak`/`grafana` → edge (ligne manuelle éliminée), `getent` OK, et le **flux SSO Grafana fonctionne via la résolution du plancher** (`login: testmail`). Boucle Phase 3 fermée : déclarer `expose` → **vhost + SAN cert + A PowerDNS + alias plancher**, tout dérivé. Bugs corrigés en chemin : `serveur_loki` (groupe `loki` manquant), `stat` sur cible→contrôle, `become` inutile sur le contrôle. - **Rôle `client_unbound` — résolveur local (DNS dynamique) : éprouvé sur un nœud.** Unbound par nœud (`127.0.0.1`) avec **stub-zone** vers l'autoritatif interne (PowerDNS) + **récursion** Internet (ou forward via `client_unbound_transitaires`). Alternative *dynamique* au plancher `/etc/hosts` statique, sans casser Internet. Bascule de `/etc/resolv.conf` **protégée** (`client_unbound_apply` + `client_unbound_confirm`) **et validée AVANT** (Unbound doit résoudre interne + Internet, sinon pas de bascule → nœud jamais coupé). **Prouvé sur data-sql-01** (rayon d'impact minimal) : `dig @127.0.0.1 keycloak/id-sso-01.lab.chezlepro.internal` → PowerDNS, `deb.debian.org` → récursion, `apt` OK. Rôle sûr par défaut (`apply: false` : installe Unbound sans toucher au resolver). Rollout flotte = opt-in par nœud. **Note direction** : OPNsense embarque Unbound → à terme, l'Unbound *réseau* peut vivre sur l'appliance de bordure (nœud public, Étape B) ; le rôle par-nœud reste portable et complémentaire (cache local). - **Bindings — Phase 1 : résolveur de liens dans `instancier.py` (relations service→service déclaratives).** Une application déclare ses `liens: [{vers, role}]` dans `plan/applications.yml` ; chaque rôle décrit les liens qu'il accepte dans `meta/liens.yml` (`setops_liens.accepte`, comme `meta/empreinte.yml`). `instancier` résout la cible (FQDN interne **dérivé de la nomenclature** + `domaine_interne`), substitue les gabarits (`{cible.fqdn}`, `{cible.hote}`, `{cible.ip}`) et **injecte les variables en host_vars du consommateur**. Validation : rôle accepteur, cible existante, genre attendu. **Migration prouvée** : les liens mail Postfix→Dovecot (`mailstore`) et Postfix→rspamd (`milter`) passent de group_vars codés en dur à des liens déclaratifs — `make instancier` donne **DIFF VIDE** (mêmes variables générées), puis les group_vars sont retirés. La topologie mail devient déclarative et portable. Cf. `docs/bindings-conception.md`. Suite : bases (Phase 2), exposition/domaines (Phase 3), GUI (Phase 4). - **Bindings — Phase 2 (bases) : constat + réconciliation de la note (`docs/bindings-conception.md` §5/§9).** Inspection du code réel : le binding app→base **existe déjà** — **côté base** (`consommateur`/`portee` dans `bases-donnees.yml`), résolu **dans le rôle** au déploiement (`include_vars` + filtre + `lookup('vars', secret)`), sur 4 rôles (postgresql, forgejo, keycloak, icinga). **Délibérément conservé** (le secret ne quitte jamais le rôle) — ne PAS dupliquer en app-side/instancier. Deux directions assumées : app→app côté app (instancier), app→base côté base (registre). Reste (reporté à l'épreuve de Keycloak) : factoriser le bloc de résolution copié-collé en include partagé (DRY). - **`docs/carte-set-ops.md` — carte d'orientation (index + mécanismes transverses).** Après audit du dépôt : point d'entrée « à lire d'abord » (index du corpus, ~22 docs), et **catalogue des mécanismes** dispersés dans le code (les 2 directions de binding, pont de cert, résolution BD par registre, socle-first, sûreté check-mode, voûte, dimensionnement) avec **où ils vivent**. But : ne plus re-découvrir l'existant. Constat : la **cruft était déjà inventoriée** dans `catalogue-services.md` (rôles-catégories inertes, échafaudages) — non dupliquée, référencée. `catalogue-services.md` « État d'implémentation » **rafraîchi** (rôles éprouvés sur VM réelles : socle, PKI, LDAP, DNS, nginx, pile courriel). Pointeur ajouté depuis `architecture-set-ops.md`. - **`serveur_postgresql` et `serveur_keycloak` éprouvés sur VM réelles (🔧→⭐).** PostgreSQL déployé (data-sql-01), écoute réseau + pg_hba VLAN, et **provisionne la base `keycloak` depuis le registre** (`bases-donnees.yml`) — **binding app→base prouvé en réel** (base + rôle créés, mot de passe = `vault_bd_keycloak`). Keycloak 26.0.7 déployé (id-sso-01), **mode prod**, connecté à PostgreSQL (**87 tables** du realm master écrites), **token admin obtenu** (auth adossée à la BD). Lacunes connues (documentées `catalogue-services.md`) : fédération LDAP et edge nginx pas encore câblés. Reste : DRY du bloc de résolution BD (keycloak/forgejo/icinga). - **`serveur_keycloak` — fédération LDAP (modèle d'identité A) : automatisée et prouvée.** Le rôle configure, via `kcadm` (idempotent), un realm applicatif (`serveur_keycloak_realm`, déf. `chezlepro`) et un **provider de stockage LDAP READ_ONLY** vers OpenLDAP (LDAPS, `uid`/`entryUUID`, `inetOrgPerson`). TLS LDAPS validé via le **truststore système** (`truststore-paths` → `/etc/ssl/certs/ca-certificates.crt`, racine step_ca posée par **client_pki**, désormais requis sur le nœud). Secrets par `environment` + `no_log`. **Éprouvé avant codification** puis prouvé par le rôle : un utilisateur LDAP (`testmail`) obtient un token via le realm (HTTP 200), et le redéploiement est **idempotent** (`changed=0`). Nouveau : `tasks/federation-ldap.yml`. Reste : edge nginx (accès HTTPS par nom), mappers d'attributs/groupes fins. - **Edge nginx + exposition (Phase 3 des bindings) — prouvés avec Keycloak.** Sans changement de code : la machinerie `serveur_nginx_publier_expositions` **existait déjà** (lit `expose` des applications + `edge` de `domaines.yml`, dérive `amont = http://:`, génère le vhost avec `X-Forwarded-*`). Le bac à sable déclare `keycloak.expose: [keycloak.lab.chezlepro.internal]` + le domaine interne `lab.chezlepro.internal` (edge `serveur_nginx`). **Prouvé** : le vhost s'auto-génère (`keycloak.lab.chezlepro.internal → http://192.168.15.81:8080`), et la découverte OIDC via l'edge renvoie `"issuer":"https://keycloak.lab.chezlepro.internal/..."` (les `X-Forwarded` passent, Keycloak se sait derrière HTTPS). **Limite connue** : le cert TLS de l'edge est encore le snakeoil auto-signé (avertissement navigateur). **Raffinement recommandé** (réutilise l'existant, pas de nouveau mécanisme) : ajouter les FQDN d'exposition aux `client_pki_sans` de l'edge (client_pki demande + renouvelle déjà le cert d'hôte), puis pointer `serveur_nginx_certificat` sur le cert client_pki (`/etc/step/certs/.crt`). - **Cert de l'edge : snakeoil → step_ca (HTTPS valide).** Appliqué le raffinement ci-dessus : `client_pki` ajouté à l'edge, ses `client_pki_sans` incluent le FQDN d'exposition (`keycloak.lab.chezlepro.internal`), et `serveur_nginx_certificat`/`_cle` pointent sur le cert client_pki. **Prouvé** : HTTPS `HTTP 200` avec `ssl_verify_result=0` (chaîne validée contre la racine step_ca, nom correct), émetteur `Set-OPS Internal CA`. Sans nouveau code (client_pki + group_var). **Gaps notés** : (1) recharger nginx au **renouvellement** du cert (le cert-renewer renouvelle en place, nginx ne recharge pas seul — hook à ajouter) ; (2) **auto-dériver** les SANs d'exposition de l'edge depuis le plan (au lieu de les lister dans le group_var). - **Grafana branché au SSO OIDC (« Se connecter avec Chezlepro ») — prouvé de bout en bout.** `serveur_grafana` : config OIDC via `GF_AUTH_GENERIC_OAUTH_*` (client confidentiel `grafana`, realm `chezlepro`, secret `vault_grafana_oidc`). `serveur_loki` + `serveur_prometheus` + `serveur_grafana` déployés sur `obs-01` (🔧→⭐). **Bug de rôle corrigé** : `serveur_loki` créait le répertoire en `group: loki` alors que le paquet crée l'utilisateur en `nogroup` sans groupe `loki` → ajout de la création du groupe. **Prouvé (flux authorization code headless, via l'edge HTTPS)** : `testmail` (user LDAP) se connecte à Grafana par le SSO — `/api/user` renvoie `login: testmail`, email et nom **fédérés depuis LDAP**. Chaîne complète LDAP → Keycloak → Grafana. **Gaps notés (pour rendre 100 % déclaratif)** : (1) l'**enregistrement du client OIDC** dans Keycloak a été fait via `kcadm` **à la main** (à codifier — rôle grafana ou liste de clients côté keycloak) ; (2) la **résolution** `keycloak.…internal → edge` sur obs-01 est un `/etc/hosts` manuel (**PowerDNS devrait porter les A d'exposition** — chaînon récurrent) ; (3) mapping de rôles Grafana (tous Viewer par défaut). - **`serveur_keycloak` — enregistrement des clients OIDC codifié (gap précédent fermé).** Le rôle gère une **liste déclarative** `serveur_keycloak_clients` (`clientId`, `redirect_uris`, `web_origins`, `secret`) et enregistre chaque client confidentiel via **kcadm idempotent** (`tasks/clients-oidc.yml`, create-si-absent, `no_log`). Décision : **côté Keycloak** (les creds admin restent dans le seul rôle Keycloak, pas répandus dans chaque rôle app) ; le `secret` référence la même variable de voûte que l'app. **Prouvé** : client `grafana` supprimé → rôle → recréé → `testmail` se connecte à Grafana (`login: testmail`) ; redéploiement **idempotent** (`changed=0`). Le déploiement de Grafana au SSO est désormais **autonome**. ## 2026-07-02 ### Décidé - **Bindings — conception des relations app/base/serveur/domaine (`docs/bindings-conception.md`).** Les relations service→service sont aujourd'hui codées en dur, éparpillées dans des group_vars (ex. Postfix→Dovecot/rspamd/LDAP), ce qui casse la portabilité multi-tenant. Direction retenue : **liens déclarés côté application** (`liens: [{vers, role}]`), résolus par `instancier.py` en variables Ansible ; chaque rôle décrit les liens qu'il accepte dans `meta/liens.yml` (comme `meta/empreinte.yml`) ; FQDN cible **dérivé de la nomenclature** (jamais codé en dur). Domaines publics traités comme lien `exposition` (écrit sur l'edge). Réconcilie l'existant (bases `consommateur`, `domaines.edge`). Preuve de migration ciblée : les 3 liens mail. Implémentation à suivre (phasée). - **Licence : passage de CC BY-NC-SA 4.0 à AGPLv3.** Les licences Creative Commons ne sont pas faites pour du logiciel (position de CC elle-même) et la clause **NonCommercial contredisait le principe fondateur « tout est libre »** — en plus de bloquer les artisans/coopératives visés. `LICENSE` remplacé par le **texte officiel intégral de l'AGPLv3** (verbatim, non modifié). Attribution + modèle **libre + services/certification** documentés dans le `README` (méthode d'attribution recommandée, sans toucher au texte de la licence). L'AGPLv3 protège la souveraineté (anti-captation propriétaire en SaaS) **sans interdire l'usage commercial**. - **Architecture d'identité/SSO (`docs/identite-sso.md`).** Modèle A : **OpenLDAP source de vérité**, **Keycloak fédéré** (SSO web OIDC, MFA, self-service), **mail en bind LDAP direct**. Une identité, un mot de passe, deux chemins d'auth (web→Keycloak, mail→LDAP), tout adossé au même OpenLDAP. Sert la portabilité multi-tenant (chaque tenant = son LDAP + son Keycloak fédéré). Pas Keycloak-source (casse-tête mail + moins portable). - **Service courriel : pivot de Stalwart vers Postfix + Dovecot + rspamd.** La Phase 1 Stalwart (`serveur_stalwart`) avait été prototypée et déployée (v0.16.11, install + démarrage en mode récupération). Le prototypage a révélé un projet **trop jeune/volatil pour un pilier mail critique** : config cassée entre 0.15 et 0.16, outil IaC `stalwart config apply` **annoncé mais non livré** dans le binaire, API REST supprimée (JMAP), gros backlog. Pivot vers la stack **mature Postfix/Dovecot/rspamd**, en prime **100 % configurable par fichiers** (alignée au modèle déclaratif Set-OPS). Le rôle `serveur_stalwart` est **retiré** (git en garde la trace) ; `docs/courriel-conception.md` mis à jour. Réévaluer Stalwart ~2028. ### Ajouté - **`docs/pouvoirs-set-ops.md` — bilan des capacités du moteur.** Inventaire structuré (moteur/plan, plan de contrôle GUI, socle durci, piliers d'infrastructure, services outillés, patrons d'ingénierie), distinguant honnêtement « prouvé sur cluster réel » de « outillé ». - **Rôle `serveur_rspamd` (rspamd 3.x) — antispam + DKIM, en milter sur Postfix.** Installé sur le nœud edge-mta (avec Postfix), backend **Redis** local, worker proxy en **mode milter auto-scan** (`:11332`), **signature DKIM** sortante (clé générée par le rôle de façon idempotente ; enregistrement DNS public affiché pour l'Étape B). Config par surcharges `/etc/rspamd/local.d/`. Postfix branché via `smtpd_milters` (option `serveur_postfix_rspamd_milter`, `milter_default_action = accept` → tolérant si rspamd indisponible). **Prouvé** : un courriel traversant le milter ressort **scanné** (`rspamc stat` : 1) et **signé DKIM** (`DKIM-Signature: d=…`), puis livré et lu en IMAP. **Étape A (courriel interne) complète** : dovecot + postfix + rspamd. - **Flux courriel interne PROUVÉ de bout en bout (Étape A).** Envoi → Postfix (`edge-mta`, validation LDAP) → LMTP réseau → Dovecot (mail-store) → boîte Maildir → **lu en IMAP** (auth LDAP, TLS step_ca) : `status=sent`, message lu (sujet + corps). Réglages Dovecot 2.4 qui débloquent la remise LMTP : `userdb static { static_allow_all_users = yes }` (sinon NOTFOUND pour l'expéditeur/raw-mail-user externe), `mail_inbox_path =` vidé (le défaut mbox `/var/mail` root refusait l'autocréation de l'INBOX), Maildir explicite (`mail_home` + `mail_path = %{home}/Maildir`), chemin par nom d'utilisateur (home identique côté LMTP local-part et IMAP adresse complète ; mono-domaine, multi-domaine = raffinement Étape B). - **Rôle `serveur_postfix` (Postfix 3.x) — MTA du nœud edge-mta.** Réception `:25`, cartes **LDAP** (validation des boîtes via l'attribut `mail`), remise **LMTP réseau** vers le nœud mail-store Dovecot (`virtual_transport = lmtp:inet:[…]:24`), TLS via **step_ca** (pont de cert), aucune boîte locale. Config `main.cf` + carte `ldap-mailboxes.cf`, validée par `postfix check`. Secret de bind : `vault_openldap_admin`. Nécessite `serveur_postfix_mailstore_hote` (FQDN du mail-store). Validé statiquement ; déploiement réel à suivre. - **Rôle `serveur_dovecot` (Dovecot 2.4) — déployé et prouvé.** IMAP `:993`/`:143` + LMTP, **auth/annuaire LDAP** (vers `serveur_openldap`, filtre `mail`), stockage Maildir (user système `vmail`), **TLS via step_ca** (pont de cert + resync au renouvellement), neutralisation de l'auth système par défaut. Config en drop-in **syntaxe Dovecot 2.4** (`mail_driver`, `ssl_server_cert_file`, `passdb ldap`/`userdb static`, `%{user}`), **validée par `doveconf`** au déploiement. Sockets d'intégration Postfix **conditionnels** (rendus si l'utilisateur `postfix` est co-localisé). Prouvé : `doveadm auth test` — bon mot de passe accepté, mauvais refusé, sur cert step_ca. Secret de bind : `vault_openldap_admin`. - **`serveur_openldap` durci pour la prod : TLS via step_ca + organisation en intrant.** - **TLS (LDAPS + STARTTLS)** : le certificat d'hôte step_ca (déposé par `client_pki`, `root:root 600`) est synchronisé vers un emplacement lisible par `openldap` (`/etc/ldap/tls`) par un script + une unité `path` systemd qui **re-synchronise et recharge slapd à chaque renouvellement** ; `olcTLS*` configuré dans `cn=config`, `SLAPD_SERVICES` expose `ldaps://`. Dégrade proprement (slapd en clair local) si `client_pki` n'a pas encore posé le cert. - **Organisation** : nouvel intrant `chezlepro_organisation` (remplace le « Exemple Inc » codé). - *Écrit en code de prod, éprouvé statiquement ; validation par déploiement réel à suivre.* - **`docs/courriel-conception.md`** — cadrage du futur service de courriel souverain : full self-host, suite **Stalwart** (adoptée, enveloppée par un rôle mince), topologie MX primaire + MX secours, intégration identité (LDAP) / DNS / PKI (Let's Encrypt public vs step_ca interne), enregistrements DNS publics, décisions ouvertes (IP/PTR, secours, stockage) et phasage. **Conception seulement — aucun rôle livré.** ## 2026-07-01 ### Ajouté - **État RÉEL vs plan dans le GUI (sonde de vie + auto-actif).** Le badge `planifié`/ `actif` décrit l'*intention* du plan, pas l'existence de la VM — d'où la confusion « serveur planifié mais vivant ». Deux ajouts : - **Sonde de vie** : le GUI teste la joignabilité SSH de chaque hôte en arrière-plan (`/api/sondes`, en parallèle) et affiche un état réel — **● vivante** / **● injoignable** — sur les tuiles et dans le détail (« Plan : … · Réel : … »), distinct du plan. - **Auto-actif** : un hôte qu'on **matérialise** (clone `creer` réussi) ou qu'on **déploie** passe automatiquement `actif` dans le plan (matérialisé = actif), puis l'inventaire est **régénéré** pour que le changement se voie partout (en-tête inclus). - **Compteur « vivantes »** dans l'en-tête (depuis la sonde), à côté de actifs/planifiés. ### Corrigé - **nginx ne validait pas sur Debian 13 (`server_tokens` en double).** Debian 13 livre `server_tokens off;` **actif** dans `/etc/nginx/nginx.conf` (avant : commenté). Le drop-in `conf.d/99-setops.conf` du rôle le redéclarait → `nginx -t` échouait (« directive is duplicate ») et le déploiement plantait au handler de validation. Le rôle `serveur_nginx` neutralise désormais la ligne distro (le drop-in reste l'unique source). Trouvé en déployant nginx pour de vrai sur un hôte edge. - **Une voûte chiffrée cassait `instancier` / « Appliquer le plan ».** `ansible-inventory --list` (utilisé pour la comparaison sémantique du plan) tente de déchiffrer `group_vars/all/vault.yml` et échoue sans mot de passe (`exit 4`) — alors que l'opération est structurelle, sans secret. `instancier` utilise désormais automatiquement le fichier conventionnel `~/.config/setops-vault-pass` (si `ANSIBLE_VAULT_PASSWORD_FILE` n'est pas déjà défini). - **Secrets des rôles non câblés à la voûte (échafaudage manquant).** Les rôles à secrets déclaraient `serveur_X_password: ""` avec, en commentaire seulement, la variable de voûte attendue (`{{ vault_X }}`) — sans mapping réel. Résultat : remplir la voûte selon `vault.exemple.yml` ne suffisait pas, le secret restait vide et l'assertion « secrets requis » échouait. Les 12 secrets des 8 rôles (`serveur_step_ca`, `serveur_forgejo`, `serveur_grafana`, `serveur_keycloak`, `serveur_openldap`, `serveur_redis`, `client_ldap`, `client_pki`) pointent désormais vers leur variable de voûte : `serveur_X_password: "{{ vault_X | default('') }}"`. Chaque instance n'a plus qu'à remplir ses `vault_*` dans sa voûte chiffrée ; aucun mapping par instance. - **« Vérifier » (dry-run `--check`) échouait faussement sur un hôte frais.** Les tâches « démarrer service » et les handlers « redémarrer / recharger / valider » des rôles applicatifs touchent un paquet que `--check` n'installe pas réellement → le service (ou le fichier de zone/conf) n'existe pas encore → faux `fatal`, qui **bloquait le déploiement** (le dry-run doit réussir pour débloquer « Déployer »). Ajout de `when: not ansible_check_mode` sur ces tâches et handlers des 13 rôles `serveur_*` (29 gardes). En dry-run elles sont sautées ; en vrai déploiement, inchangées. - **Le bouton ⚙ « Appliquer le plan » du GUI refusait en silence** dès que le plan divergeait de l'inventaire (il appelait `instancier appliquer` **sans** `--force`). Résultat : après une édition (disque, état, auto-actif), l'inventaire n'était jamais régénéré et les compteurs restaient figés. Le clic « Appliquer » **est** l'intention explicite → le GUI force désormais (git reste le filet). - **Bouton « Pousser » dans le GUI — le flux devient 100 % cliquable.** Chaque objet du détail se matérialise sur son hôte, en streaming console (avec mot de passe vault + confirmation renforcée en prod) : - **Serveur** → `🖥 Pousser` clone la VM depuis le golden template (`make creer-vm`), disponible même sur un hôte planifié — comble le trou : le GUI ne clonait pas. - **Application** → `Pousser` déploie l'hôte porteur (`make deployer HOTE=`). - **Base** → `Pousser` déploie l'hôte du serveur de BD (crée la base). Nouveaux modes `creer` / `pousser` dans `executer_flux` + routes `/api/creer` et `/api/pousser`. Flux complet depuis l'interface : éditer → ⚙ Appliquer → 🖥 Pousser → Vérifier → Déployer. - **Premier déploiement RÉEL validé de bout en bout** (cluster asgard) : flux canonique plan-piloté clonant + durcissant + faisant tourner un PowerDNS qui résout. Voir les 3 correctifs ci-dessous. ## 2026-06-30 ### Ajouté - **Plancher de résolution `/etc/hosts` (indépendant du DNS).** Nouveau rôle de socle `hosts_statiques` (dans `serveur_debian`) : génère `/etc/hosts` sur **chaque** VM depuis l'inventaire (nom + FQDN interne → IP réelle). Tout l'écosystème se résout par nom **même serveur DNS éteint**, et le bootstrap ne dépend plus du DNS. PowerDNS devient une **commodité** (zone/externe/dynamique) ; `client_dns` est rendu tolérant (inerte si aucun DNS interne) et sa dépendance à `serveur_powerdns` passe **molle**. - **Adressage fédéré : index d'instance.** Le VMID n'est plus codé `9CSNN` en dur : il prend le préfixe d'un `index` déclaré en tête de `plan/nomenclature.yml` (`{index}{catégorie}{service}{séq}`). Convention : `supernet = 10.(10+index).0.0/16`, `VMID = index·CSNN`. Permet à N écosystèmes de **coexister/s'interconnecter** sans collision (Chezlepro=1 → `10.11`/`1xxxx`, Technolibre=2 → `10.12`/`2xxxx`). Sans index → `9CSNN` (rétro-compatible ; bacs à sable, plages ad-hoc `172.19.x`). Code mort retiré (`deriveServeur` JS). Voir `docs/multi-instances.md`. - **Doc `docs/multi-instances.md`** : cadrage « un moteur, N écosystèmes » — l'instance comme dépôt autonome, la bascule, l'isolation, et le socle multi-tenant. - **Bascule d'instance (`make instance-utiliser NOM=…`).** Repointe le symlink `instance` vers un autre dépôt d'instance (prod ↔ bac à sable) ; `make instance-courante` affiche l'instance montée. Permet d'exploiter plusieurs instances (séparation **par instance**) depuis un seul moteur. - **Identité des intrants relative à l'inventaire.** Le panneau « Intrants » lit/écrit l'identité dans `group_vars/all/10-intrants.yml` de l'inventaire monté (fichier réel pour une instance autonome, symlink vers une source partagée sinon — l'écriture suit le symlink). Fonctionne donc aussi bien pour une instance « par instance » que pour l'ancien partage lab/production. - **Inventaire d'instance neutre et configurable (`principal` / `SETOPS_INVENTAIRE`).** Le moteur (Makefile + `inventory_gui`, `instancier`, `config_proxmox`, `serveurs`, `applications`) ne code plus en dur `inventories/lab` / `inventories/production` : il vise **un inventaire par instance**, détecté de façon **rétro-compatible** (`principal` > `production` > `lab`) et surchargeable par `SETOPS_INVENTAIRE`. Les instances existantes (découpage lab/production) continuent de fonctionner à l'identique ; les nouvelles peuvent adopter `inventories/principal/`. Deuxième pierre de la séparation **par instance** (la 1re étant le drapeau `setops_production`). - **Garde-fou de prudence par instance (`setops_production`).** Le déploiement réel est désormais possible sur **toute instance** (un bac à sable déploie sur *son* infra lab — il est isolé **et** fonctionnel). Le drapeau `setops_production` dans `group_vars/all/` ne **bloque** plus rien : il marque la PRODUCTION pour exiger une **confirmation renforcée** au déploiement (bannière/badge rouge « PROD », bouton Déployer en rouge, re-saisie du nom d'hôte) ; un bac à sable (`false`) affiche « bac à sable » et déploie sans cette étape. Rétro-compatible (à défaut de drapeau, ancien repère « inventaire production »). Le GUI affiche toujours quel type d'instance est monté. ### Modifié - **Voûte de secrets unique par environnement.** Fini les voûtes éparpillées : tous les secrets de l'instance (token Proxmox + 17 `vault_*` pour PKI, LDAP/SSO, bases, forge, observabilité) vivent dans **un seul fichier chiffré**, `inventories//group_vars/all/vault.yml`. Gabarit committé `exemples/vault.exemple.yml`. `make config` (`config_proxmox.py`) écrit/édite désormais cette voûte (semée depuis le gabarit si absente). `.gitignore` durci (`**/vault.yml`). Docs mises à jour (config-proxmox.md avec étapes de migration, intrants-communs.md §H, QUICKSTART). Le GUI ne stocke toujours aucun secret. ### Corrigé - **Trois bugs trouvés au premier déploiement réel (cluster asgard).** - **Clonage/Makefile codaient `inventories/lab` en dur** (config Proxmox + voûte) — vestige du modèle env qui cassait les instances `principal`. Le playbook de clonage et le Makefile détectent maintenant l'inventaire (`lab` > `principal` > `production`). - **Redimensionnement disque non idempotent** : quand le disque dérivé du plan est plus petit que le golden template, Proxmox refuse (`shrinking disks is not supported`) et le clone échouait. Le resize est désormais **grow-only** (tolère le cas, la VM garde le disque du template — le dérivé est un minimum). - **PowerDNS refusait de démarrer** (`multiple backends 'bind'`) : le rôle redéclarait `launch+=bind` que le paquet `pdns-backend-bind` pose déjà. Le rôle ne déclare plus `launch` (seulement `bind-config`). - **`chezlepro_timezone` n'était appliqué nulle part.** Cet intrant de base global était défini mais aucun rôle ne s'en servait. Le rôle `chrony` (appliqué à tout hôte via `serveur_debian`) règle désormais le fuseau horaire à partir de `chezlepro_timezone` (`chrony_timezone` par défaut, vide = ne pas toucher). Les autres défauts globaux (nœud / stockage / pont Proxmox) étaient déjà réutilisés comme valeurs par défaut, surchargeables par hôte. ### Modifié - **Détail GUI : section « Groupes (dérivés) » retirée (redondante).** Depuis la fusion en atelier maître-détail, elle ne répétait que le socle (universel), les rôles des applications (déjà dans « Applications ici ») et les intégrations (déjà cochées). Son seul signal unique — le prérequis bloquant — est désormais nommé dans le pied (« Prérequis manquant : … ») ; le panneau Dépendances reste pour la vue d'ensemble. Code mort retiré (`renduGroupe`, `detailGroupe`, `titreGroupe`, CSS `.groupe*`). - **Nettoyage CSS/HTML du GUI** après la refonte : retrait des règles et éléments morts (`.message`, `.chips-filtre`/`.chip-f`, `.onglet*`, `.base-ligne`, `.bases-liste`, `.ch-grp*`/`.ch-fleche`/`.ch-roles`, `.champ-val`, divs `#message` et `#chips`). - **GUI refondu en atelier maître-détail unifié.** Toutes les vues suivent le même motif : tuiles à gauche, **détail + saisie à droite**, le panneau droit reflétant la sélection de la vue courante (fin du panneau « figé » au changement de vue). Les vues **Applications** et **Bases** passent de tableaux pleine largeur à ce motif ; la saisie se fait dans le panneau droit, avec **liens cliquables** entre objets (serveur → application → base). Le panneau droit est élargi (~38 %). - **Fusion Serveur/Hôte.** Les vues « Inventaire » (hôtes, lecture seule) et « Serveurs » (plan) faisaient doublon : elles sont fusionnées en **une seule vue Serveurs**. Sa tuile porte le statut de réconciliation (réconcilié / divergent / non instancié) ; son détail réunit l'**identité éditable** (plan), les **dérivés** (VMID/IP/VLAN), les **groupes**, les **applications et bases hébergées**, et **Vérifier/Déployer**. La navigation clavier (`1-3`, `j/k`, `v/d`, `/`) et le filtre opèrent désormais sur les serveurs. Un bandeau « Comment lire ce parc » explicite le modèle serveur·hôte·groupe·application·base. Code mort retiré (`carte`, `renduChaine`, `ONGLETS`, `champLecture`, sélection d'hôte, etc.). ### Ajouté - **Fluidité d'exploitation du GUI** (sans dépendance, stdlib pure) : - **Notifications empilées (toasts)** auto-effaçables au lieu d'une bannière unique écrasée ; les erreurs restent plus longtemps, les opérations longues mettent à jour leur propre toast. - **Durée d'exécution** affichée en direct dans la console (Vérifier / Déployer) et suivi vivant de « Appliquer le plan ». - **Navigation clavier** : `1-5` changent de vue, `j/k` parcourent les hôtes, `v`/`d` vérifient/déploient l'hôte sélectionné, `/` cible le filtre, `Ctrl+S` sauvegarde la vue éditable courante (ignorés pendant la saisie). - **Validation inline** des champs du plan (vue Serveurs) : nom `fonction-NN`, mémoire, cœurs, disque — liseré rouge et blocage avant l'envoi. - **Garde-fou anti-perte** : confirmation `beforeunload` si des éditions de plan ne sont pas sauvegardées. - Le bouton **Sauvegarder** de l'en-tête devient contextuel (sauve la vue éditable, désactivé en lecture seule) — fin de l'impasse 409 ; suppression du code mort. ### Ajouté - **Vue Serveurs : champs alimentés par les paramètres globaux.** À `+ Serveur`, **Nœud** et **Stockage** deviennent des listes déroulantes (catalogues `proxmox_noeuds` / `proxmox_stockages`, vide = défaut global) et **Intégrations** une rangée de cases à cocher des rôles `client_*` disponibles (au lieu d'une saisie texte). Les catalogues Nœuds / Stockages / Ponts sont de nouveaux intrants (classe « Catalogue ») éditables dans le panneau « Intrants de base » et stockés dans `group_vars/proxmox.yml` ; l'API expose `integrations_disponibles` (scan de `roles/client_*`). Le type `liste` (chaîne virgulée → liste YAML dédoublonnée) est ajouté au schéma des intrants. ### Supprimé - **Vue « Chaîne » du bandeau (redondante).** Son contenu (`renduChaine`) était déjà rendu, par hôte, dans l'onglet « Chaîne » du détail de la vue Inventaire ; la vue ne faisait que le répéter pour tous les hôtes à la fois, sans réseau/proxmox ni boutons d'opération. Retirée (bouton, rendu `dessinerArbre`, CSS `.arbre-*`) ; la chaîne reste consultable hôte par hôte. Raccourcis clavier ramenés à 1-4. ### Modifié - **Vue Inventaire (cartes) : détail converti en inspection lecture seule.** La vue affichait « généré depuis le plan (lecture seule) » tout en exposant une surface d'édition (+ Hôte, ✕, bascule actif/planifié, ✨ Proposer, champs et cases de groupes éditables) qui ne pouvait pas être persistée (l'inventaire est généré ; `/api/inventaire` renvoie 409). Ces contrôles orphelins sont retirés : nom, champs Réseau/Proxmox et groupes en lecture seule, état affiché en badge. **Vérifier / Déployer** et les onglets d'inspection sont conservés. L'édition reste dans les vues **Serveurs / Applications / Bases**. Code mort supprimé (`ajouterHote`, `supprimerHote`, `definir`, `definirNom`, `definirEtat`, `basculerGroupe`, `proposer`, `autoProposer`, `prochainSeqLibre`, `marquerModifie`, état `modifie`). - **Référence des paramètres de `make config`** (`docs/config-proxmox.md`). Explique un par un les 16 paramètres Proxmox non sensibles + les secrets API demandés par l'assistant : sens, valeur par défaut, quoi saisir, et quels champs sont des constantes vs des défauts surchargeables par hôte. Renvois ajoutés depuis `make help` et `QUICKSTART.md`. Comble un trou : ces invites n'étaient expliquées nulle part de façon pérenne (impératif « exploitable sans IA »). - **Panneau « Intrants de base » dans le GUI.** Un bouton ⚙ Intrants ouvre une fenêtre unique pour saisir les valeurs communes à tout l'écosystème, avec une distinction visible entre **constantes** (valeur unique, non surchargeable) et **défauts** (valeurs proposées, surchargeables dans les instances). Les secrets ne sont **jamais** saisis ni affichés ici (garde-fou `INTRANTS_CLES_INTERDITES` + filtrage par schéma) : ils restent dans le Vault Ansible, édités en CLI ; le panneau les liste seulement à titre informatif. Un changement de `domaine_interne` (clé de voûte) demande une confirmation explicite ; un `domaine_interne` vide est refusé côté serveur. La nomenclature reste en lecture seule (modifiable dans le plan). Voir `docs/intrants-communs.md` et `docs/intrants-base-gui-conception.md`. - **Source unique d'identité partagée.** `domaine_interne` et `chezlepro_timezone` vivent désormais dans `inventories/partage/intrants-identite.yml`, référencé par symlink depuis chaque environnement (`group_vars/all/10-intrants.yml`) — fin de la duplication lab/production. - **Info-bulles d'aide sur les champs du GUI.** Survoler la description d'un champ à saisir affiche une bulle avec des instructions sommaires. ## 2026-06-28 ### Ajouté - **Document de présentation de l'écosystème Chezlepro** (`docs/ecosysteme-chezlepro.md`). Description vulgarisée à destination client/partenaire : les piliers de l'écosystème souverain, le modèle reproductible (plan → génération → clonage → conformité), et un inventaire des **mesures de renforcement** réellement en place (SSH durci, fail2ban, sysctl, nftables, AppArmor, auditd, mises à jour automatiques, gestion des secrets, garde-fous destructifs, sécurité par l'architecture). Honnête sur le statut « défini/validé vs déployé ». ### Ajouté - **Dimensionnement dérivé des ressources VM.** Les cœurs/RAM/disque d'une VM sont désormais **estimés depuis les logiciels hébergés + le socle SE**, au lieu d'hériter des specs du golden template (CPU/RAM identiques pour toutes les VM auparavant). Chaque rôle déclare son empreinte (`roles//meta/empreinte.yml`) ; le générateur somme par hôte (marge + arrondis) et écrit `proxmox_coeurs`/ `proxmox_memoire`/`proxmox_disque_taille` ; le clonage passe `cores`/`memory` à Proxmox (`omit` si absent → aucune régression). Override par hôte possible dans le plan (`serveurs.yml`). Voir `docs/dimensionnement-ressources.md`. ### Corrigé - **`make instancier-appliquer FORCE=1` n'honorait pas `--force`.** La recette Makefile lançait `instancier.py appliquer` sans relayer `FORCE` ; le message « Utilise FORCE=1 » était donc trompeur (un changement de plan intentionnel restait bloqué). La recette passe désormais `$(if $(FORCE),--force)`. Trouvé en dogfooding. ## 2026-06-24 — Première publication publique Première mise à disposition publique de **Set-OPS**, moteur Ansible d'écosystèmes numériques souverains sur Proxmox — offert à la communauté québécoise par l'**Alliance Boréale**, à la Saint-Jean-Baptiste 2026. - **Moteur générique, piloté par un plan déclaratif** : on édite le plan (`instance/plan/*.yml`), l'inventaire Ansible se génère, les VM se clonent depuis un golden template Debian 13, les rôles s'appliquent par groupes. Tout passe par `make`, le GUI local et la documentation. - **Piliers d'un écosystème souverain** : socle Debian durci, AC/PKI interne, DNS interne, identité (LDAP + SSO), relais courriel, bases de données, observabilité, forge. - **Catalogue de modèles prêts à déployer** (`exemples/modeles/`) : un hébergeur copie un modèle, le renseigne à ses couleurs, et instancie. - **Souveraineté jusqu'au bout** : Set-OPS s'exploite entièrement à la main, sans aucune IA. Pour démarrer : **`QUICKSTART.md`**.