# CHANGELOG — Set-OPS ## 2026-10-07 (105) — Les releases et le wiki n'allaient que là où on les poussait à la main **94 preuves.** La forge du site fait autorité (D-81), mais deux choses n'y arrivaient jamais d'elles-mêmes : les étiquettes de release, et le wiki. ### Les releases manquaient à la forge du site `v2026.10.07` poussée sur eregion, la question suivante a été : et la forge du site ? Mesure (`git ls-remote --tags`) : elle portait `v2026.10.04`, pas `v2026.08.21` ni `v2026.10.07`. Le colis de `make publier` (`genome_colis.py`) ne transporte que `refs/heads/`, et `publier.py` ne poussait que la branche vers eregion. `v2026.10.04` y avait été portée par un geste à part, que rien ne gardait. Les deux étiquettes manquantes ont été poussées par `origin` (rebond SSH du poste), et relues identiques des deux côtés. **Fait** : - `genome_colis.py` : le colis emporte toutes les étiquettes (`bundle create … --tags`) ; le runner les récupère et les pousse avec la branche, **sans forcer** : une étiquette déjà présente sous un autre objet est refusée, des deux côtés. Une release ne bouge pas. - `genome_pousser.yml` : la forge est relue par son API (`git/refs/tags`), et doit porter toutes les étiquettes du poste. - `publier.py` pousse aussi les étiquettes vers eregion. **Option écartée** : faire d'eregion un miroir poussé par la forge du site. Mesuré : eregion porte deux branches de contribution non fusionnées (`essai_integration`, 4 commits ; `fix_client_pki`, demande de fusion n° 5 ouverte), qu'un miroir effacerait. L'exploitant garde les deux forges poussées depuis le poste, par prudence envers les contributions. **Éprouvé** : `test_genome_colis.py` (dans `make test`), sur de vrais dépôts git jetables : sans étiquette, la tête passe ; une release annotée arrive sur la forge, même objet ; une étiquette déplacée est refusée, la forge garde l'ancienne. Témoin : sur l'ancien `genome_colis.py`, le test échoue dès « la release voyage jusqu'à la forge ». `make verifier` conforme, **94/94**. **Pas encore éprouvé** : la relecture par l'API de la forge (`git/refs/tags`). Le premier `make publier` qui suivra le dira : « N etiquette(s) du poste, toutes sur la forge ». ### Le wiki de la forge du site était vide `make wiki-publier` publie vers UNE forge (`WIKI_REMOTE`) ; son témoin (`docs/audit/wiki-publie.yml`) montrait la dernière publication, le 2026-09-29, vers eregion seulement. Rien n'avait jamais publié vers la forge du site. Sa branche de wiki demandée à l'API de Forgejo (`wiki_branch: main`), le wiki est publié sur les deux forges ; relus : même arbre (`b3a70ba`), 96 pages, 8 figures, des deux côtés. Le témoin ne retient qu'une forge (la dernière publiée, eregion) : P60 ne voit pas l'autre. ## 2026-10-07 (104) — L'historique de supervision traverse la reconstruction : les deux locataires, sur `2cbfa2d` **94 preuves.** Les deux locataires rasés et reconstruits par le site qui les nomme, sur le même code, avec des témoins pris pour de vrai : aucun écart. C'est le commit à étiqueter. ### Technolibre, puis Chezlepro, 43 min chacun `make reconstruire-locataire` (journaux `OPS-Technolibre/logs/reconstruction-20261007-153647.log` et `OPS-Chezlepro/logs/reconstruction-20261007-171658.log`) : le runner du site tire `2cbfa2d` ; 13/13 VM rasées puis recréées ; `valider` sans échec ; pare-feu Proxmox 13/13, 0 critique dans Icinga. **8 jeux** restaurés chacun, dont le nouveau jeu `icinga`, tous depuis l'instantané `avant-raser` du jour, et chacun **encore au dépôt** au bilan. ### Les témoins : aucun écart, mesuré contre l'instantané d'avant rasage Par nœud : Nextcloud, annuaire, AC d'Icinga et web dorsal **identiques** ; AC (seule sa base `db/`), rspamd, courriel, PostgreSQL **conformes**. Les seuls changements PostgreSQL sont des ajouts (historique d'Icinga, événements de Keycloak) et ce que Keycloak renouvelle de lui-même. ### L'historique d'Icinga est resté, et rattaché Ce qui distinguait cette reconstruction des précédentes, mesuré sur la base vivante : | | Technolibre | Chezlepro | |---|---|---| | AC d'Icinga | `ca.crt` du 2026-10-07 12:01:15 (gardée) | `ca.crt` du 2026-10-07 14:33:48 (gardée) | | Environnement d'Icinga DB | `a11feb68…`, inchangé | `0afe46c3…`, inchangé | | Lignes d'historique d'avant | 385, **0 retirée** | 384, **0 retirée** | | Lignes orphelines (sans hôte vivant) | **0** sur 769 | **0** sur 768 | L'historique perdu de Chezlepro avant 14:34 aujourd'hui (reconstruction sur `12caff1`, avant la correction) ne revient pas : il appartenait à l'ancien environnement. ### Observé, non traité Keycloak perd ses **sessions hors ligne** à chaque reconstruction (2 sur 2, sur les deux locataires) ; les applications qui en vivent doivent se reconnecter. À comprendre avant de le dire normal. ## 2026-10-07 (103) — L'historique de supervision survit à la reconstruction ; les témoins comparent par clé **Chezlepro reconstruite** sur `12caff1` (43 min, journal `OPS-Chezlepro/logs/reconstruction-20261007-140824.log`) : rasage par la face (13/13), pool `OPS-Chezlepro`, `valider` à 0 échec, pare-feu 13/13. Les 7 jeux restaurés depuis l'instantané `avant-raser` de 14:08 — **toujours au dépôt** après le premier dépôt d'après reconstruction : le défaut de M4 est corrigé dans les faits. Témoins : aucun écart. **Ce que « 412 -> 380 lignes » cachait.** Comparées à l'instantané d'avant rasage, ligne à ligne : les 412 lignes d'historique d'Icinga DB d'avant étaient **toutes** perdues, les 380 présentes toutes neuves (14:34 -> 14:49). Mesure par clé sur toute la base : ses 30 tables renouvelées. Cause : une **décision du 2026-09-30** (`serveur_postgresql` excluait la base d'Icinga de la restauration), parce qu'Icinga 2 dérive l'environnement d'Icinga DB — et tout identifiant d'hôte, de service et d'historique — de **son AC**, recréée à chaque reconstruction (`37503b53fd8…` avant, `0afe46c3…` après, mesuré). Restaurée seule, la base serait restée orpheline. **Décision revue avec l'exploitant** : l'historique se garde. Il porte la disponibilité (`sla_history_state`) qu'une offre ou un label doit pouvoir prouver, et la mémoire des incidents. **Fait** : - **Jeu `icinga`** au catalogue de `client_backup` : `/var/lib/icinga2/ca` et `/var/lib/icinga2/icingadb.env`. `serveur_icinga` les remet **avant** `icinga2 api setup`, qui réutilise une AC présente (« Found CA, skipping and using the existing one », mesuré sur mon-01 ; aucun paquet n'en crée). `serveur_icinga` entre aux détenteurs d'état. - `serveur_postgresql` ne refuse plus de restaurer la base d'Icinga (reste exclu : `nextcloud`, qui se remet lui-même). - L'import du schéma Icinga DB ne se fait plus que sur une base **vierge** : le marqueur vit sur mon-01, que la reconstruction efface, alors que la base revient restaurée. - **Les témoins comparent PostgreSQL par clé** (première colonne, au format du dump), et non plus par nombre de lignes. Règles tirées de la mesure sur Chezlepro : **écart** pour une base dont plus aucune ligne d'avant ne subsiste, et pour toute ligne perdue d'une table d'historique ; listé sans écart, ce que Keycloak renouvelle de lui-même (`jgroups_ping`, sessions, 23 configurations de composants sur 139). Jeu `icinga` : `ca.crt`, `ca.key`, `icingadb.env` sont des identités. `template0` n'est plus dite « nouvelle ». **Éprouvé** : `test_temoins.py`, 23 contrôles, dont le cas réel (même nombre de lignes, aucune en commun : écart) et l'environnement d'Icinga changé (écart). Couverture : `serveur_icinga` inclut son point de restauration. `make verifier` conforme, **94/94**. **Déployé** (confirmé par l'exploitant), simulé d'abord, sur les deux locataires : `client_backup` (comparateur partout ; sur mon-01, le jeu `icinga`, son minuteur, ses contrôles), puis `serveur_icinga` sur mon-01. Un ordre à respecter : sur Chezlepro, `client_backup` est passé le premier, et le premier contrôle du dépôt de mon-01 a reçu `404 No objects found` — Icinga ne connaissait pas encore ses services « sauvegarde » et « restauration », que `serveur_icinga` crée d'après la liste des détenteurs d'état. Le dépôt, lui, avait réussi (`3cb7b5bd` : `/var/lib/icinga2/ca`, `icingadb.env`). `serveur_icinga` puis `client_backup` : 0 échec, premier rapport complet. Jeu `icinga` acté « neuf » (Chezlepro) et « en place » (Technolibre) : aucune AC écrasée. **Les témoins par clé, rejoués sur Chezlepro** contre `avant-raser` : trois écarts, `icingadb.public.history`, `sla_history_state` et `state_history`, toutes leurs lignes d'avant perdues (412, 410, 412). C'est la règle d'historique qui les voit ; celle de la base entière renouvelée ne s'est pas déclenchée, des tables fixes (version du schéma) gardant leurs clés. Le reste : Nextcloud, annuaire identiques ; AC, DKIM, courriel conformes. **Reste** : reconstruire Technolibre. L'environnement d'Icinga DB doit garder son identifiant, l'historique d'avant rester visible dans la vigie, et les témoins ne plus voir d'écart. Puis Chezlepro, puis la release. **Hors périmètre, remarqué** : `client_backup/tasks/verifier.yml` active son minuteur sans garde `not ansible_check_mode` ; une simulation sur un nœud qui n'a pas encore l'unité échoue (« Could not find the requested service »), ce qui n'arrive pas au vrai passage. ## 2026-10-07 (102) — Le devis de placement vérifiait un gabarit que personne ne clone **Trouvé au pré-vol de la reconstruction de Chezlepro** (`make placement-plan TENANT=OPS-Chezlepro`, runner du site, lecture seule) : rasage conforme (13 VM, chacune sous son nom, aucun conflit), mais placement en écart, avec un gabarit « None » introuvable. **La cause.** Depuis le 2026-09-01, le gabarit et son stockage viennent du **site** (`underlay.py --gabarit` : 9006 sur `vishnu`, stockage `CephNVMe`) : `cloner-vm` les passe en `-e`, par-dessus le tenant. Chezlepro suit la règle et ne déclare plus de gabarit ; Technolibre déclare encore `99998` et `TrueNAS`, valeurs que plus rien n'utilise (les 13 clones de M4 sont nés du 9006). Le devis lisait le tenant : il **validait chez Technolibre ce que personne ne clone**, et refusait Chezlepro. Défaut antérieur à la face (l'ancien chemin lisait les mêmes valeurs) ; il ne touche pas le clonage. **Fait** : `devis_placement.ce_que_le_clonage_utilise()` remplace le gabarit et le stockage du tenant par ceux du site, même règle que le Makefile : le site décide ; s'il ne déclare rien, le tenant reprend la main. **Éprouvé** : `test_devis_placement.py`, un sixième test (valeurs périmées remplacées, tenant sans gabarit servi, site muet = tenant tel quel). Témoin : le correctif neutralisé, le test échoue. `make verifier` conforme, **94/94**. **Reste** : les lignes `proxmox_clone_vmid_modele` et `proxmox_clone_source_nom` de Technolibre sont mortes ; les mettre en commentaire comme chez Chezlepro changera sa face (à republier). Sans urgence : le site a la priorité partout. ## 2026-10-07 (101) — M4 : Technolibre reconstruite par le site qui la nomme ; l'instantané d'avant rasage ne survivait pas **M4, la preuve par reconstruction** (`make reconstruire-locataire TENANT=OPS-Technolibre`, lancée par l'exploitant, 44 min, journal `OPS-Technolibre/logs/reconstruction-20261007-113441.log`) : - le runner du site tire `a204ead` ; `raser` s'annonce « Locataire nommé … d'après sa face publiée », 13/13 VM détruites ; `creer` range les 13 VM dans le pool `OPS-Technolibre` ; - insémination, armement, montage : `flux`, socle, `deployer-tout`, `valider` à 0 échec (courriel de bout en bout, HTTPS par l'edge, Prometheus, restauration d'un instantané) ; - pare-feu Proxmox : 13/13 actifs, `connectivite` saine, 0 critique dans Icinga ; - **les 7 jeux d'état restaurés depuis l'instantané pris à 11:34:44**, à l'étape `sauvegarder`, juste avant le rasage (marqueurs de chaque nœud, journal restic). La matérialisation est donc prouvée sur le cluster : le site crée, rase et range un locataire d'après sa face, sans monter son dépôt. **Le défaut** : à 12:16, le premier dépôt de chaque machine reconstruite (gestionnaire « Premier rapport à Icinga ») a **chassé l'instantané de 11:34** par `--keep-daily`, le même jour, sur les 7 nœuds. Rien de perdu (le dépôt de 12:16 porte l'état remis), mais plus rien à quoi le comparer, et plus moyen de refaire une restauration depuis l'avant-rasage. Les témoins prévus après coup n'ont pas pu être pris. Et une lecture trompeuse : le bilan affichait un « candidat » recalculé à l'heure du bilan (l'instantané de la veille, une fois celui de 11:34 chassé), sans dire ce qui avait été restauré. Je m'y suis trompé un moment. **Fait** : - **Le dépôt d'avant rasage est étiqueté** `avant-raser` (`setops-sauvegarder.sh --avant-raser`, nouvelle unité `setops-sauvegarde-avant-raser.service`, lancée par `make sauvegarder-maintenant` et l'étape `sauvegarder`). La rétention garde l'étiquette (`--keep-tag`) **jusqu'à la reconstruction suivante** (décision de l'exploitant) : le dépôt suivant la reçoit, l'ancien la perd, **après** le dépôt, jamais avant. Un nœud sans la nouvelle unité refuse de déposer plutôt que de déposer sans étiquette. - **Le bilan** (`setops-restaurer etat`) nomme l'instantané `avant-raser`, dit que le candidat est « ce que choisirait une restauration lancée maintenant », et marque chaque instantané restauré « (au dépôt) » ou « (RETIRÉ du dépôt) ». - **Les témoins** : `setops-restaurer temoins` extrait l'instantané d'avant rasage (ou `--candidat`), lit l'annuaire vivant, et `setops-temoins` compare. **Écart** = une perte (fichier, courriel, entrée, base, rôle, table) ou une identité changée (clés et certificats de l'AC, DKIM, `instanceid` de Nextcloud, `userPassword` d'une entrée). Le reste est listé (« modifié », « retiré », « nouveau ») sans être un écart. `make temoins-etat [HOTE=] [SOURCE=candidat]` ; l'étape `bilan` de la reconstruction les lance et s'arrête sur un écart. - `temoins-etat` et sa variable `SOURCE` entrent au registre des runbooks (la console les proposera) ; le runbook d'exploitation et le README du rôle décrivent l'étiquette et les témoins. **Éprouvé** : - `test_temoins.py` (nouveau, dans `make test`) : 18 contrôles sur des arbres fabriqués, dans les deux sens (la vie ordinaire passe ; clé de l'AC, DKIM, instanceid, `sysadmin` remis à l'amorçage, entrée, base, rôle, table, courriel et fichier perdus échouent). - `test_restauration.py` : les témoins prennent l'instantané **étiqueté**, même plus ancien que le candidat ; un instantané chassé est dit « RETIRÉ » ; la sauvegarde étiquette, retire l'étiquette au seul précédent et après le dépôt, et la rétention la garde aussi au dépôt ordinaire. - Témoins : `--keep-tag` retiré, ou les témoins ramenés au candidat, trois tests échouent. - `shellcheck` des deux scripts rendus : rien, avant comme après. - `make verifier` conforme, **94/94**. **Déployé** : `client_backup` sur les deux locataires, simulé d'abord (`--check --diff`) : 4 changements par nœud détenteur d'état (script, outil, comparateur, unité), 2 ailleurs, 0 échec ; l'application a fait exactement cela. **Premier essai réel**, Technolibre, contre l'instantané de la veille (`SOURCE=candidat` : celui de 11:34 n'existe plus). Nextcloud identique (268 fichiers, `instanceid` compris) ; annuaire identique (12 entrées, `sysadmin` compris) ; clés et certificats de l'AC inchangés, seule sa base `db/` a bougé ; DKIM inchangée ; aucun courriel perdu ; Keycloak a quelques sessions et événements de plus. **Un faux écart**, du comparateur : `template1` « perdue », parce que les bases vivantes étaient lues sans les modèles alors que `pg_dumpall --clean` recrée `template1`. Corrigé (toutes les bases) ; le test reproduit le filtre de PostgreSQL, et l'ancienne requête le fait échouer. **Reste** : reconstruire Chezlepro avec des témoins pris pour de vrai, puis étiqueter la release. ## 2026-10-07 (100) — Pré-vol de M4 : la face tient devant le cluster, deux en-têtes disaient faux **Le pré-vol**, lancé par l'exploitant depuis le runner du site, en lecture seule : `make locataire-raser TENANT=OPS-Technolibre` (sans `CONFIRMER`) et `make placement-plan TENANT=OPS-Technolibre`. C'est la **première confrontation de la face au vrai cluster**, celle que M2 n'avait pas pu faire depuis le poste (errno 111). Résultat : 13 VM, chacune sous son nom, **aucun conflit** ; placement conforme (`asgard`, `TrueNAS`, gabarit `99998`, les six réseaux `t23*`). Rien n'a été touché. **Deux en-têtes disaient faux** sur le chemin `TENANT=`, sans rien changer à ce qui est visé : - `raser.py` annonçait « Écosystème **monté** » sur le runner du site, où rien n'est monté. Il dit désormais « Locataire **nommé** … d'après sa face publiée » (`entete()`), et le refus faute de nom dit « Nomme » au lieu de « Monte ». - `devis_placement.py` annonçait « tenant « **?** » » : avec `--locataire`, aucun fichier source d'où tirer le nom. `nom_du_tenant()` rend celui qu'on a désigné. Corrigés **avant** M4, pour que la reconstruction couvre le commit qu'on étiquettera. **Éprouvé** : - `test_raser.py` juge l'en-tête sur la **sortie réelle** de `raser.main`, aux deux chemins, face à un faux cluster qui rend exactement les machines publiées. - `test_devis_placement.py` : le nom désigné, le nom tiré de la source, et l'en-tête affiché. - Témoins : les anciens libellés remis en place, les deux tests échouent (« EN-TETE TROMPEUR », puis l'assertion du devis). ## 2026-10-05 (99) — Matérialisation, M3 : la reconstruction nomme son locataire, et le pool suit **Un trou dans M2, trouvé avant tout usage.** `locataire-creer` comparait les paramètres de clonage, pas la ligne qui part. Le sous-make `cloner-vm` nommait encore le pool par l'instance **montée** (`devis_proxmox_pools.py --pool-actif`). Sur le runner du site, sans instance, la VM serait née hors de tout pool. Sur le poste, avec Chezlepro monté, `locataire-creer TENANT=OPS-Technolibre` aurait rangé les VM dans le pool de Chezlepro. Mon `make -n` ne descendait pas dans le sous-make : il ne pouvait pas le voir. Personne n'a lancé `locataire-creer` entre-temps. **Fait** : - `devis_proxmox_pools.py --pool-du-locataire ` : même dérivation (`pool_de`), à partir de l'index de la face. `cloner-vm` l'emploie quand `TENANT` est donné. - `reconstruire_locataire.py` : les étapes `raser` et `creer` lancent sur le runner du site `make locataire-raser TENANT=…` et `make locataire-creer TENANT=…` ; plus de `SETOPS_INSTANCE=/opt/setops/OPS-x`. Le dépôt du locataire y reste tiré : sa face y est publiée. - `docs/conception-contextes.md` : l'état de l'étape 3. **Éprouvé** : - `scripts/tests/test_appels_locataire.py` (dans `make test`) : un faux `ansible-playbook` consigne la ligne qui part réellement. Pour les 26 machines, trois appels : l'ancien chemin (instance montée), le site (aucune instance, `TENANT=`), le poste (l'autre locataire monté, `TENANT=`). Identiques **à l'argument près**. - Témoin : l'ancienne ligne de pool remise en place, le test échoue (pool vide côté site, pool de l'autre locataire côté poste). - Le test désigne les dépôts par des liens sans espace : le Makefile prend `SETOPS_INSTANCE` dans des `$(wildcard …)` qu'une espace coupe en deux (`Espace Chezlepro` sur le poste ; le runner du site vit sous `/opt/setops`). Premier essai faussé par là, les 26 « écarts » venaient de l'instrument. **Pas encore** : la conformité horaire du site (`conformite-fabric.sh`) et `eprouver_parefeu.py` montent encore un locataire comme instance pour leurs devis — reste de l'étape 3, à part de M3. M4, la preuve par reconstruction, reste à faire et relève de l'exploitant. ## 2026-10-05 (98) — Matérialisation, M2 : les commandes du site nomment leur locataire **Le chemin** (`docs/conception-contextes.md`, matérialisation M2) : le runner du site crée, rase et confronte le placement d'un locataire **nommé**, d'après sa face réseau, sans monter son dépôt comme instance. **Fait** : - `make locataire-creer TENANT= CONFIRMER=true` remplace `SETOPS_INSTANCE=… make flotte-creer`. `flotte-creer` et `creer-vm` acceptent `TENANT=` : la liste des machines actives (`contexte.py --hotes-actifs`) et les paramètres de clonage (`contexte.py --parametres-clonage … --hote`) viennent de la face. Sans `TENANT`, l'ancien chemin est inchangé, et l'instance reste exigée. - `make locataire-raser TENANT= CONFIRMER=true INSTANCE=` (et `raser TENANT=`) : `raser.py --locataire` prend les couples (hôte, VMID) dans la face. Les quatre verrous restent : liste, refus d'un VMID au nom étranger, `CONFIRMER`, nom tapé. - `make placement-plan TENANT=` : `devis_placement.py --locataire` lit les valeurs de placement (`placement`, désormais dans la face) et les ponts des machines publiées. - Les deux cibles entrent au runbook du site (P83). **Éprouvé** : - Lignes de paramètres de clonage **identiques à l'octet** aux appels de l'ancien chemin, pour les 26 machines ; listes de machines actives identiques. - **P94**, « Créer, raser et placer un locataire par sa face visent les mêmes machines » : chaque intrant comparé à la commande de l'ancien chemin, lancée à part : `lister-actifs` (création), `plan_derive` (le plan, via `serveurs.py lister`) pour raser, `proxmox.yml` et les ponts dérivés de l'inventaire (placement). Aucun écart sur les deux locataires. - `test_contexte.py`, 116 contrôles : un VMID, un état, un pont, une valeur de placement altérés dans la face se voient, chacun du côté qu'il touche. - `make -n` : `TENANT=` appelle les nouvelles options sans instance montée ; sans `TENANT` et sans instance, refus. - `make verifier` conforme, **94/94**. Les trois documents comptent 94 preuves. **Pas éprouvé ici** : la confrontation au cluster. Depuis le poste, l'API Proxmox refuse la connexion (errno 111) aux **deux** chemins ; une sortie identique sur un refus ne prouve rien. Elle se fera depuis le runner du site (M3), puis par reconstruction (M4). ## 2026-10-05 (97) — Matérialisation, M1 : la face publie les paramètres de clonage **La décision de l'exploitant** : la seconde voie, fidèle au modèle — le **site** matérialise les VM d'un locataire depuis sa face réseau, sans monter son dépôt. Plan validé avec lui : M1 la face publie les paramètres de clonage ; M2 des commandes du site qui nomment leur locataire (`locataire-creer`, `locataire-raser`, `placement-plan TENANT=`), à appels identiques ; M3 la reconstruction les emploie ; M4 la preuve par reconstruction. **Le constat** : la matérialisation tourne **déjà** sur le runner du site, mais elle y monte le dépôt du locataire comme instance (`SETOPS_INSTANCE=/opt/setops/OPS-x make flotte-creer`), et `make creer-vm` lit son inventaire par `inventory_host.py parametres-proxmox`. **Fait (M1)** : chaque machine de la face porte `clonage`, exactement ce que `parametres-proxmox` rend, calculé par la **même** fonction : VMID, adresse, masque, passerelle, VLAN, pont, stockage, disque, nœud, cœurs, mémoire, DNS d'amorçage, domaine, et les clés d'amorçage — **publiques** (vérifié : `ssh-ed25519` du runner du site et du runner du locataire, aucun bloc privé). Faces republiées. **Éprouvé** : - **P93**, « La face publie, machine par machine, les paramètres de clonage de l'inventaire » : la face publiée comparée à la commande `parametres-proxmox` elle-même, lancée à part pour chacune des 26 machines. Aucun écart. - Le témoin a d'abord signalé un écart sur `ops-01` : la seule machine à deux clés d'amorçage, sur deux lignes, que ma lecture de la commande coupait au premier retour à la ligne. Le défaut était dans le témoin ; il lit désormais la sortie comme le shell (`shlex`). - `test_contexte.py`, 110 contrôles : un paramètre altéré dans la face se voit. - `make verifier` conforme, **93/93**. Les trois documents comptent 93 preuves. ## 2026-10-05 (96) — Étape 3 : les pools et les tunnels d'administration lisent la face réseau **Le chemin** (`docs/conception-contextes.md` §6), étape 3, matérialisation, premiers temps : les deux devis du site qui lisaient encore le plan des locataires. **Fait** : - la face publie l'**état** de chaque machine (actif ou planifié, lu dans l'inventaire) et les **pairs WireGuard entiers** (`vpn_admin` les reprend tels quels ; des clés publiques et des adresses, rien de secret) ; faces republiées ; - `devis_proxmox_pools.py` prend dans la face le nom, le VMID et l'état de chaque machine, au lieu de relire `plan/serveurs.yml` et de re-dériver les VMID ; - `vpn_admin.py` prend dans la face les pairs et l'index, au lieu de `plan/acces.yml` et de la nomenclature. Sans face, l'ancienne lecture, et chacun le dit. **Le critère, comparé comme il faut** (leçon de (95)) : un `git worktree` à `2e9b31b`, arbre complet, même environnement. Identiques octet pour octet : le plan des tunnels (`tunnels()` et `plan_vpn()`), les pools, la frontière, Proxmox, le SDN, le commutateur, l'inventaire du site. **Témoins qui doivent bouger** (`test_contexte.py`, 107 contrôles) : un VMID altéré dans la face est repris par le devis des pools ; des pairs retirés de la face retirent le tunnel du plan (1 pair, puis 0). **Validation** : `make verifier` conforme, 92/92. **Reste de la matérialisation** : les outils du poste qui agissent sur le locataire monté (placement, configuration Proxmox, clonage, rasage) lisent son inventaire. Décider qui matérialise — le poste sur l'inventaire du locataire, ou le site sur sa face — est une question pour l'exploitant. ## 2026-10-05 (95) — Une régression publiée : le devis des pools vide, et une comparaison qui ne testait rien **Ce qui s'est passé.** (94) a fait passer la découverte des locataires par la face réseau, qui ne publiait que cinq champs de leur nomenclature. Le devis des pools dérive les VMID par `deriver_nomenclature`, qui lit aussi `fonctions` : sans ce champ, **chaque pool de locataire s'est retrouvé vide** (0 membre, toutes les VM « sans VMID »), sans erreur. Publié dans `d0a6002`. **Pourquoi la vérification ne l'a pas vu.** Pour prouver « identique à HEAD », j'ai lancé l'**ancien script** (`git show HEAD:scripts/X.py`) à côté du nouveau. Mais il importait le **nouveau** module de découverte : pour tous les consommateurs de la découverte, la comparaison comparait le nouveau code à lui-même. Sept « IDENTIQUE » qui ne testaient rien. **Ce que ça a coûté** : un devis publié faux. Rien n'applique les pools automatiquement, et le clonage nomme son pool par une lecture directe de la nomenclature, non touchée. **Corrigé** : - la face publie aussi `fonctions` ; faces republiées. Les pools retrouvent leurs 13 VM par locataire ; - **la comparaison refaite correctement** : un `git worktree` à `d6591cb` (avant toute l'étape 3), arbre complet, même environnement. Les sept sorties du site (commutateur, frontière, Proxmox, SDN ×2, pools, inventaire du site) sont **identiques octet pour octet**. Une seule différence apparente — `instance_active` à la frontière — venait du worktree, sans lien `instance` : la frontière lit le lien et non `SETOPS_INSTANCE`, une des devinettes de contexte que la conception doit supprimer. Avec le même lien : identique ; - garde de régression dans `test_contexte.py` (105 contrôles) : chaque pool porte autant de VM que le locataire en publie, aucune sans VMID. **La leçon, retenue** : comparer à l'ancien code se fait dans un **arbre complet**, jamais en lançant l'ancien script seul ; vérifier `git diff --stat` après une édition scriptée avant de comparer ; accompagner chaque « identique » d'un témoin qui doit changer. **Validation** : `make verifier` conforme, 92/92. ## 2026-10-05 (94) — Étape 3 : le site découvre ses locataires par leur face réseau **Le chemin** (`docs/conception-contextes.md` §6), étape 3. La découverte des locataires (`devis_reseau.decouvrir_du_site`) alimente tous les devis du site : frontière, Proxmox, SDN, pools, commutateur, et l'inventaire du site. Elle lisait la nomenclature de chaque locataire. **Fait** : - relevé de ce que le site lit d'une nomenclature : **cinq champs** (`index`, `categories`, `reservations`, `cidr_hote`, `federe`) ; la face réseau les publie (`nomenclature`) ; - `decouvrir_du_site` part des noms que le site déclare (`underlay.tenants`) et prend index et zones dans la face publiée de chacun. Ordre, filtre `federe`, avertissement pour un nom sans dépôt et refus d'un site sans locataire inchangés. Sans face, la nomenclature, et elle le dit ; - faces republiées chez les deux locataires. **Le critère** : les sept sorties des consommateurs du site sont **identiques octet pour octet** à celles de la version de HEAD, lancée à part : commutateur (8,8 Ko), frontière (167 Ko), Proxmox (63 Ko), SDN (ses deux formats), pools, inventaire du site (81 Ko). `make frontiere-plan` contre la frontière réelle : rien à faire. **Ce qui lit encore la nomenclature des locataires**, nommé : la découverte de la **fédération** (`decouvrir`), qui sert sur le poste à garder les collisions d'index (P21) et à lister les instances — légitime, ce n'est pas le site — et que `decouvrir_du_site` appelle encore pour son repli ; `vpn_admin.py` (index pour le tunnel) ; `devis_proxmox_pools` (nommer les VM, avec le plan). Ils partiront avec la matérialisation et la fin du repli. **Éprouvé** : - `test_contexte.py`, 103 contrôles : un index altéré dans la face publiée est celui que le site découvre (17, puis 99) — la découverte lit bien la face. - `make verifier` conforme, 92/92. ## 2026-10-05 (93) — Étape 3 : la frontière lit la face réseau **Le chemin** (`docs/conception-contextes.md` §6), étape 3. Quatrième consommateur du site, le plus lourd : le devis de la frontière (167 Ko). Il lisait chez chaque locataire ses réseaux d'administration (`group_vars`), son inventaire (les adresses de chaque groupe, dont les runners et les primaires DNS de tous les locataires), ses variables (conditions `seulement_si`), son `plan/domaines.yml` et son `plan/acces.yml`. **Fait** (`devis_opnsense.py`) : quatre lecteurs, la face d'abord, l'ancienne lecture en repli explicite — `cibles_du_tenant()` (adresses d'un groupe), `admin_du_tenant()` (l'intrant), `applicable_chez()` (le verdict publié), et la face pour le tunnel (présent si un pair est actif) et pour savoir qui publie des zones. Les copies en ligne du réseau et du port du tunnel ne servent plus que le repli ; elles partiront avec lui. **Un faux succès, attrapé avant le commit** : la première modification s'est arrêtée sur une assertion (un commentaire séparait deux lignes du motif) **avant d'écrire**. La comparaison qui suivait a dit « identique » — l'ancien code comparé à lui-même. Relevé, refait ; la comparaison avec l'ancien code se fait désormais contre la version de HEAD, lancée à part. **Le critère** : le devis de la frontière est **identique octet pour octet** depuis la face, par le repli, et par l'ancien code (HEAD). `make frontiere-plan` contre la frontière réelle : rien à créer, rien à retirer. **Éprouvé** : - `test_contexte.py`, 102 contrôles : un verdict basculé dans la face publiée retire sa règle WAN (soumission Postfix 587 : 1 règle, puis 0) — la frontière lit bien la face. - `make verifier` conforme, 92/92. **Restent à basculer** : la découverte des locataires (index et catégories, lus par la frontière, le SDN, les pools et le commutateur), le placement et le clonage. ## 2026-10-05 (92) — Étape 3 : le pare-feu Proxmox lit la face réseau **Le chemin** (`docs/conception-contextes.md` §6), étape 3. Troisième consommateur du site : le devis du pare-feu Proxmox. Il lisait chez chaque locataire son inventaire (adresses, groupes, VMID), ses variables (pour les conditions `seulement_si`) et ses réseaux d'administration (intrant, `plan/acces.yml`). **Fait** : - la face réseau publie le **verdict de chaque flux conditionnel**, groupe par groupe (`conditions`), évalué chez le locataire par la même `flux_applicable` : le site n'a plus à lire ses variables. Chez Technolibre : la soumission de Postfix (465, 587) et le DNS public (5300) actifs ; les ports locaux de la vigie (8080) et de la console (8090) non ; - `contexte.inventaire_depuis_face()` reconstruit l'inventaire minimal que lisent les fonctions de flux (appartenance, adresse, VMID — rien d'autre n'y est lu) ; - `devis_proxmox_fw.py` lit la face publiée : inventaire reconstruit, verdicts, administration. Sans face, l'ancienne lecture, et il le dit ; - les faces des deux locataires sont republiées (le champ nouveau). **Le critère** : le devis Proxmox complet (63 Ko, les deux locataires) est **identique octet pour octet**, depuis la face ou par l'ancienne lecture. **Éprouvé** : - `test_contexte.py`, 101 contrôles : un verdict basculé dans la face publiée retire sa règle du devis (Postfix 587 : 3 règles, puis 0) — le devis lit bien la face, il ne la contourne pas. - `make verifier` conforme, 92/92 (P92 garde la face publiée à jour, verdicts compris). **Restent à basculer** : la frontière, le SDN, les pools, le placement et le clonage. **À commiter** : `face-reseau.yml` republiée chez les deux locataires. ## 2026-10-05 (91) — Étape 3 : le locataire publie sa face réseau, et le site la lit **Le chemin** (`docs/conception-contextes.md` §6), étape 3, sens locataire → site. Par symétrie avec la fiche du site, le site lit un fichier que le locataire **publie**, au lieu d'ouvrir ses fichiers internes. **Fait** : - `locataire.publier_face()` écrit `face-reseau.yml` dans le dépôt du locataire (environ 33 Ko, aucun chemin propre au poste, aucune date : deux publications sont identiques, vérifié) ; `make face-reseau-publier`, au runbook « Matérialiser » juste après `flux` ; publiée chez OPS-Chezlepro et OPS-Technolibre ; - **deux consommateurs du site basculent** (`site_inventaire.py`) : les **comptes de sauvegarde** lisent la clé publique dans la face réseau (plus de `group_vars` ouverts sous un `principal` écrit en dur) ; les **relations du DNS public** y lisent zones, zones signées, adresse et port du primaire (plus d'adresse re-dérivée avec la séquence 1 par défaut). Sans face publiée, l'ancienne lecture reprend, et le dit. **Le critère** : l'inventaire du site, sortie complète, est **identique** avant et après, octet pour octet ; identique aussi par le repli, quand on retire les faces publiées. **Éprouvé** : - **P92**, « Chaque locataire a publié sa face réseau à jour » : une face absente ou périmée est refusée. Aucun écart. - `test_contexte.py`, 100 contrôles : une face publiée altérée est vue périmée. - `make verifier` conforme, **92/92**. Les trois documents comptent 92 preuves. **Restent à basculer** : la frontière, le pare-feu Proxmox, le SDN, les pools, le placement et le clonage, qui lisent encore les fichiers internes des locataires. **À commiter** : `face-reseau.yml` chez les deux locataires. ## 2026-10-05 (90) — Étape 3, premier consommateur : l'inventaire d'un locataire se génère sans son site **Le chemin** (`docs/conception-contextes.md` §6), étape 3 : les consommateurs lisent les fiches au lieu des fichiers de l'autre, à résultat identique. Premier consommateur : l'instancier. **Le site dépose sa fiche** (décision du 2026-10-04) : - `site.deposer_fiche(locataire)` écrit `fiche-site.yml` dans le dépôt du locataire ; aucune date dedans, deux dépôts d'une même fiche sont identiques (vérifié) ; - `make fiches-site-deposer` (runbook « Tenir un site en état ») dépose chez chaque locataire dont le site est l'hébergeur actif ; le fichier se commite chez le locataire ; - déposée chez OPS-Chezlepro et OPS-Technolibre. **L'instancier la lit** : la zone du site et ses résolveurs (avec sa règle propre, pas de délégation de sa propre zone), les serveurs de temps, le mode de routage, viennent de `fiche-site.yml`. Sans fiche, il lit encore le site monté, et le dit (transition). **Le critère, mesuré en mémoire sans rien écrire**, pour les deux locataires : | | inventaire généré | |---|---| | site monté, avec fiche | **identique** au versionné, à l'octet près | | **site absent, avec fiche** | **identique** à l'octet près | | site absent, sans fiche | différent (sans temps, sans SDN, sans délégation DNS) | C'est la portabilité d'un locataire : son runner, qui n'a pas le dépôt de son site, génère désormais l'inventaire exact. **Éprouvé** : - **P91**, « La fiche du site est déposée à jour, et l'inventaire se génère sans le site » : la fiche déposée est celle que le site calcule aujourd'hui, et l'instancier, lancé dans un processus sans le site, rend l'inventaire versionné. Aucun écart chez les deux locataires. - `test_contexte.py`, 97 contrôles : une fiche déposée altérée est vue périmée, et elle change l'inventaire généré (le test la remet en place). - `make verifier` conforme, **91/91**. Les trois documents comptent 91 preuves. **À commiter** : `fiche-site.yml` chez les deux locataires. ## 2026-10-05 (89) — Étape 2 terminée : les sorties, et le contrat entier tient **Le chemin** (`docs/conception-contextes.md` §6), étape 2, frontière, dernier temps : ce qui sort de chez un locataire vers l'Internet. Avec lui, **l'étape 2 est complète** : chaque chose que le site lit chez un locataire est publiée par le locataire, et une preuve vérifie que la publication dit exactement ce que le site en tire. **Fait** : - `resoudre_flux.py` relève, avec les mêmes conditions que les entrées (groupe opérationnel, `flux_applicable`), les **sorties vers l'extérieur** de chaque machine, et les publie dans son `*.connectivite.json` (`sorties_externes` : protocole, port, rôle). Ses machines acceptent tout en sortie : il ne les relevait pas, la frontière les filtre ; - flux régénérés chez les deux locataires : les 13 fichiers de connectivité de chacun gagnent le champ, **aucun `.nft` ne bouge**. La sonde `connectivite` l'ignore ; - la face réseau reprend ces sorties ; `verifier_sorties(site, locataire)` les confronte, machine par machine, aux règles de sortie de la frontière vers « tout sauf l'interne », et signale toute sortie vers une autre destination. **Éprouvé** : - **P90**, « Ce qui sort d'un locataire vers l'Internet, la frontière le tient de lui » : aucun écart. Comparés chez Technolibre : 43 sorties publiées (machine × port), 7 règles de sortie (le système en 80, 443 et ICMP ; Postfix en 25 ; PowerDNS et le résolveur en 53). Les deux sorties fausses de `serveur_ops` étaient déjà retirées ((85), (87)). - `test_contexte.py`, 91 contrôles : une sortie retirée ou ajoutée est vue à la frontière. - `make verifier` conforme, **90/90**. Les trois documents comptent 90 preuves. **Le bilan de l'étape 2** : sept preuves (P84 à P90) ; trois choses que le locataire **jetait** ou **ne relevait pas** et publie désormais (les clients nommés d'un port public, le drapeau `poste`, ses sorties) ; deux sorties fausses retirées de la frontière, dont un passage entre locataires ; une garde sur la passerelle de zone. Prochaine étape (3) : les consommateurs du site lisent les fiches au lieu des fichiers internes, à résultat identique. ## 2026-10-05 (88) — La passerelle n'est pas une porte : une garde sur l'API Proxmox **Le reste ouvert de (87)** : chez un locataire en SDN, la passerelle de chaque zone est une interface de l'**hyperviseur**, dans le VRF du locataire. Une API Proxmox qui y répondrait serait jointe sans passer par la frontière. Fermée aujourd'hui (`ECONNREFUSED`). **Option choisie par l'exploitant** : tester la **propriété**, depuis les machines des locataires, plutôt que lire `net.ipv4.tcp_l3mdev_accept` sur les hyperviseurs. Cela aurait demandé un nouveau rôle sur des machines que Set-OPS ne gère pas (Prometheus les tire ; leur node_exporter 1.5 n'a pas de collecteur `sysctl`). **Fait** (`nftables_baseline`) : - la sonde `connectivite`, qui tourne chaque minute sur chaque machine, essaie aussi les ports de `nftables_baseline_connectivite_passerelle_interdits` (`[8006]`) sur la **passerelle par défaut**. `ouvert` → **CRITIQUE** (« PASSERELLE OUVERTE ») ; refusé, rejeté ou silencieux → attendu ; passerelle introuvable → avertissement, une garde qui ne s'exerce pas le dit. Ici, `ouvert` prouve bien quelque chose : entre une machine et sa passerelle, aucun relais ; - elle couvre toute cause : le réglage du noyau, un pare-feu d'hôte retiré, un service qui se mettrait à écouter partout ; - `CONNECTIVITE_PASSERELLE=` vise une autre « passerelle » : le contrôle négatif de la garde ; nouvelle mesure de performance `passerelle_ouverte`. **Éprouvé**, sonde rendue et lancée à la main : - cas normal sur `ops-01` et `web-dorsal-01` (Technolibre), `web-frontal-01` (Chezlepro) : la passerelle de zone refuse le 8006, sonde verte, ses mesures habituelles intactes ; - au site (`site-mon-01`, passerelle = la frontière) : verte, aucune fausse alarme ; - témoin négatif (443 interdit, « passerelle » = la forge du site, qui répond) : **CRITIQUE**, code 2. **À déployer** : `nftables_baseline` (groupe `serveur_durci`), chez les deux locataires et au site. Le ruleset nftables ne change pas ; seule la sonde change. **Validation** : `ansible-lint` (0 violation), `make verifier` conforme, 89/89. ## 2026-10-05 (87) — Une règle qui disait qu'un locataire pouvait créer des VM **La question de l'exploitant** : « un flux est permis entre une machine d'un tenant et l'API Proxmox ? » Déclaré : oui. `roles/serveur_ops/meta/flux.yml` portait une sortie 8006 vers `externe`, « API de l'hyperviseur : créer et cloner les VM d'un écosystème descendant », et la frontière en tirait une règle pour le runner de chaque locataire. **Mesuré** (2026-10-05) : **non**. Depuis trois machines de deux locataires, vers les neuf adresses des trois hyperviseurs et vers leur passerelle de zone, avec la forge du site pour témoin (200) : - `192.168.11.4x:8006` et `10.0.4.4x:8006`, et leur 22 : `ECONNREFUSED`. La frontière refuse ; son journal porte les tentatives en `block`, par le refus audible du lien de transit. La règle déclarée visait « tout sauf les plages privées », et les hyperviseurs y sont : elle ne laissait passer **aucun** paquet ; - la passerelle de zone (`10.23.19.1`, l'hyperviseur lui-même dans le VRF du locataire) : `ECONNREFUSED`, sans passer par la frontière ; c'est l'hyperviseur qui refuse. Cause probable, **non vérifiée** (poste hors site, sans accès SSH aux hyperviseurs) : pveproxy écoute dans le VRF par défaut et `net.ipv4.tcp_l3mdev_accept` vaut 0. **Fait** : la sortie est retirée de `serveur_ops`, avec la mesure en trace. Une règle morte qui affirme un pouvoir qu'aucun locataire ne doit avoir. Matérialiser reste au **site** (`serveur_ops_site` → `SETOPS_FABRIC`, 8006, inchangé). Registre des flux régénéré ; flux des locataires inchangés. **Simulé** (`make frontiere-plan`) : 5 retraits — la règle 8006 des deux locataires et celle de la console du site (`SETOPS_SITE_SERVEUR_OPS`, morte pour la même raison), et les deux alias `SERVEUR_OPS` des locataires, devenus orphelins. Rien d'autre sur 335 règles et 12 routes. **Reste ouvert** : le chemin par la passerelle de zone contourne la frontière. Il est fermé aujourd'hui ; la garde qui le surveillera est à décider avec l'exploitant. **Validation** : `make verifier` conforme, 89/89. ## 2026-10-05 (86) — Étape 2 : la frontière, troisième temps (l'administration) **Le chemin** (`docs/conception-contextes.md` §6), étape 2, frontière, troisième temps, première moitié : ce que l'administration atteint chez un locataire. Les sorties suivent. **Fait** (`scripts/contexte.py`) : - la face réseau dit, pour chaque entrée ouverte à tous, si son flux l'ouvre **aussi au poste** (`poste`, vrai par défaut). Elle le lit dans le catalogue des flux du moteur, que le runner du locataire possède aussi : aucun changement chez le locataire. Une seule entrée est fermée au poste, le 25 de Postfix (un flux de serveur à serveur) ; - `verifier_administration(site, locataire)` confronte, pour **chaque classe d'administration** que la frontière range par interface (gestion, VPN), ce qu'elle atteint machine par machine avec ce que la face lui ouvre : le SSH, les entrées qui nomment l'administration parmi leurs sources, les entrées publiques ouvertes au poste. Le **tunnel** du locataire doit exister si la face en a un, et viser toute sa flotte ; ses ports (22 et 443) sont une politique du site, écrite dans le devis, pas une donnée du locataire. **Relevé, non modifié** : le devis justifie `poste: false` sur le 25 par « deux couches qui ne déclarent pas la même politique » (mesure du 2026-08-09 : la bordure autorisait, nftables refusait). Depuis le 2026-09-29, nftables accepte le 25 de partout (un port public ouvert à tous) : l'administration y accède donc par la machine, pas par la bordure. La bordure est la plus stricte, sans danger ; mais les deux couches divergent de nouveau, dans l'autre sens. **Éprouvé** : - **P89**, « Ce que l'administration atteint chez un locataire, la frontière le tient de lui » : aucun écart chez les deux locataires. - `test_contexte.py`, 86 contrôles : une administration altérée de **quatre façons** (le 25 ouvert au poste, les sources d'administration retirées d'une machine, un SSH d'administration retiré, le tunnel retiré) est vue à la frontière. - `make verifier` conforme, **89/89**. Les trois documents comptent 89 preuves. ## 2026-10-05 (85) — Un passage entre locataires que rien ne justifiait **Trouvé en confrontant la frontière à la face réseau** (sous-étape 3 de la frontière, `docs/conception-contextes.md`). Le relevé des sorties de Technolibre portait une règle anormale : son runner (`serveur_ops`) pouvait sortir en 443 vers **tout le supernet de Chezlepro**, et celui de Chezlepro vers tout celui de Technolibre, avec pour raison « cloner le génome depuis la forge du site ». **Cause** : `roles/serveur_ops/meta/flux.yml` déclarait cette sortie vers le pair `voisins_site`, écrite le 2026-08-24, quand la forge du génome vivait chez un locataire voisin. Elle est au **site** depuis ; le pair n'avait pas suivi. Vu d'un locataire, `voisins_site` désigne les **autres** locataires : la frontière, censée refuser par défaut entre locataires, leur ouvrait un passage. **Ce que ça exposait** : peu, en pratique. Côté destination, les machines n'admettent le 443 que depuis leur propre flotte et l'administration, dans leurs nftables comme dans le pare-feu Proxmox. Mais une couche d'isolement ne tenait plus. Et la règle ne servait à rien : le clonage du génome passe par la règle que déclare `serveur_forge_site` (chaque locataire vers `10.37.33.11`, en 443). **Fait** : la sortie est retirée de `serveur_ops`, avec une trace dans le fichier. Registre des flux régénéré (une ligne de moins) ; les flux générés des deux locataires ne changent pas (une sortie n'y figure pas : leurs machines acceptent tout en sortie). **Simulé** (`make frontiere-plan`) : **2 règles périmées à retirer**, une par locataire, et rien d'autre sur 338 règles et 12 routes. **Application à la frontière : décision de l'exploitant** (`make frontiere-appliquer CONFIRMER=true`). **Même vestige, inactif** : `serveur_artefacts` déclare une sortie 3142 vers `voisins_site` (« prendre le cache du site comme amont »). Le rôle n'est déployé chez aucun locataire (seul le laboratoire le porte) : aucune règle n'en sort aujourd'hui. Signalé, non modifié. **Validation** : `make verifier` conforme, 88/88. ## 2026-10-04 (84) — Étape 2 : la frontière, second temps (les entrées publiques) **Le chemin** (`docs/conception-contextes.md` §6), étape 2, frontière, second des trois temps : ce qui entre chez un locataire depuis l'Internet. **Fait** (`scripts/contexte.py`) : - la face réseau porte le **port de son tunnel d'administration**, dérivé de son index par `inventory_rules.port_vpn_locataire()` ; - `verifier_entrees_publiques(site, locataire)` confronte, machine par machine, les entrées que la face ouvre à tous (`0.0.0.0/0`) avec les **règles du WAN** (alias de destination développés en adresses, le supernet en toutes ses machines), les **redirections** (depuis l'adresse que la fiche du site attribue, vers la machine et son port) et la règle du **tunnel** ; - la construction du devis de la frontière est partagée (`_devis_frontiere`) entre les deux temps déjà faits. **Le port public d'un locataire** est son port local : seul `serveur_dns_public`, rôle du site, déclare un `port_public`. La face ne porte donc pas de port public ; la vérification le dirait si une redirection d'un locataire en avait un autre. **Relevé en chemin, non corrigé ici** : `devis_opnsense.py` recalcule le port du tunnel (`52000 + index`) au lieu d'appeler `port_vpn_locataire()`. Une copie de plus de la même formule ; elle disparaîtra quand la frontière lira la face réseau (étape 3). **Éprouvé** : - **P88**, « Ce qui entre depuis l'Internet chez un locataire, la frontière le tient de lui » : aucun écart chez les deux locataires. Comparés chez Technolibre : 19 entrées ouvertes à tous (l'ICMP « fragmentation nécessaire » sur ses 13 machines, et 6 ports publics), 6 redirections, 8 règles WAN dont celle du tunnel (52023). - `test_contexte.py`, 79 contrôles : une entrée publique altérée de **quatre façons** (un port fermé, un port ouvert en trop, l'adresse publique, le port du tunnel) est vue à la frontière. - `make verifier` conforme, **88/88**. Les trois documents comptent 88 preuves. ## 2026-10-04 (83) — Étape 2 : la frontière, premier temps (les identités) **Le chemin** (`docs/conception-contextes.md` §6), étape 2, dernier consommateur : la frontière. Découpée en trois temps avec l'exploitant : les **identités** (ici), les **entrées publiques** (redirections, règles depuis Internet), les **sorties et l'administration**. **Fait** (`scripts/contexte.py`) : - la face réseau porte ses **zones** : un sous-réseau par catégorie de sa nomenclature, dérivé de son index par `sous_reseau_de`, comme la frontière et le SDN le dérivent ; - `verifier_frontiere(site, locataire)` confronte les identités par lesquelles le devis de la frontière désigne le locataire à ce que sa face réseau et la fiche du site portent : le supernet (ses zones), l'administration (que la frontière range par interface, gestion et VPN), le tunnel, **chaque alias de groupe** (8 chez chaque locataire : les groupes qui ont des flux de bordure), une route par zone, et la traduction sortante par l'adresse publique que le site attribue ; - les notes que le devis imprime (« flux `externe` sans `port_public` », « sortie vers un groupe absent de ce site ») sont retenues pendant la vérification : utiles à qui lit le devis, du bruit dans une preuve. Elles serviront au temps suivant. **Éprouvé** : - **P87**, « La frontière désigne chaque locataire par les identités qu'il publie » : aucun écart chez les deux locataires. - `test_contexte.py`, 73 contrôles : une identité altérée de **cinq façons** (zones, administration déclarée, tunnel, membres d'un groupe, adresse publique attribuée) est vue à la frontière. La vérification compare bien les 8 alias de groupe de Technolibre, contrôlé à part avant d'écrire la preuve. - `make verifier` conforme, **87/87**. Les trois documents comptent 87 preuves. ## 2026-10-04 (82) — Étape 2 : les flux de chaque machine, et une information que le locataire jetait **Le chemin** (`docs/conception-contextes.md` §6), étape 2, sens locataire → site : les flux. La face réseau porte désormais les flux de **chaque machine**, ceux que consomme le pare-feu Proxmox. Ceux de la **frontière** (interfaces, alias, traduction d'adresses, flux entre site et locataires) restent à faire. **Fait** (`scripts/contexte.py`) : - la face réseau reprend, pour chaque machine, ce que le locataire a **déjà résolu** pour ses propres pare-feux (`flux-genere/.connectivite.json`) : chaque entrée admise et ses réseaux d'administration ; - `verifier_flux(site, locataire)` compare, machine par machine et port par port, les sources admises par **nftables** (le locataire) et par **Proxmox** (le devis du site). Les deux écrivent la même politique autrement : règle par groupe contre sources réunies, ensembles nommés contre adresses en clair, « Internet » (`0.0.0.0/0` contre « tout sauf RFC 1918 », doublé d'une règle « administration (poste) »), l'administration à part, l'ICMP nommé différemment. Ces différences sont neutralisées, et décrites dans le code. **Ce que la première mesure a trouvé** : 3 écarts par locataire, tous sur `edge-mta-01` (25, 465, 587). Ce n'était pas une divergence de politique, mais une **information perdue** : quand un port est à la fois public et ouvert à des clients internes (`[externe, client_smtp]`), `resoudre_flux.py` écrivait `0.0.0.0/0` et **jetait** la liste des clients nommés. Juste pour l'hôte, qui accepte de partout ; insuffisant pour Proxmox, dont l'Internet exclut les plages privées et qui doit admettre ces clients nommément. Telle quelle, la face réseau n'aurait pas suffi au site. **Corrigé à la source** (décision de l'exploitant) : `resoudre_flux.py` publie, à côté de `0.0.0.0/0`, les `sources_declarees`. Flux régénérés chez les deux locataires : seul `edge-mta-01.connectivite.json` change, **aucun `.nft` ne bouge**. La sonde `connectivite`, qui lit ces fichiers sur les machines, ignore le champ nouveau ; le fichier sera recopié au prochain passage de `nftables_baseline`, sans effet. **Éprouvé** : - **P86**, « Machine par machine, le locataire et Proxmox admettent les mêmes entrées » : aucun écart chez les deux locataires. - `test_contexte.py`, 66 contrôles : des flux altérés de **quatre façons** (une source retirée, un port ajouté, les sources déclarées effacées, l'administration retirée) sont vus en écart. - `make verifier` conforme, **86/86**. Les trois documents comptent 86 preuves. ## 2026-10-04 (81) — Étape 2, seconde moitié (les faits) : la face réseau du locataire **Le chemin** (`docs/conception-contextes.md` §6), étape 2, sens locataire → site. La face réseau se livre en deux temps vérifiés : d'abord les **faits** que le site lit chez le locataire (ici), ensuite ses **flux résolus**, confrontés aux devis de la frontière et de Proxmox (à suivre). **Fait** (`scripts/contexte.py`) : - `locataire.face_reseau()` publie, calculé **chez le locataire** : son index ; ses machines (adresse, groupes, paramètres de matérialisation `proxmox_*`) ; ses groupes ; son administration (l'intrant `nftables_admin_ssh`, son tunnel, ses pairs WireGuard avec leurs clés **publiques**) ; ses zones publiques (zones, signées, adresse et port de son primaire) ; la clé publique de sa sauvegarde. Elle porte l'empreinte de ses six sources. - `verifier_face(site, locataire)` interroge **les consommateurs du site eux-mêmes**, le site monté : la découverte des locataires (`devis_reseau`), le devis du pare-feu Proxmox (la flotte, chaque groupe qu'il utilise, l'administration), la frontière (`admin_de`), et l'inventaire du site (relations du DNS public, comptes de sauvegarde). - `--face ` l'affiche ; `--verifier-faces` confronte chaque couple. **Un faux écart, corrigé avant le commit** : la première comparaison exigeait un ensemble d'adresses par groupe dans le devis Proxmox, et en signalait 19 absents par locataire. Le devis ne garde que les ensembles que ses règles **utilisent** (13 chez chacun) ; la vérification se fait désormais dans ce sens. **Deux lectures fragiles du site, que la face réseau fera disparaître à l'étape 3** : `site_inventaire.py` lit la clé de sauvegarde sous `inventories/principal/` écrit en dur, et re-dérive l'adresse du primaire DNS avec la séquence 1 par défaut (juste aujourd'hui, les primaires sont des `-01`). La face réseau prend l'une dans le dossier d'inventaire réel du locataire, l'autre dans son inventaire. **Ce que la vérification ne couvre pas encore** : la matérialisation est recopiée de l'inventaire, que le clonage lit par Ansible, sans consommateur Python à confronter ; les pairs WireGuard ne sont vérifiés qu'à travers le tunnel qu'ils ouvrent ; les flux arrivent dans le second temps. **Éprouvé** : - **P85**, « La face réseau du locataire dit exactement ce que le site en tire » : 2 faces, aucun écart. - `test_contexte.py`, 60 contrôles : les deux faces réelles concordent, et une face altérée de **huit façons** (index, adresse d'une machine, membres d'un groupe, administration déclarée, tunnel, zones publiques, primaire DNS, clé de sauvegarde) est vue en écart à chaque fois. - `make verifier` conforme, **85/85**. Les trois documents comptent 85 preuves. ## 2026-10-04 (80) — Étape 2, première moitié : la fiche du site, et la preuve qu'elle dit vrai **Le chemin** (`docs/conception-contextes.md` §6), étape 2 : générer les fiches **à côté** de l'existant, et prouver qu'elles disent exactement ce que les lectures croisées produisent. Première moitié : le sens site → locataire. La face réseau du locataire suit. **Fait** (`scripts/contexte.py`) : - `site.fiche_pour(locataire)` réunit ce que trois canaux portaient sans fiche : ce que le site **attribue** (index, adresse publique), ce qu'il **offre** (les intrants de service, calculés par `site_intrants.contrat()`), sa **racine de confiance** (le certificat lui-même), et ce que l'instancier **lisait directement** dans le site (zone et résolveurs à déléguer, serveurs de temps, mode de routage), par les mêmes fonctions qu'aujourd'hui. La fiche porte l'empreinte de ses quatre sources. - `verifier_fiche(site, locataire)` confronte la fiche à ce que le locataire porte : ses copies (la table de `site_intrants`), sa racine, et son inventaire généré (`chrony_serveurs` sur chaque machine, la délégation DNS sur la machine qui résout, le rattachement SDN). - `locataire.domaine_interne()`, lu comme le lit `instancier._domaine_interne`. La première version le cherchait dans `00-instance.yml` ; il est dans `10-intrants.yml`. La comparaison passait quand même, parce que les domaines du site et du locataire diffèrent : relevé et corrigé avant le commit. - `_monter(site, locataire)` : les modules d'avant lisent « le site monté » ; la fiche leur désigne le site voulu le temps de l'appel, puis remet l'environnement, même sur exception. Passerelle de transition, jusqu'à l'étape 3. - `python3 scripts/contexte.py --fiche ` affiche la fiche ; `--verifier-fiches` confronte chaque couple dont le site est l'hébergeur **actif** (un site de reprise ne dépose rien tant que la bascule n'a pas eu lieu). **Rien n'est encore déposé ni consommé** : la fiche est calculée, pas écrite chez le locataire. Le dépôt et la bascule de l'instancier sont l'étape 3, à inventaire identique. **Éprouvé** : - **P84**, « La fiche du site dit exactement ce que chaque locataire porte » : 2 fiches, SITE-Chezlepro → OPS-Chezlepro et → OPS-Technolibre, aucun écart. - `test_contexte.py`, 50 contrôles : les deux fiches réelles concordent, et une fiche altérée de **sept façons** (index, adresse publique, intrant de service, racine, serveurs de temps, délégation DNS, mode de routage) est vue en écart à chaque fois. - `make verifier` conforme, **84/84**. `AGENTS.md`, `devis-services.md` et `responsabilites-locataire-hebergeur.md` comptent désormais 84 preuves. ## 2026-10-04 (79) — Étape 1 : le module `contexte.py`, que rien n'utilise encore **Le chemin** (`docs/conception-contextes.md` §6), premier pas : le tronc commun et les deux classes existent, testées, sans qu'aucun script ne change. **Fait** : - `scripts/contexte.py` : `Ecosysteme` (nom, dépôt, plan, voûte et sa clé, filiation), `Site` et `Locataire`, qui surchargent l'index, la voûte et l'inventaire (un script pour le site, le `hosts.yml` généré pour le locataire). Relations : `site.locataires()`, les absents **nommés** par `locataires_absents()`, et `locataire.site()`, l'hébergeur **actif** que nomme `parente.yml`. Un locataire peut figurer chez deux sites : SITE-Technolibre, site de reprise, déclare les mêmes deux locataires que SITE-Chezlepro. - `contexte.actif()` : le fichier `contexte` à la racine du moteur fait foi (`site:` ou `locataire:`) ; sans lui, les indices d'avant (`SETOPS_*`, liens), et le locataire l'emporte quand les deux sont montés, comme la console d'aujourd'hui. Un contexte nommé qui ne désigne rien est un **refus**, pas un contexte vide. - Le module **réutilise** `voutes.cle_de`, `inventory_rules.dossier_inventaire` et `underlay.allocations` au lieu d'en refaire des copies. - `/contexte` au `.gitignore` : propre à chaque machine, comme `instance/` et `underlay.yml`. **Éprouvé** : `scripts/tests/test_contexte.py`, 40 contrôles, inscrit dans `make test`. Sur une arborescence fabriquée (un site, des locataires, un dépôt ambigu refusé), puis sur les dépôts réels : chaque locataire déclaré par un site porte l'index que ce site lui attribue (17 et 23, chez les deux sites) et nomme un hébergeur qui existe. Les tests savent échouer : le module altéré (le site l'emporte sur le locataire) en fait échouer deux. **Ce que `make verifier` a trouvé, et corrigé avant ce commit** : - **P34 et P48 étaient rouges depuis (78)**, publié sans `make verifier` : la page de conception ne déclarait pas son lecteur, et la carte d'orientation comptait toujours 45 documents. La page déclare son lecteur ; la carte en compte 46 et la range dans le parcours « Le modèle », après `meta-classe.md`, que la page cite désormais (la méta-classe est le mécanisme de la classe `Locataire`). - **P41** refusait la première version du module : il relisait `SETOPS_INSTANCE` lui-même. Il passe par les deux résolutions qui font foi, `inventory_rules.instance_courante()` et `underlay.chemin()`. `python3 scripts/contexte.py` sur le poste dit ce que les indices d'avant désignent : `locataire OPS-Chezlepro ET site SITE-Chezlepro`. C'est l'ambiguïté qu'un sélecteur lèvera. ## 2026-10-04 (78) — Conception : un tronc commun, deux classes (SITE et LOCATAIRE) **Demande de l'exploitant** : en finir avec les « classes à tout faire ». Deux grandes classes, SITE et LOCATAIRE, qui héritent du tronc commun Set-OPS et surchargent ce qui dépend de leur contexte ; des consoles cohérentes avec leur contexte. **Page arrêtée avec lui** : `docs/conception-contextes.md`. Rien n'est encore construit. **Le relevé qui la fonde** : - 33 scripts devinent leur contexte, à partir de cinq indices (liens `instance/` et `underlay.yml`, variables `SETOPS_*`, dossiers frères, `SITE-*`). Trois défauts passés en viennent, dont le `make ci` mélangé du jour. - Le site informe le locataire par **trois canaux**, sans fiche : des copies à la main (douze valeurs, un certificat, une valeur hors contrat), une **lecture directe du site par l'instancier** (mesurée : sans le site monté, l'inventaire de Technolibre perd son serveur de temps, son SDN et sa délégation DNS), et le génome. - Le site **fouille** six fichiers internes du locataire et **recalcule** ses flux avec sa propre version du moteur. **Décisions** : `Ecosysteme` → `Site`, `Locataire` ; un **contrat** en deux fiches générées (la fiche du site pour chaque locataire, **déposée par le site** dans le dépôt du locataire ; la face réseau du locataire, flux déjà résolus compris) ; le poste devient un **sélecteur** ; le contexte actif se nomme dans un fichier `contexte` ; `SITE-Modele` à côté d'`OPS-Modele` ; un `parente.yml` pour le site. On commence par le moteur. **Ce que ça donne en plus** : la portabilité. Un locataire qui change de site reçoit la fiche du nouveau, et son dépôt ne contient plus rien d'interne à l'ancien. ## 2026-10-04 (77) — Release `v2026.10.04` : les deux locataires reconstruits sur `f42d30b` **Pourquoi une seconde fois le même jour.** (72) prouvait `5340220`. Ensuite, (73) et (74) ont touché `client_backup`, et `setops-restaurer` avec lui, l'outil même dont la reconstruction se sert pour remettre l'état. La validation de (76) a trouvé un test que (74) avait cassé. L'exploitant a demandé une nouvelle preuve avant toute release, avec les dernières corrections incluses. | Étape | Technolibre | Chezlepro | |---|---|---| | sauvegarder + raser | 1 min | 1 min | | créer, inséminer, armer | 7 min | 8 min | | monter (déploiement, état remis, `valider`) | 32 min | 32 min | | pare-feu (13/13 VM, 0 critique) | 2 min | 2 min | | **total** | **42 min** | **42 min** | Journaux : 3 151 et 3 155 lignes, aucun `failed`, aucun hôte injoignable, aucun `fatal`. Chez les deux, les 7 jeux d'état sont restaurés. **Relu sur les machines neuves**, en plus de la grille de (72) (versions, épingles, vigie, vues, filtres Alloy, synthèse Grafana, cloud-init absent) : - (74) : `--retry-lock 30m` dans les 4 scripts de `client_backup`, sur les 7 nœuds sauvegardés de chaque locataire ; - (73) : `web-dorsal-01` dit « RIEN A SAUVEGARDER » et « RIEN A RESTAURER » ; ce sont les deux seuls avertissements sur 183 services, et aucun hôte n'est en problème ; - le contrôle de restauration a tourné sur la flotte neuve : sur les 6 nœuds qui ont des données, restauration réelle, contenu recomparé ; - Chezlepro : le mot de passe de `cn=icingaweb2`, tourné plus tôt dans la journée, a survécu à la reconstruction. L'annuaire restauré et la vigie s'accordent, et la liaison LDAPS réussit. **Ce qui n'a pas été exercé** : aucun script n'a eu à attendre un verrou (aucune mention dans les journaux). L'attente reste prouvée par l'essai sur dépôt jetable de (74) et par `test_restauration`. **Étiquette** : `v2026.10.04`, signée par clé SSH comme `v2026.08.21`, posée sur ce commit. Le code prouvé est `f42d30b` ; ce commit n'ajoute que cette entrée. Limite connue, écrite dans l'étiquette : `make ci` (le modèle public) n'est pas conforme ; le modèle sera refait dans son propre dépôt, `OPS-Modele`. ## 2026-10-04 (76) — Validation avant release : un test cassé par (74), trois défauts d'outillage **Contexte** : avant d'étiqueter, `make verifier` et `make ci`. La validation a trouvé une régression **introduite par (74)**, que sa propre validation n'avait pas vue : `--syntax-check` et `ansible-lint` seulement, pas `make test`. **Corrigé** : - `scripts/tests/test_restauration.py` : le test rendait `restaurer.sh.j2` sans `client_backup_attente_verrou` (ajoutée en (74)) et échouait au rendu. Il la fournit désormais, et son faux `restic` **exige** `--retry-lock` avant la sous-commande : l'attente du verrou est couverte, plus seulement tolérée. - `scripts/tests/test_frontiere_refus.py` (depuis le 2026-10-01) : il lisait le WAN sous `interfaces.wan`, une clé que le devis ne produit pas (c'est `if_wan`). Il retombait sur « wan », juste au site par chance. - `scripts/prouver.py` : en mode `--verifier`, le harnais comptait les échecs (« 6 echec ») sans les nommer. Il nomme maintenant chaque preuve en échec, avec son détail. - `Makefile` : sur un clone sans aucune clé de voûte (la CI de la forge), l'export posait `ANSIBLE_VAULT_IDENTITY_LIST=` vide ; Ansible le lisait comme un fichier de mot de passe et refusait (« can not be a directory »). `make ci` mourait à sa première commande. Vide, la variable n'est plus exportée. Sur le poste, la valeur exportée est **identique** (mêmes 5 clés, comparée avant et après). **Pas corrigé ici, et nommé** : `make ci` reste non conforme (77 OK, 6 échecs : P02, P20, P74, P79, P81, P82). Le modèle public `exemples/modeles/socle` date d'avant la séparation site / locataire et ne satisfait pas les gardes ajoutées depuis août. Décision de l'exploitant : le modèle aura son propre dépôt, `OPS-Modele`, comme les locataires. Aucune preuve n'a été assouplie pour le vieux modèle. **Validation** : `make verifier` conforme (83/83), `ansible-lint` (0 échec). La release attend une reconstruction qui prouve (73), (74) et ces corrections. ## 2026-10-04 (75) — Exporter les voûtes une seconde fois ne bute plus sur la première **Signalé par l'exploitant** : `make voutes-exporter VERS=` a refusé (« existe deja. On n'ecrase pas une sauvegarde »). Il a dû retirer l'ancienne archive à la main, puis relancer. **Cause** : le nom par défaut était fixe (`setops-voutes-.tar`, et de même `setops-cles-.tar[.gpg]`). La docstring promettait `setops-voutes--`, mais le code ne datait pas. Les exports précédents n'avaient passé que parce qu'on datait soi-même par `NOM=`. **Fait** (`scripts/exporter_cles.py`, `scripts/exporter_voutes.py`) : - le nom porte la date (`-AAAA-MM-JJ-`), et l'heure en plus au second export du même jour. Le refus de l'écrasement reste, en dernier recours ; - voûtes : si la dernière archive de ce poste sur le support porte exactement ces voûtes (mêmes chemins, mêmes empreintes), **rien n'est écrit** (« A JOUR »). Une copie de plus n'ajoute rien ; - le LISEZ-MOI déposé sur la clé et `docs/sortir-les-cles-du-poste.md` restaurent la **plus récente** archive (`ls -t … | head -1`). Leur `setops-voutes-*.tar`, avec plusieurs archives sur le support, passait plusieurs noms à `tar -xf`. **Éprouvé** sur un support simulé (scratchpad, effacé ensuite) : premier export daté ; second export identique → « A JOUR », rien d'écrit ; une voûte changée (archive précédente altérée) → nouvelle archive horodatée à côté de la première. Le LISEZ-MOI déjà sur la clé garde l'ancienne formule ; `make cles-compagnons VERS=` le remplace sans refaire l'archive. ## 2026-10-04 (74) — Un dépôt verrouillé n'est plus une panne **Constaté en vérifiant (73)** : lancées ensemble sur `web-dorsal-01`, la vérification du dépôt et le contrôle de restauration se sont gênés. Le `restic check` tenait le verrou exclusif, et la sonde a rapporté « AUCUN INSTANTANE lisible » (critique) pendant 20 s. Les minuteurs peuvent faire de même sans personne : `Persistent=true` rattrape au démarrage tout ce qui a été manqué, d'un coup, et le contrôle du dimanche (04:00 + jusqu'à 60 min) peut croiser la vérification de 05:00. restic refuse aussitôt un dépôt verrouillé ; la sonde concluait à l'absence, ou le contrôle à la corruption. **Fait** (`client_backup`) : les quatre scripts qui touchent au dépôt (sauvegarde, vérification, contrôle de restauration, `setops-restaurer`) appellent restic à travers une fonction qui ajoute `--retry-lock {{ client_backup_attente_verrou }}` (30 min par défaut ; le plus long des occupants est le contrôle de restauration). Les unités sont `oneshot`, sans délai d'expiration : l'attente n'est pas coupée. **Éprouvé** sur un dépôt jetable (`/var/tmp`, `web-dorsal-01` de Technolibre, retiré ensuite), le verrou tenu par un vrai processus restic, scripts rendus avec le rapport remplacé par un `echo` : | | sans attente | avec attente | |---|---|---| | contrôle de restauration face à un verrou partagé (`backup` en cours) | critique « DEPOT CORROMPU OU ILLISIBLE », 0 s | attend 17 s, puis vérifie | | vérification du dépôt face à un verrou exclusif (`check` en cours) | critique « AUCUN INSTANTANE lisible », 1 s | attend, puis « OK », 8 s | Deux essais d'abord ne prouvaient rien et ont été écartés : un dépôt vide se vérifie trop vite pour qu'un verrou dure, et `backup --dry-run` n'en pose aucun. **Validation** : `--syntax-check` de `client_backup.yml`, `ansible-lint` (0 violation). ## 2026-10-04 (73) — Un instantané vide dit pourquoi **Demande de l'exploitant** : l'avertissement convient quand il n'y a rien à sauvegarder ; seul le message doit dire lequel des cas on voit. La sonde `sauvegarde` de `web-dorsal-01` écrivait « N'EMPORTE RIEN … Legitime si ce noeud n'a pas encore de donnees — a confirmer » : la machine ne tranchait pas, et laissait l'humain aller voir. **Fait** (`client_backup`) : devant un instantané vide, les deux sondes comptent ce que les chemins sauvegardés contiennent sur le nœud même (la liste de `setops-sauvegarde`). - `sauvegarde` : **rien sur le nœud** → avertissement « RIEN A SAUVEGARDER » ; **des fichiers tous arrivés après l'instantané** → avertissement « DONNEES ARRIVEES DEPUIS » (la prochaine sauvegarde les emportera) ; **des fichiers déjà là quand l'instantané a été pris** → **critique** « N'EMPORTE RIEN … La sauvegarde manque ses donnees ». Ce dernier cas était un avertissement : c'est une vraie panne. - `restauration` : avertissement dans les deux cas, message « RIEN A RESTAURER » ou renvoi à la sonde `sauvegarde`. La sonde côté dépôt du site (`serveur_backup`) garde son message : le site ne voit pas le disque du nœud, il ne peut pas trancher. **Éprouvé** sur `web-dorsal-01` (Chezlepro), scripts rendus avec le rapport à Icinga remplacé par un `echo` : `/srv/webapp` vide → « RIEN A SAUVEGARDER » et « RIEN A RESTAURER » ; un chemin aux fichiers anciens → critique ; un fichier créé après l'instantané → « DONNEES ARRIVEES DEPUIS ». **Validation** : `--syntax-check` de `client_backup.yml`, `ansible-lint` (0 violation). ## 2026-10-04 (72) — Les deux locataires reconstruits : les correctifs de (62) à (71) tiennent **La question de l'exploitant** : une reconstruction ramène-t-elle les problèmes corrigés ces deux derniers jours ? Il a lancé les deux (`make reconstruire-locataire TENANT= CONFIRMER=true ARMER=oui`). **Aucun arrêt, aucun `failed`** dans les deux journaux. | Étape | Technolibre | Chezlepro | |---|---|---| | sauvegarder + raser | 1 min | 1 min | | créer, inséminer, armer | 7 min | 8 min | | monter (déploiement, état remis, `valider`) | 34 min | 31 min | | pare-feu (13/13 VM, 0 critique) | 2 min | 2 min | | **total** | **44 min** | **42 min** | Chez les deux, les 7 jeux d'état ont été restaurés depuis l'instantané pris avant de raser (Nextcloud, PostgreSQL, rspamd, OpenLDAP, courriel, step-ca, web dorsal). **Relu sur les machines neuves** (installées par les runners, sans le cache du poste) : - Keycloak 26.7.5, oauth2-proxy v7.15.5, Nextcloud 34.0.4 (hors maintenance, données revenues) : (67) et (68) ; - les 21 préférences apt posées ; step-cli 0.31.0, icingadb-redis 8.2.10, Grafana 13.2.3, Loki 3.7.8, Alloy 1.20.1 : (65) et (66) ; - cloud-init absent ; - la vigie sur sa base (`config_backend = "db"`), aucune erreur de migrations : (64) ; - Loki en `warn` et les 9 filtres Alloy des sondes : (62) et (63) ; - le tableau de synthèse comme accueil de Grafana : (70) ; - les trois vues métier posées et évaluées par le module : (71). Chezlepro : 25 nœuds racines ; seule « Sûreté des données » est en avertissement. **Icinga après coup** : 183 services chez chacun, aucun hôte en problème. Seuls la `sauvegarde` et la `restauration` de `web-dorsal-01` sont en avertissement : il n'héberge encore aucun site, son instantané est vide, et la sonde le dit. L'exploitant le juge normal. ## 2026-10-04 (71) — Vues métier dans la vigie : une par clientèle interne, dérivées **Demande de l'exploitant** : que le module Business Process d'Icinga offre des vues d'ensemble à différentes clientèles internes. Choisi avec lui : personnel, direction, exploitation ; au site et chez les locataires ; dérivées du plan. Catalogue validé avant d'écrire. **Constat** : le seul processus existant (`supervision`, écrit à la main dans les group_vars des deux locataires) visait un hôte `icinga` et des contrôles (`load`, `procs`, `swap`, `ssh`) qui n'existent plus. Il ne pouvait rien montrer de juste. **Retiré.** **Fait** : - chaque sonde déclare son service métier dans le `meta/supervision.yml` de son rôle (`metier:`, 47 sondes, 38 rôles) ; - `serveur_icingaweb2` porte le catalogue (`serveur_icingaweb2_bpm_vues`, `_services`) : titres, clientèles, regroupement « fondations » et socle par machine ; - `bpm-membres.json.j2` calcule les contrôles de chaque service avec la même dérivation que `serveur_icinga` (hôtes du rôle et de `client_sante`, condition `seulement_si`), plus ce qui ne vient pas des rôles : sauvegardes (les deux modèles, lus chez `client_backup`), `sante` et `ping4` de chaque machine, `materiel` des hyperviseurs, pattes de la frontière ; - `bpm-vue.conf.j2` écrit `setops-.conf`. Une vue sans service n'est pas posée, et « Mes outils » n'existe que là où des personnes se connectent : pas au site. **Trois vues** : **Mes outils** (personnel — se connecter, courriel, fichiers, édition, sites web, sans nom de machine) ; **Services rendus** (direction — un voyant par service, sûreté des données, « Fondations techniques ») ; **Exploitation** (tout, déplié jusqu'au contrôle et à la machine). **Éprouvé** : chaque contrôle nommé comparé à ce qu'Icinga surveille vraiment — site 152, Chezlepro 183 : aucun absent, aucun service laissé hors vue. Trois défauts trouvés ainsi et corrigés : la condition `seulement_si` passée par `| bool` (« frontiere » devenait faux, `journaux-frontiere` disparaissait) ; les sauvegardes écrites « l'un ou l'autre » alors que le site porte les deux modèles ; l'edge classé « sites web publics » (c'est la passerelle des services internes : il fabriquait une vue « Mes outils » au site). Puis chaque vue évaluée par le module (`icingacli businessprocess process check`) au site et chez les deux locataires, sans erreur. Ce qu'elles disent aujourd'hui, et c'est juste : au site, socle en avertissement (gandalf, matériel) ; chez les deux locataires, sûreté des données en avertissement (sauvegarde VIDE de `web-dorsal-01`) ; chez Chezlepro, socle (correctif redis de `edge-mta-01`). **Validation** : `ansible-lint` (0 violation), `make verifier` conforme (83/83). ## 2026-10-04 (70) — Un tableau de synthèse, au site et chez les locataires **Demande de l'exploitant** : la seconde moitié de « rendre les tableaux plus parlants » — un tableau d'accueil. Contenu arrêté avec lui avant d'écrire ; site ET locataires. **Fait** : `tableau-synthese.json.j2` (« Set-OPS — Synthèse », `setops-synthese`), quatre rangées, quatre questions : 1. **Faut-il agir ?** — tuiles colorées : matériel des hyperviseurs (niveau 0/1/2 tiré du même catalogue que le tableau Matériel et la sonde Icinga), swap le plus plein des hyperviseurs, disque le plus plein des machines, correctifs de sécurité en attente, redémarrages en attente, machines muettes ; 2. **Reste-t-il de la marge ?** — mémoire et swap des hyperviseurs, disque et mémoire par machine (de la plus pleine à la moins pleine) ; 3. **Que se passe-t-il ?** — processeur et trafic réseau (interfaces physiques) ; 4. **Où regarder ensuite ?** — liens vers Matériel, Machines, PostgreSQL, Journaux et la vigie. Chaque panneau dit ce qu'il mesure, ses seuils et quoi faire quand sa couleur change. **Un gabarit, deux visages** : au site (`setops_materiel` présent), il ajoute les hyperviseurs ; chez un locataire, il ne voit que ses machines. Le nom des machines vient de `node_uname_info` : les locataires n'ont pas l'étiquette `hote` du site. **Éprouvé avant de poser** : chaque requête exécutée sur le vrai Prometheus, au site et chez Chezlepro. Une erreur trouvée ainsi : la tuile matériel affichait « 3 » — `count(x) > 0` garde la valeur du compte ; elle rend désormais un niveau. Ce que la synthèse dit au premier regard, au site : matériel « à planifier » (disque de gandalf), **swap de vishnu à 98,6 %**, 65 correctifs de sécurité en attente, 9 redémarrages en attente. **Appliqué** au site, puis aux deux locataires (17 et 14 panneaux), sans erreur de chargement. **Page d'accueil** : la synthèse s'affiche à la connexion, au site et chez les deux locataires (`GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH`, posé par le rôle) — la question « faut-il agir ? » plutôt que la liste des tableaux. Relu dans l'environnement du processus Grafana des trois observatoires. **Validation** : `ansible-lint` (0 violation), `make verifier` conforme (83/83). ## 2026-10-04 (69) — Le tableau Matériel dit quoi faire **Demande de l'exploitant** : rendre les tableaux Grafana plus parlants — « phrases et contexte » d'abord, sur le tableau Matériel ; un tableau de synthèse ensuite. **Constat** : les panneaux avaient déjà leur « pourquoi », mais au survol seulement, sans accents (écrits comme des commentaires de code : « CONCU », « annoncee »), et aucun ne disait quoi faire quand il change de couleur. **Fait** : - `scripts/materiel.py` : les 14 textes réécrits en français accentué, et un champ `action` par mesure — quoi faire à l'orange ou au rouge. Seuils et expressions inchangés : la sonde Icinga n'en lit pas les textes. - `tableau-materiel.json.j2` : un mode d'emploi en tête (à quelle question répond chaque rangée, ce que disent les couleurs) ; chaque description composée en trois temps — ce que ça mesure, les seuils, **quoi faire** ; les descriptions écrites à la main (ventilateurs, puissance, usure, secteurs, heures) reprises sur le même patron. **Appliqué au site** (seul à porter ce tableau) : rendu vérifié en local (JSON valide, 28 panneaux), puis servi par Grafana sans erreur de chargement. Le passage a aussi monté Grafana du site de 13.2.1 à 13.2.3, son épingle (66). **Validation** : `ansible-lint` (0 violation), `make verifier` conforme (83/83). ## 2026-10-04 (68) — Forgejo 16.0.5 et Nextcloud 34.0.4 ; Nextcloud apprend à monter, et vérifie sa signature **Demande de l'exploitant** : poursuivre le relevé des logiciels en retard (Forgejo 16.0.2, Nextcloud 34.0.2). La frontière (OPNsense 26.7.4_1 -> 26.7.5), l'exploitant s'en charge par son interface. **Forgejo savait monter** : binaire par version (`forgejo-`), lien vers la courante, redémarrage au changement de lien, signature vérifiée. Seule la SIMULATION d'une montée échouait — le lien vers un binaire pas encore copié. Corrigé (lien sauté en simulation tant que le binaire n'est pas là). 16.0.5 vérifiée contre la clé épinglée (`EB114F5E…`), dépôt du site garni, appliquée au site : API en 16.0.5, la forge sert le génome (`make genome-etat`). **Nextcloud ne savait pas monter** — même défaut que Keycloak (67) : `creates: occ`, le code restait celui de la version installée quelle que soit la version demandée. Et chez nous, `data/` vit DANS le code. **Fait** : `tasks/monter.yml`, la mise à jour manuelle documentée par Nextcloud — maintenance ; ancien code mis de côté sous `-` ; nouvelle version extraite ; on y ramène `data/` (déplacée, pas copiée), la configuration, et tout ce que la livraison n'apporte pas (à la racine, dans `apps/`, dans `themes/`) ; `occ upgrade` ; php-fpm rechargé (cache d'opcodes revalidé toutes les 60 s) ; fin de maintenance. Refus d'une rétrogradation et d'un saut de branche. En cas d'échec, le rôle dit comment revenir. La reprise a été éprouvée sur des arborescences factices avant d'écrire : livré gardé neuf, manquant repris, `config.php` et `data/` intacts. **Nextcloud installait sans vérifier.** Signature PGP vérifiée désormais (`scripts/verifier_signature.py`, comme Keycloak et Forgejo) ; clé dans `files/nextcloud-release.asc`, empreinte `28806A87…937A` (« Nextcloud Security »). Ancrage relevé et sa réserve écrite dans le rôle : clé servie par nextcloud.com, identifiant publié sur nextcloud.com/security, même clé sur keys.openpgp.org mais sans adresse vérifiée — toutes sources de la même organisation. Le dépôt du site sert aussi la signature. **Appliqué chez les deux locataires**, chacun précédé d'une sauvegarde fraîche (fichiers et base) : 34.0.2 -> 34.0.4, repris `alliance-logo.svg`, `apps/richdocuments`, `apps/user_oidc`, `themes/alliance`. Relu chez chacun : `status.php` en 34.0.4 par l'edge, pas de maintenance ni de mise à jour de base en attente, Collabora et OIDC actifs, thème en place, données dans le nouveau code, `bureau` (Collabora) répond. Seule erreur : « Failed to connect to the app store » pendant `occ upgrade` — `collab-01` n'a pas de sortie Internet, par conception. **À décider par l'exploitant** : les anciens codes (`/var/www/nextcloud-34.0.2`, ~950 Mo chacun) restent pour le retour arrière ; les retirer une fois la version éprouvée. **Validation** : `ansible-lint` (0 violation), `make verifier` conforme (83/83), simulations au site et chez les deux locataires. ## 2026-10-03 (67) — Keycloak 26.7.5 et oauth2-proxy v7.15.5 ; deux montées qui n'auraient rien monté **Demande de l'exploitant** : mettre à jour Keycloak et oauth2-proxy, premiers du relevé des logiciels en retard (Keycloak 26.7.1 : quatre correctifs de retard, chacun corrigeant plusieurs CVE ; oauth2-proxy v7.15.3 : deux, tous deux de sécurité). **Deux défauts trouvés en préparant, avant tout déploiement — les deux rôles ne savaient pas MONTER.** Ils ne faisaient jusque-là que des premières installations. - **oauth2-proxy** décidait sur la seule PRÉSENCE du binaire : changer la version sur une machine installée ne faisait rien. Le rôle demande désormais sa version au binaire. - **Keycloak** avait un repère par version (`.kc-built-`), mais l'extraction portait `creates: bin/kc.sh` : sautée, puis `kc.sh build` sur les ANCIENS fichiers, puis repère de la NOUVELLE version — la machine se serait dite en 26.7.5 en restant en 26.7.1. Et extraire par-dessus ne suffit pas : les bibliothèques portent leur version dans leur nom, les deux générations auraient coexisté. Lors d'une montée, le rôle arrête Keycloak, retire la distribution précédente (`data/` gardée), extrait, puis repose sa configuration et son thème comme à l'installation. **oauth2-proxy installait sans rien vérifier.** Empreinte SHA-256 de l'archive écrite dans le rôle (`serveur_oauth2_proxy_sha256`), après relecture : la somme publiée par le projet recalculée sur l'archive. Une archive qui ne la porte pas — dépôt du site ou Internet — est retirée du cache et refusée. Keycloak, lui, vérifiait déjà sa signature PGP : 26.7.5 est signée par la clé épinglée (`861AB50E…`), contrôlé avant déploiement. **Appliqué** : dépôt de binaires du site garni des deux versions (les runners n'auront pas à sortir) ; puis les deux locataires. Relu chez chacun : Keycloak 26.7.5 démarré en 6 s après la mise à jour automatique de sa base, aucune bibliothèque 26.7.1 restante, aucune erreur ; oauth2-proxy v7.15.5 sur `mon-01` et `ops-01` ; vigie et console renvoient vers la connexion Keycloak, issuer juste ; sondes `identite`, `passerelle`, `vigie`, `console-ops` au vert. Interruption de l'identité : environ une minute par locataire, pendant le remplacement. **À savoir** : le rôle Keycloak n'est pas simulable jusqu'au bout — `courriel-realm.yml` échoue en `--check` faute de jeton d'administration, avec ou sans ce changement. L'extraction est sautée en simulation (l'archive n'y est pas copiée). **Vu au passage** : `web-dorsal-01` (Chezlepro) rapporte une sauvegarde VIDE depuis la reconstruction du 2026-10-01 — légitime s'il n'héberge encore aucun site, à confirmer. **Validation** : `ansible-lint` (0 violation), `make verifier` conforme (83/83). ## 2026-10-03 (66) — Les épingles tiennent, cache ou pas **Demande de l'exploitant** : faire respecter les épingles de `paquets-tiers.yml`. **Deux fuites, mesurées sur les 21 paquets épinglés et toutes les machines.** (1) Sans le cache du contrôleur — c'est le cas des runners — les rôles installaient la dernière version publiée (`apt install alloy`, sans version). (2) Le socle (`common_packages`, `upgrade: full`) montait tout au-delà de l'épingle, cache ou pas. Résultat le jour même : sept paquets plus récents que leur épingle chez les deux locataires, et un au site (`icinga-php-thirdparty` 1.0.1, alors que le cache avait posé 1.0.0). **Épingles relevées** sur ce que portent les locataires (en plus des trois de Grafana, 65) : `step-cli` 0.31.0-1, `icingadb-redis` 8.2.10-1, `icinga-php-library` et `icinga-php-thirdparty` 1.0.1-1. Cache retiré, empreintes vérifiées. **Fait — les épingles deviennent des préférences apt** (`paquets_tiers/tasks/epingles.yml`), une par paquet, priorité 990 : apt préfère la version épinglée à toute plus récente, y monte un paquet plus ancien, et ne rétrograde jamais. Éprouvé avant d'écrire : au site, épingle sur 1.20.0-1 alors que 1.20.1-1 existe — candidat 1.20.0-1, `dist-upgrade` s'y arrête ; chez un locataire déjà en 1.20.1-1 — rien ne bouge. Posées par chaque rôle pour ses paquets, et par le socle pour TOUTES, avant son `dist-upgrade`. **Garde** : refus si un paquet est déjà plus récent que son épingle (la relever), ou si l'épingle ne prend pas — version absente de l'index, qu'apt ignore sans un mot (contrôle réservé à l'application réelle : en simulation, la préférence n'est pas écrite). **Deux défauts de `paquets_tiers` révélés par la première vraie mise à jour.** Le rôle ne faisait jusque-là que des premières installations. (1) Sur `site-dns-01`, `alloy` 1.19.2 -> 1.20.1 a demandé quoi faire de `/etc/default/alloy`, modifié par `client_journal` — sans personne pour répondre : paquet déballé, NON configuré (`iU`). Réparé à la main (`dpkg --configure --force-confold`, fichier local gardé), puis corrigé : `apt-get` reçoit désormais `--force-confdef --force-confold`, comme le socle. Éprouvé sur `site-cache-01` (`ii`, port local conservé). (2) La garde `changed_when` cherchait « 0 nouvellement installés », qu'apt écrit aussi lors d'une mise à jour : la montée se rapportait « ok ». Corrigé. **Appliqué au site** (`client_journal`, 9 VM) : `alloy` 1.20.1-1 partout, configuré, préférence posée, aucune perte d'envoi. Les autres épingles se poseront au prochain passage du socle ou des rôles concernés — au site comme chez les locataires, qui n'ont encore rien reçu de ceci. **Vu au passage, non traité** : le socle réinstalle `cloud-init` à chaque passage (« Installer les paquets communs ») et `serveur_durci` le retire derrière — un va-et-vient à chaque déploiement complet. **Validation** : `ansible-lint` (0 violation), `--syntax-check` des sept playbooks touchés, `make verifier` conforme (83/83) ; socle simulé sur `site-dns-01` (20 préférences, garde au vert). ## 2026-10-03 (65) — Épingles Grafana relevées sur ce que portent les locataires **Demande de l'exploitant** : relever les épingles (point 4 du rapport d'anomalies). `make cacher-paquets` avait été relancé, mais il retire ce que `paquets-tiers.yml` épingle : `alloy 1.19.2-1`, `grafana 13.2.1`, `loki 3.7.7`. Les locataires portent `1.20.1-1`, `13.2.3` et `3.7.8` depuis leur reconstruction du 2026-10-01 — leurs runners n'ont pas ce cache et tirent la dernière version publiée. Un déploiement depuis le poste aurait donc tenté de les rétrograder ; il a fallu passer `-e paquets_tiers_cache=/dev/null/aucun`. **Fait** : les trois épingles relevées sur ces versions ; cache retiré (empreintes vérifiées contre l'index du dépôt : `alloy` 3faed6cc381a…, `grafana` 55ec511999f9…, `loki` f6b0dcf22e08…). Le contournement n'est plus nécessaire. Le site, encore en 1.19.2 / 13.2.1 / 3.7.7, montera à son prochain passage — la config Alloy a été éprouvée sur 1.20.1. **La cause reste ouverte** : sans cache, un runner installe la dernière version publiée, sans regarder l'épingle. L'écart se reformera à la prochaine publication de Grafana Labs. ## 2026-10-03 (64) — La vigie a sa base partout ; trois restes du site corrigés **Demande de l'exploitant** : traiter les points 3 et 5 du rapport d'anomalies. **3. Icinga Web sans base chez les locataires.** Depuis leur reconstruction, la vigie de Chezlepro journalisait 145 fois par heure, page ouverte, « Failed to load pending migrations : Please check if a db instance exists at all » ; Technolibre de même dès qu'on la regardait. Le site avait reçu sa base le 2026-09-15, mais seulement en mode `db` ; les locataires, en SSO, étaient restés en `config_backend = "ini"`. Lu dans le code d'Icinga Web : `ConfigMenu::createMigrationBadge` compte les migrations SANS condition de permission — aucun réglage de rôle ne le fait taire. **Fait** : `serveur_icingaweb2` résout et utilise sa base dans TOUS les modes (`config_backend = "db"`, ressource `icingaweb_db`, schéma chargé une fois) ; les comptes en base et le compte d'amorçage restent propres au mode `db`. Base `icingaweb2` ajoutée aux plans de Chezlepro, Technolibre, du lab et des deux modèles qui portent une vigie (`observabilite`, `integral`) ; `vault_bd_icingaweb2` aux gabarits de voûte et à l'exemple public. **Appliqué chez les deux locataires** — secret posé par l'exploitant dans les trois voûtes (relu par l'assistant sans en afficher la valeur : 40 caractères, trois valeurs distinctes), puis `serveur_postgresql`, `serveur_icingaweb2`, `serveur_nginx` et `serveur_ops_tenant`. Relu : vigie en `config_backend = "db"`, schéma chargé (6 tables), plus aucune erreur de migrations depuis le redémarrage de php-fpm (elle revenait toutes les 15 s) ; voûte du runner identique à celle du poste, à l'empreinte près, toujours chiffrée. Les préférences déjà rangées en fichiers ne sont pas reprises : ce ne sont que des réglages d'affichage. **Ce que le premier passage a révélé : `psycopg2` absent du `mon-01` d'un locataire.** Les requêtes du rôle vers la base s'exécutent sur l'hôte de la vigie. Au site, c'est aussi celui de PostgreSQL, qui y avait posé `python3-psycopg2` ; chez un locataire, la vigie est sur `mon-01` et la base sur `data-sql-01`. Échec masqué par `no_log`, et invisible en simulation (la requête y est sautée : la base n'existe pas encore). **Fait** : le rôle pose `python3-psycopg2` avant sa première requête. **5a. Le cache du site donné aux hyperviseurs, qui ne l'atteignent pas.** Mesuré depuis asgard : `10.37.33.21:3142` injoignable. Or `artefacts_amorcage` leur était transmis par `communes` : un passage de `client_journal` aurait réécrit leur dépôt Grafana en `http://` par ce mandataire, et apt l'aurait perdu. **Fait** : `site_inventaire.py` ne leur donne plus ni `artefacts_amorcage`, ni `setops_depot_binaires`, ni `dns_amorcage` (son jumeau, P79) — amorçages d'une VM née du gabarit, qu'aucun rôle des hyperviseurs ne lit. Simulé : un passage de `client_journal` n'y change plus que la config Alloy. **5b. apache2 sur `site-mon-01`.** Tiré par `libapache2-mod-php8.4`, dont plus rien ne dépendait : au site, les paquets d'Icinga viennent du cache du contrôleur et s'installaient AVANT `php8.4-fpm`, et apt prenait la première alternative PHP. Les locataires, en une seule transaction, n'avaient pas ce reste. **Fait** : `php8.4-fpm` et nginx posés avant les paquets tiers ; apache2 retiré seulement si `apt-get -s purge` confirme qu'il part seul. Appliqué : 0 paquet apache, port 80 fermé, vigie en 302. **5c. Grafana Live sans WebSocket.** Chaque page de l'observatoire journalisait `GET /api/live/ws` en 400, au site comme chez les locataires : l'edge relayait sans `Upgrade`. Le mécanisme existait (Collabora) ; Grafana ne le déclarait pas. **Fait** : `websocket: true` sur `grafana` dans les six plans qui le portent (site, deux locataires, lab, deux modèles). Appliqué au site : la poignée WebSocket atteint Grafana (401 sans session, au lieu de 400). **5d. `ssl_protocols` en double.** Le `nginx.conf` de Debian le déclare déjà ; `99-setops.conf` le répétait au même niveau — `duplicate value "TLSv1.3"` à chaque rechargement. **Fait** : la ligne de Debian est neutralisée, comme `server_tokens` avant elle. Appliqué au site : `nginx -t` sans avertissement. **Validation** : `ansible-lint` (0 violation), `make verifier` conforme sur Chezlepro (83/83) et preuves conformes sur Technolibre (82 + 1 sautée, la voûte) ; `voute.py verifier` complet pour les deux. Au site : simulé puis appliqué, 0 échec. **Fuite à signaler** : en lisant la config d'Icinga Web de Chezlepro, l'assistant a affiché en clair le mot de passe de liaison LDAP `cn=icingaweb2` (`bind_pw` n'était pas masqué). Il n'est écrit nulle part ailleurs que dans la conversation ; une rotation est recommandée. ## 2026-10-03 (63) — Les locataires mesurés et corrigés : même bruit, et le courriel en ajoute **Demande de l'exploitant** : mesurer chez les deux locataires les défauts corrigés au site (62), puis y appliquer les correctifs. **Mesuré (une heure, depuis chaque `obs-01`)** — identique chez Chezlepro et Technolibre : sonde SSH 635 lignes/h **par machine** (environ 65 % de son journal) ; sonde TLS 53/h par machine, environ 650 sur `obs-01` et `infra-pki-01` ; audit de `node_exporter` à 31-35 % ; Loki qui se relit (100 à 186 lignes/h) ; aucune unité en échec, donc pas de reste à la façon des hyperviseurs. Environ 500 000 lignes de journal par jour et par locataire. **Ce que le site n'avait pas : le courriel.** Postfix (`edge-mta-01`) écrit trois lignes par connexion de sonde — `connect`, `lost connection after CONNECT`, `disconnect … commands=0/0` — et une quatrième sur le port chiffré (`SSL_accept error`) : 7 000 lignes par heure, la machine la plus bavarde de chaque locataire. Dovecot LMTP (`infra-mail-01`) en ajoute environ 105, dont une sur deux étiquetée `Error`. **Fait** : sept `stage.drop` de plus, même règle qu'en 62 — la phrase exacte, et seulement depuis une machine sondeuse. Une vraie session venue de la flotte ne perd que sa ligne `connect from` (son `client=` et son `disconnect` compté en `commands=N/M` restent) ; un échec TLS LMTP autre que « fermé avant de commencer » passe. **Éprouvé avant d'écrire, deux fois.** Sur une heure de journaux réels de Chezlepro : 69 % de lignes jetées (96 % sur `edge-mta-01`), et le balayeur externe (`censys-scanner.com`) reste visible. Puis avec le binaire Alloy (v1.20.1, sur `edge-mta-01`) : un fichier de 16 lignes témoins à travers le même `loki.process` vers `loki.echo` — les 9 lignes de sonde jetées, les 7 légitimes reçues (source externe, `[preauth]`, certificat refusé, scanner, vraie session SMTP, vrai échec TLS LMTP, témoin de fin). **`make appliquer` accepte `ARGS`**, comme `site-appliquer` : simuler (`--check --diff`) passe par la même porte que l'application. **Trouvé en simulant : le cache de paquets du poste est plus vieux que les locataires.** Ils portent `alloy 1.20.1` et `loki 3.7.8` (tirés à la reconstruction du 2026-10-01) ; le cache du poste fournit `1.19.2` et `3.7.7`. La simulation saute l'`apt-get install` : elle ne montre rien. En réel, `paquets_tiers` tenterait une rétrogradation. Contournement pour ce passage : `-e paquets_tiers_cache=/dev/null/aucun` — le rôle se dégrade comme prévu (aucun manifeste, rien à déposer) et les paquets en place ne bougent pas. Remède de fond : `make cacher-paquets` depuis le poste. **Appliqué chez les deux locataires par l'exploitant** (`serveur_loki`, `client_journal`, `serveur_durci`, avec `-e paquets_tiers_cache=/dev/null/aucun`), puis relu machine par machine : 26 sur 26 portent les 9 filtres sans aucune perte d'envoi, la règle `never` est armée (`node_exporter` : 0 événement en 30 s), Loki est en `warn`, versions inchangées (`alloy 1.20.1`, `loki 3.7.8`). Effet mesuré contre la même fenêtre la veille : journal −77 % (Chezlepro) et −86 % (Technolibre), audit −72 % et −80 %, `edge-mta-01` −96 % et −98 %. **Deux pièges rencontrés en route.** (1) Lancé depuis l'invite de l'assistant (`!`), `ansible-playbook` meurt sur « Ansible requires blocking IO » avant de toucher une machine — les trois premiers déploiements n'avaient rien posé. Parade : `… < /dev/null 2>&1 | cat`. (2) Une commande coupée par un retour à la ligne a lancé `make appliquer` SANS groupe : le Makefile a pris son défaut, `serveur_debian`, et l'a appliqué à tout Technolibre — une mise à jour de sécurité de redis (celle qu'`unattended-upgrades` avait déjà faite ailleurs le matin) et cloud-init réinstallé, neutralisé par son drapeau puis retiré par `serveur_durci`. **Corrigé** : `appliquer`, `deployer-groupe` et `site-appliquer` refusent désormais un `GROUPE` que l'opérateur n'a pas donné (`$(origin GROUPE)` vaut `file` quand il vient du défaut). Leur garde `-z "$(GROUPE)"` ne pouvait jamais se déclencher — le commentaire de `site-appliquer` le disait déjà, sans que la garde ait été corrigée. Éprouvé : sans `GROUPE`, les trois rendent le code 2. **Vu au passage, non traité** : Icinga Web ne trouve pas sa base de configuration chez les deux locataires depuis leur reconstruction (`Please check if a db instance exists at all`, 145/h chez Chezlepro tant qu'une page de la vigie est ouverte). **Validation** : `ansible-lint` (0 violation), `make verifier` conforme (83/83). ## 2026-10-03 (62) — Le site parlait trop dans ses journaux, et trois hyperviseurs échouaient en silence **Demande de l'exploitant** : analyser les journaux centralisés (Loki) et les événements Icinga du site, en faire un rapport d'anomalies, puis corriger quatre points de ce rapport. **1. Un porteur de santé resté sur les trois hyperviseurs.** `client_sante` y avait été posé le 2026-09-10, puis retiré du groupe le jour même : la route par défaut gelée (D-57) empêche un hyperviseur de joindre Icinga, et ses métriques sont depuis TIRÉES. Mais personne ne défaisait ce qui avait été posé. Le minuteur visait encore `10.0.36.11` (l'adresse de `site-mon-01` d'avant la renumérotation) et échouait toutes les 15 min depuis au moins sept jours, soit environ 2 050 échecs par hyperviseur. `node_exporter` publiait donc sur chacun une unité en échec permanente : le jour où une vraie unité tombait, rien ne la distinguait. **Fait** : `client_sante/tasks/retirer.yml` (minuteurs arrêtés, unités, script, mot de passe d'API et AC retirés, `reset-failed`), joué par un 3ᵉ play `hyperviseurs:!client_sante` du playbook de groupe ; `client_sante_unites_posees` nomme ce que le rôle pose. `site_inventaire.py` ne dérive plus `client_sante_icinga_url` pour les hyperviseurs : la valeur faisait croire qu'ils poussaient. Relu : 0 unité `setops-sante*`, 0 unité en échec sur les trois, et aucune unité en échec dans Prometheus. **2. Le bruit de la sonde `connectivite`, filtré à la collecte.** Depuis le 2026-10-01, chaque machine ouvre puis referme chaque minute une connexion vers les ports promis : `sshd` (« Connection closed by… ») et les serveurs Go — Forgejo, step-ca — (« TLS handshake error… EOF ») le notent. Le journal de chaque VM du site est passé de 2 000 à 14 000 lignes par jour, et ces deux phrases faisaient 98 % des « erreurs » de la forge et de l'autorité. Rien à corriger à la source : `sshd` journalise toute connexion fermée avant l'échange de clés. **Fait** : `loki.process "sondes"` dans le gabarit Alloy, avec deux `stage.drop` : la phrase exacte, et seulement depuis une machine de `client_sante` (`client_journal_sondeurs`, dérivé de l'inventaire). La même phrase venue d'ailleurs passe ; les sessions d'administration par rebond (pattes `10.37.x.1` de la frontière) passent, et c'est vérifié. Compte de ce qui est jeté : `loki_process_dropped_lines_total{reason="sonde_connectivite"}`, distinct des pertes que surveille la sonde `journaux`. Éprouvé avant d'écrire : `alloy validate` (v1.19.2) sur la config réelle. Appliqué aux 9 VM seulement — sur les hyperviseurs, le même rôle aurait aussi changé leurs sources apt (Grafana par le cache du site), un écart antérieur laissé à part. **3. Loki se relisait lui-même.** À `info`, Loki écrit quatre lignes par requête, qu'Alloy lui renvoyait : 913 000 lignes en sept jours sur `site-mon-01`, et une recherche par motif retrouvait ses propres requêtes (« segfault » : 1 787 faux positifs). **Fait** : `serveur_loki_log_level: warn`. Relu : 55 lignes du service en dix minutes après le redémarrage, des erreurs seulement ; ce qui reste à `info` vient de Grafana. **4. L'audit enregistrait une lecture d'horloge toutes les 15 s.** Le collecteur `timex` de `node_exporter` appelle `adjtimex` en lecture ; la règle `time-change` ne peut pas distinguer une lecture d'un réglage. 4 événements par minute, 35 % de la piste d'audit d'une VM. **Fait** : une règle `never` étroite, placée DEVANT (seul `adjtimex`, seul ce binaire ; chrony et `settimeofday`/`clock_settime` restent audités). Éprouvé à la main : 4 événements/min sans la règle, 0 avec ; `auditctl` accepte un `exe=` absent. **Trouvé en relisant : redémarrer `auditd` ne recharge pas les règles.** Sur Debian 13, elles sont chargées par `audit-rules.service` (`augenrules --load`), qu'un redémarrage d'`auditd` ne rejoue pas : le handler laissait le fichier juste et le noyau sur les règles du dernier démarrage. Nouveau handler `Recharger les regles auditd`, sans `failed_when: false`. Relu : la règle `never` est armée dans le noyau sur les 9 VM. **Validation** : `--syntax-check` des 4 playbooks, `ansible-lint` (profil production, 0 violation), `make verifier` conforme (83/83, carte à 51 pièces d'audit après le rapport du jour). Au site : `--check --diff` des quatre groupes, puis application, 0 échec. Icinga revient à une seule alerte, l'ancienne : secteurs défaillants sur `gandalf`. Le passage a aussi aligné le site sur des modes déjà décidés par le dépôt (`/etc/setops` 0700, `/var/lib/setops` 0755). **Reste du rapport, non traité ici** : aucune notification Icinga n'a jamais été émise au site (0 objet `Notification` : les règles d'exemple exigent `host.vars.notification.mail`, que rien ne pose) ; disque `sda` de `gandalf` (osd.4) qui se dégrade ; lien SATA instable du SSD système de `vishnu` (CRC 16), que smartmon ne remonte pas. ## 2026-10-01 (61) — Une donnée restaurée l'emportait sur une règle du plan (Nextcloud) **Constat de l'exploitant** : dans Nextcloud, `sysadmin` n'a pas les mêmes droits chez les deux locataires. Mesuré : administrateur (groupe `admin`) chez Technolibre, pas chez Chezlepro — reconstruite en 40 min juste avant. **Ce qui devait être identique et ne l'était pas.** Les DONNÉES d'un locataire peuvent différer d'un autre : chacune revient de son propre passé. Les RÈGLES du plan, non : « le groupe d'habilitation est administrateur de Nextcloud » vaut pour tous. Or son effet est rangé dans la base de Nextcloud, et le rôle l'applique AVANT de remettre la base restaurée (la remise vient en fin de rôle, à cause du code des applications — 47) : sur la base neuve, le groupe n'existe pas encore, l'étape passe sans rien faire, puis la base d'avant revient avec son état d'avant. Technolibre avait reçu `admin` d'un déploiement antérieur, que la sauvegarde a gardé ; Chezlepro jamais. La donnée décidait à la place de la règle. **Correction** : l'étape sort dans `admin.yml`, appelée comme avant depuis `oidc.yml`, et REJOUÉE juste après la remise d'un Nextcloud restauré. Les autres services n'ont pas ce défaut : Keycloak est remis avant son premier démarrage et reconfiguré ensuite ; l'annuaire réaligne les comptes de service sur la voûte après sa remise. ## 2026-10-01 (60) — La frontière refuse à voix haute à l'intérieur, et se tait sur l'Internet **Demande de l'exploitant** : « pour éviter des délais internes en cas de pépin, les règles qui ne font pas face à l'extérieur ne doivent pas être en drop mais en reject ». Les machines refusaient déjà en le disant (nftables `reject … admin-prohibited`, Proxmox `REJECT`). La FRONTIÈRE jetait en silence sur toutes ses interfaces — c'est elle qu'avait mesurée la sonde au site (« délai » entre deux zones). **Fait** : un `reject` final, journalisé, séquence 950 (après les autorisations, WireGuard et les silences déclarés), sur chacune des 12 interfaces internes — gestion, transit, l'ancienne patte du site, les 8 zones du site, la grappe de contrôle, WireGuard. **Le WAN reste muet.** Sûr : la frontière n'a qu'une règle héritée, sur le WAN — rien d'existant n'est court-circuité ; ce qui n'était pas autorisé était déjà refusé, seul le MODE du refus change. Appliqué (sur accord) : 12 créations, 0 retrait ; plan ensuite vide (340 règles). Après coup : les 35 machines `connectivite` au vert (9 site, 13 + 13 locataires). Essai négatif : deux flux NON déclarés — `site-edge-01 → site-forge-01:3000` (« délai » la veille) et Technolibre `web-frontal-01 → site-forge-01:3000` — échouent en `ECONNREFUSED` en moins d'une milliseconde ; le flux déclaré témoin (443) passe. **Garde** : P53 affinée (refus audible interne exigé, jamais sur le WAN) ; P50 ne compte comme silence que les `block` ; `test_frontiere_refus.py` sur le vrai devis. **La sonde** classe désormais `ECONNREFUSED` en AVERTISSEMENT (« service absent, ou refus de la frontière ») : le `reject` d'OPNsense rend un RST, indiscernable d'un port fermé — et depuis les `seulement_si`, aucun flux déclaré ne mène plus à un port sans service, ce cas n'est donc plus jamais normal. ## 2026-10-01 (59) — Chezlepro reconstruite par Set-OPS en 41 minutes, sans un arrêt Lancée par l'exploitant (`make reconstruire-locataire TENANT=OPS-Chezlepro CONFIRMER=true ARMER=oui`) : 8 étapes sur 8, **aucun arrêt, 41 min**, contre 60 la veille et 43 visées. | Étape | Durée | |---|---| | sauvegarder + raser | 1 min | | créer les VM | 5 min | | inséminer + armer | 2 min | | monter (déploiement, état remis, `valider`) | 31 min | | pare-feu (sonde `connectivite`) | **2 min** (20 la veille) | | bilan | < 1 min | Les 7 jeux d'état restaurés depuis l'instantané pris avant de raser ; Icinga à 0 critique ; l'archive Nextcloud prise au dépôt de binaires du site, plus sur Internet (58). Les deux corrections du jour se lisent dans ce chiffre : la sonde à la minute (−18 min), le dépôt du site rouvert (−20 min sur Technolibre, 63 min une heure plus tôt). ## 2026-10-01 (58) — Le dépôt de binaires du site rendait 403 : quatre rôles se disputaient un répertoire **Le symptôme** (reconstruction de Technolibre lancée par l'exploitant, 63 min au lieu de ~43) : `deployer-tout` restait 20 min sur « Télécharger le tarball Nextcloud dans le cache du contrôleur » — 281 Mo depuis download.nextcloud.com à 130–230 Ko/s. Le runner, né à vide, devait les prendre au dépôt de binaires du SITE ; l'étape qui l'y cherche (`failed_when: false`, « le dépôt est une commodité ») avait rendu « ok » sur un **403**. **La cause** : le dépôt existe et est complet depuis le 2026-09-12 (Forgejo, Keycloak, Nextcloud 34.0.2, oauth2-proxy), `LocalDirs` est en place — mais apt-cacher-ng, qui ne tourne pas en root, ne traversait plus `/var/lib/setops` (0750). QUATRE rôles tiennent ce répertoire : `common_packages`, `client_journal`, `serveur_ops_site` en 0750, et `serveur_artefacts` en 0755. Le dernier déployé gagnait ; mes déploiements au site du matin l'avaient refermé (06:16). Le piège était documenté dans `serveur_artefacts` — pas chez les trois autres. **Correction** : 0755 partout (le répertoire ne tient que des états de sondes, déjà lisibles) ; appliqué à `site-cache-01` : les quatre archives répondent 200, à la taille exacte, depuis une VM de locataire. **La garde, `test_repertoires_partages.py`** : un répertoire dont plusieurs rôles imposent le mode n'en a qu'un — chemins Jinja RÉSOLUS depuis les défauts (celui de `serveur_artefacts` s'écrit `{{ … | dirname }}`, une lecture littérale ne l'aurait pas vu). À sa première exécution, elle a trouvé deux autres désaccords : - `/etc/setops` : 0700 (`client_backup`, `serveur_backup`) contre 0755 (`client_sante`, `serveur_icinga`). Il tient des secrets, tous ses lecteurs tournent en root : **0700**. - `/srv/restic` sur `site-backup-01` : `serveur_backup` y est appliqué deux fois — en dépendance de `serveur_backup_site` (racine `/srv/restic/site`) et par son propre groupe (défaut `/srv/restic`, 0700 au compte `restic`). Joué seul, ce second passage refermait le parent des dépôts de tous les locataires (sshd `StrictModes`, mesuré le 2026-09-01). Dormant — un déploiement complet du site rejoue `serveur_backup_site` après. L'inventaire du site donne désormais au groupe la racine de la dépendance, LUE dans son `meta/main.yml`. Le test admet ce cas : une dépendance qui surcharge le chemin n'est pas un désaccord. ## 2026-10-01 (57) — Aucune exception : la soumission n'est ouverte que là où elle existe J'avais présenté `site-mon-01:465/587` (« sans service ») comme une exception tolérable. L'exploitant l'a refusée : « je ne comprends pas pourquoi on tolérerait cette exception » — et a demandé si c'était le relais qui avait une lacune. Il n'en a pas : le relais du site fait sortir le courriel des MACHINES par le 25 ; la soumission (465/587) est la porte des PERSONNES, authentifiée par l'annuaire — que le site n'a pas, par conception. Le défaut était dans le registre : ces deux flux, ajoutés le 2026-09-29, avaient été déclarés pour toute instance de Postfix, sans suivre l'interrupteur qui active le service (`serveur_postfix_submission_actif`, faux par défaut, levé par les seuls locataires). Ils le suivent désormais (`seulement_si`). Site : `site-mon-01` perd ses deux règles ; les 9 machines, sonde lancée directement après le déploiement (simulé d'abord) : **tous les flux déclarés ouverts, aucun « sans service », aucune entrée hors des règles**. Locataires et frontière : inchangés (`edge-mta-01` garde ses 465/587, la frontière ses redirections publiques). Les trois niveaux de pare-feu n'ouvrent plus aucun port où rien n'écoute — sans exception. ## 2026-10-01 (56) — Le registre des flux sait dire « seulement si » ; la sonde au site **Le registre ne savait pas exprimer une condition.** La sonde `connectivite` montrait, chaque minute, des ports ouverts vers rien : la vigie (8080) et la console (8090) en SSO, liées à `127.0.0.1` derrière la passerelle — le code le savait (« la règle devient simplement sans objet »). `seulement_si: {variable, egal}` ou `{variable, non_vide: true}`, évalué PAR ÉCOSYSTÈME (variables d'hôte, `group_vars`, défauts du rôle) dans les trois générateurs — nftables, Proxmox, frontière. Une expression Jinja indéterminable garde le statu quo ; une variable absente partout compte comme vide pour `non_vide`. | Flux | Condition | Effet | |---|---|---| | vigie 8080 | `serveur_icingaweb2_nginx_bind == ""` | retiré chez les locataires (SSO) ; gardé au site | | console 8090 | `serveur_ops_gui_auth == locale` | idem | | forge 3000 | `serveur_forgejo_tls == false` | retiré au site (TLS propre sur 443) | | AXFR 5300 | `dns_public_site` non vide | retiré au site (pas d'instance publique) ; gardé chez les locataires | Appliqué : locataires — nftables, et Proxmox (8 règles, 2 groupes de sécurité vides retirés), `connectivite` 13/13 au vert des deux côtés, l'edge à 27/27 ; site — nftables, ses deux « coupures » (`site-dnspub-01 → site-dns-01:5300`, `site-edge-01 → site-forge-01:3000`) disparues avec leurs règles. Frontière, sur accord de l'exploitant : 8 règles d'administration vers 8080/8090 et 2 alias orphelins retirés ; plan ensuite à 0 création, 0 retrait. Après coup : `connectivite` 13/13 au vert chez les deux locataires, et la vigie comme la console répondent toujours par l'edge (302 vers la connexion SSO). **Un défaut latent trouvé au passage** : la console du SITE tourne en `locale` depuis le 2026-09-15, mais son plan ne le disait pas — et le défaut du rôle est `oidc`. Le prochain déploiement du site l'aurait liée à `127.0.0.1` derrière une passerelle inexistante. Le plan le déclare désormais (`serveur_ops_gui_auth: locale`). **La sonde au site** (simulée d'abord, `--check --diff`) : la simulation a montré que le chemin de la liste pointait hors du dépôt (inventaire dynamique : même surcharge que le ruleset), que la tâche aurait changé les droits de `/etc/setops` (la liste, qui n'a rien de secret, va dans `/usr/local/lib/setops/`), et que l'activation du minuteur échouait en simulation. Corrigés, puis déployés : les 9 machines rapportent ; reste une information — `site-mon-01:465/587`, le relais du site n'offre pas la soumission. ## 2026-09-30 (55) — Première reconstruction sans un seul arrêt ; le pare-feu jugé par une sonde à la minute **Chezlepro, quatrième reconstruction, lancée par l'exploitant** (`reconstruire-locataire`, détachée) : 8 étapes sur 8, **aucun arrêt**, 60 min, les 7 jeux d'état restaurés. Première reconstruction de bout en bout sans aucune intervention. Sur ces 60 min, 20 pour l'étape `parefeu` — « vraiment trop de temps », dit l'exploitant, qui propose des sondes de connectivité rapportant à Icinga chaque minute. **La sonde `connectivite`** (déclarée par `serveur_durci`, déposée par `nftables_baseline`) : chaque VM teste chaque minute les flux SORTANTS que le registre lui promet. La liste est écrite par `make flux` à côté de chaque `.nft`, depuis les MÊMES règles résolues (`flux-genere/.connectivite.json`). La cause est mesurée, pas supposée (2026-09-30, depuis `web-frontal-01`) : rejet Proxmox = `EHOSTUNREACH` → COUPÉ ; `drop` nftables = délai → COUPÉ ; port autorisé où rien n'écoute = `ECONNREFUSED` → information (pas une coupure). Elle signale aussi les connexions entrantes établies que les règles ne couvrent pas. Contrôle négatif : une liste imposée (flux bloqué, port fermé, flux ouvert) → COUPÉ, code 2, chaque cause nommée. **Le porteur à la minute** : `client_sante` joue `sondes-minute/` chaque minute (`ttl` 3 min), hors du répertoire au quart d'heure — sinon le `ttl` de 90 min masquerait le silence. Même script, mode `minute`. Icinga accepte une `fraicheur:` par sonde ; P64 reconnaît `sondes-minute/`. **L'étape `parefeu`** (`eprouver_parefeu.py --flotte --sondes`, utilisée par `reconstruire-locataire` et `parefeu-*-flotte`) : refus si un flux est DÉJÀ coupé, activation des 13 VM d'un coup, attente des rapports postérieurs, seul ce qui change compte. Mesuré : **Technolibre 135 s, Chezlepro 94 s** (au lieu de ~20 min), 0 critique apparu. La matrice VM par VM reste pour le diagnostic (`--hote`). **Deux défauts de la sonde vus au premier déploiement, corrigés** : une machine qui se parle par sa propre adresse (Loki sur `obs-01`) se déclarait « hors des règles » ; deux flux déclarés sans service (`infra-edge-01 → mon-01:8080`, `→ ops-01:8090` : interfaces liées en local derrière la passerelle SSO) étaient un avertissement permanent — ce sont désormais des informations nommées. **Reste** : retirer ces deux déclarations obsolètes du registre ; générer et déployer la sonde au SITE (ses listes ne sont pas encore générées — un `serveur_icinga` du site redéployé l'attendrait en rouge). ## 2026-09-30 (54) — Chezlepro au bout ; Technolibre arrêtée par un verrou dpkg **Chezlepro** (lancée par l'exploitant, reprise `DEPUIS=parefeu` après (53)) : pare-feu 13/13 sans flux perdu, les 7 jeux d'état **restaurés** depuis les instantanés de 15:59. Silence de douze minutes pendant `parefeu` : la sortie du Python enfant restait dans son tampon (tuyau) — les sous-commandes écrivent désormais sans tampon. **Technolibre** (lancée par l'exploitant) : arrêt à `monter`, `mon-01` en échec dans `common_packages` — `apt-get dist-upgrade` refusé, verrou dpkg tenu par `unattended-upgrades`, né avec la VM. La tâche portait pourtant `lock_timeout: 300` : il ne protège que la vérification du MODULE ; `apt-get`, appelé ensuite, n'attend pas — APT 3.0 ne donne d'attente qu'à la commande `apt` (`Binary::apt::DPkg::Lock::Timeout "120"`). Le verrou a été repris entre les deux. **Correction** : `DPkg::Lock::Timeout` posé pour tout appel d'APT (`/etc/apt/apt.conf.d/10setops-verrou`), en toute première tâche du rôle. Rien à voir avec la restauration : un tirage au sort du premier démarrage, que la troisième reconstruction de Chezlepro n'avait pas tiré. **Reprise `DEPUIS=monter`, second arrêt** : `collab-01` en échec — mais la tâche avait été tuée SUR L'AC (`Killed`, rc=137). `client_pki : Dériver l'empreinte du root CA` était déléguée à `infra-pki-01` pour chacun des 13 hôtes, en parallèle : treize modules Python sur une machine de 765 Mo et 1 vCPU. Le noyau (OOM) a tué le module de `collab-01`, et Alloy au passage. L'empreinte est la même pour toute la flotte : `run_once`. Éprouvé depuis le runner de Technolibre : **1** délégation au lieu de 13, `client_pki` sans échec, AC saine. ## 2026-09-30 (53) — Troisième reconstruction (Chezlepro, lancée par l'exploitant) : arrêtée au pare-feu L'exploitant a lancé lui-même `make reconstruire-locataire TENANT=OPS-Chezlepro CONFIRMER=true`. `sauvegarder` → `raser` → `creer` → `inseminer` → `armer` → `monter` : sans intervention. Arrêt à `parefeu` (code 4), deux fois : Icinga portait des critiques `sauvegarde` puis `restauration` sur les sept détenteurs d'état. **Le défaut était dans (49).** Le premier rapport à un Icinga neuf se déclenchait sur le changement de `icinga-ca.crt` — or `client_sante` dépose le MÊME fichier, plus tôt dans le déploiement : sur une flotte neuve, il était déjà là, « ok », et rien ne partait. L'épreuve de (49) avait joué `client_backup` seul, fichier retiré : elle ne pouvait pas le voir. `sauvegarde` est passé au vert à 17:01 par son minuteur de 4 h ; `restauration` aurait attendu le dimanche. **Correction** : `client_backup` retient lui-même l'empreinte de l'Icinga qui l'a entendu, dans un marqueur à lui, écrit APRÈS le rapport. Éprouvé sur le vrai cas (les nœuds neufs de Chezlepro) : premier rapport sur les 7, Icinga à 0 critique ; second passage sans aucun gestionnaire. **Et la procédure du pare-feu s'arrêtait sur des critiques qu'elle n'avait pas causés** : elle relève désormais les critiques AVANT l'activation, et seul ce qui apparaît après compte. `collab-01`, activé deux fois pendant ces arrêts, n'a perdu aucun flux (16/16). Reprise : `make reconstruire-locataire TENANT=OPS-Chezlepro CONFIRMER=true DEPUIS=parefeu`. ## 2026-09-30 (52) — `make reconstruire-locataire` : une commande, du site à la recette Les reconstructions du jour passaient par cinq endroits — poste, runner du site, poste, runner du locataire, poste — avec des commandes SSH tapées à la main. La séquence vit désormais dans `scripts/reconstruire_locataire.py`, lancé depuis le POSTE (seul à tenir à la fois la voûte du locataire et l'accès au runner du site) : `sauvegarder` → `raser` → `creer` → `inseminer` (runner du site) → `armer` (poste ; pause, sauf `ARMER=oui`) → `monter` (`make monter-flotte` sur le runner du locataire, suivi toutes les 30 s) → `parefeu` (`--flotte`) → `bilan` (ce que chaque jeu d'état est devenu). `TENANT=` désigne l'écosystème en toutes lettres ; le nom qu'exige `raser` en est DÉRIVÉ (`raser.nom_court`, une seule dérivation), pas redemandé. La première version exigeait aussi `INSTANCE=` — relevé par l'exploitant : « pourquoi cette cible a besoin de se faire dire "chezlepro" deux fois ? ». Le garde-fou de `raser` n'a de sens que pour `raser` seul, qui rase l'écosystème du lien `instance/` ; ici, rien d'implicite n'est à confirmer. `raser` n'a pas d'autre condition que sa confirmation. Arrêt à la première étape en échec, avec la commande de reprise (`DEPUIS=<étape>`) ; journal complet dans `/logs/`. **Le runner du site se met à jour avant sa première étape** (quelle que soit la reprise) : il tire le moteur, le plan du locataire et SON dépôt de site — déduit du lien `underlay.yml`, pas nommé —, en avance rapide seulement, et refuse devant une modification locale (éprouvé : une ligne ajoutée au README du site → REFUS, rc=1 ; fichier remis). Avant, seul `raser` tirait, deux dépôts sur trois, et une reprise `DEPUIS=creer` ne tirait rien. **Éprouvé sur Technolibre de `armer` à `bilan`** (les étapes non destructives) : 26 min, 0 échec — armement, flotte montée et recettée, pare-feu 13/13 sans flux perdu, bilan. Un défaut de forme corrigé : chaque relevé de suivi recopiait tout le journal distant ; seul le nouveau y va désormais. Reste l'épreuve entière, destruction comprise. ## 2026-09-30 (51) — Ce qui était fait à la main dans les reconstructions entre dans le code **Le bilan de l'exploitant** : aucune des deux reconstructions du jour n'est allée au bout seule. Au-delà des défauts corrigés, trois gestes vivaient hors du dépôt : 1. **La séquence du runner du locataire** était un script écrit dans `/tmp` du runner. Elle devient `make monter-flotte CONFIRMER=true` : `flux`, AC et DNS (`_amorcer-socle`), `deployer-tout`, `valider` — étapes horodatées, arrêt à la première en échec. `reconstruire` s'appuie désormais dessus (et gagne `valider`, qu'il n'avait pas). 2. **La réactivation du pare-feu Proxmox** était une boucle tapée à la main. `eprouver_parefeu.py --flotte` porte la procédure pour toutes les VM, **une à une, le runner en dernier, arrêt au premier refus** — la règle qui la rend sûre vit avec elle. Cibles `parefeu-verifier-flotte` (mesure) et `parefeu-activer-flotte` (depuis le poste : lui seul tient à la fois les accès du locataire et le runner du site). 3. **La sauvegarde fraîche avant de raser** reste une ÉTAPE du runbook `flotte-refaire`, pas une condition de `raser` : décision de l'exploitant, « une confirmation suffit ». **`--flotte` a trouvé un défaut dès sa première passe** (lecture seule, Technolibre) : il s'est arrêté sur `infra-edge-01`, un flux `10.37.0.17 → 443` « hors des règles ». C'était le navigateur de l'exploitant, depuis la zone d'administration du site — légitime, et effectivement admis par Proxmox. La matrice ne retenait des IPSet que les membres qui sont des MACHINES ; les RÉSEAUX (`t23-admin` : `10.37.0.0/24`…) étaient ignorés, et l'observateur ne connaissait l'administration que sur le port 22. Les réseaux de chaque règle (exclusions `!` comprises) comptent désormais sur tous les ports. Seconde passe : **13/13, runner en dernier, aucun flux perdu**. Ce faux refus aurait bloqué une reconstruction le jour où l'exploitant a un onglet ouvert sur ses services — c'est-à-dire presque toujours. ## 2026-09-30 (50) — Le web frontal n'est plus un détenteur d'état Le frontal RELAIE : ses vhosts, son WAF et sa page 404 se redéploient depuis le dépôt. Le catalogue de sauvegarde lui gardait `/srv/web`, venu de l'époque où il servait du statique — ses instantanés étaient vides, et `valider` le disait « À CONFIRMER » à chaque passage. Retiré du catalogue et de la liste en clair : Icinga, le témoin de dépôt du site et P36 le suivent du même geste (P36 : 82 OK, 0 échec). Le frontal ne restaure plus rien. **Ce que ça a révélé** : `client_backup`, devant un nœud qui n'a plus rien à sauvegarder, retirait la sauvegarde mais pas les VÉRIFICATIONS du dépôt et de la restauration — elles auraient rapporté, toutes les quatre heures, à des services qu'Icinga ne déclare plus. Elles partent désormais avec elle. L'intégration `client_backup` quitte ensuite le plan des frontaux de Chezlepro et de Technolibre. ## 2026-09-30 (49) — Un Icinga neuf entend les nœuds tout de suite (signal corrigé en (53)) **Le défaut** (45, point 4) : un Icinga neuf marque `sauvegarde` et `restauration` CRITIQUES tant qu'aucun rapport n'est arrivé — voulu : le silence doit alerter. Mais les minuteurs ne rapportent que toutes les 4 h (dépôt) et le DIMANCHE (restauration) : après chaque reconstruction, Icinga restait rouge jusqu'à une semaine, et on déclenchait les contrôles à la main (Technolibre, puis Chezlepro, ce 2026-09-30). **Le signal retenu** : le compte de rapport d'un nœud ne sait que DÉPOSER, il ne peut pas demander à Icinga s'il l'a déjà entendu. Mais chaque nœud recopie l'AC d'Icinga : un Icinga refait a une AC neuve, un nœud refait n'en a pas encore de copie. Dans les deux cas, cette copie change — et elle seule notifie « Premier rapport a Icinga » : déposer, vérifier le dépôt, vérifier la restauration, dans cet ordre. Une retouche de gabarit ne le déclenche pas : elle ne dit rien de ce qu'Icinga a reçu. **Éprouvé** sur `web-frontal-01` de Technolibre, copie de l'AC retirée (Icinga « neuf ») : les trois unités tournent (04:39:37, :40, :42), rc=0 — donc acceptées par Icinga, le script échouant sinon. Second passage : aucun gestionnaire, `changed=0`. Garde statique ajoutée à `test_restauration.py`. ## 2026-09-30 (48) — Chezlepro rasée et reconstruite par les runners : l'état est revenu, à l'identique **La preuve visée** : que (47) remette réellement l'état, sur une vraie reconstruction. **Déroulé** : `make sauvegarder-maintenant` (8 instantanés à 03:06) ; empreintes témoins relevées ; depuis le runner du site `raser` (13/13), `flotte-creer`, `inseminer` ; armement depuis le poste (voûte, clé de voûte, identité SSH du runner reprise de la voûte) ; sur le runner de Chezlepro : `_amorcer-socle`, `deployer-tout`, `valider` — **0 échec**. **Chaque jeu a choisi l'instantané de 03:06**, le dernier avant la naissance des machines (03:12), et l'a remis : | Témoin | Avant | Après | |---|---|---| | racine de l'AC | `F0:32:89:A0:BB:98:79:AC` | identique — la flotte s'est enrôlée auprès d'elle | | `sysadmin` | empreinte `4406f2fa…`, changé le 2026-09-16, pas de changement forcé | identique | | annuaire | 12 entrées | 12 | | Nextcloud | 217 fichiers, `instanceid` `ocytcfy7ss5a` | identique | | clé DKIM | `760208b7…` | identique — l'enregistrement publié reste valable | | Keycloak | 100 tables, 2 utilisateurs | identique | | Nextcloud (base) | 139 tables | 139 | | boîtes | `root` | `root` (+ `testmail`, écrit par `valider`) | **Un défaut trouvé, dans le code d'hier soir** : la mesure « l'annuaire est-il vierge ? » filtrait par `grep -v` — sur un annuaire VRAIMENT vierge, plus aucune ligne, `grep` sort en 1, `pipefail` fait échouer la tâche. Technolibre, déjà peuplée, ne pouvait pas l'exercer. Comptage par `awk`, puis reprise à `deployer-tout` : l'AC et les bases, déjà remises, ont été reconnues « actées » et n'ont pas été rejouées. **Après** : les 8 dépôts passent la garde ; contrôles de dépôt et de restauration rapportés à Icinga ; recette — fédération LDAP authentifiée, passerelle vers Keycloak, vigie, Nextcloud, Dovecot ; `essai.chezlepro.ca` sur `.61` → 200, page absente → la 404 du PSPBT. Pare-feu Proxmox réactivé VM par VM : 13/13, flux identiques avant et après, Icinga à 0 critique. **Ce que ça prouve, et ce que ça ne prouve pas.** Une reconstruction complète d'un écosystème, conduite par les runners, **rend l'état qu'on lui a confié** — la limite qu'énonçait honnêtement (46). Le SITE, lui, n'a toujours jamais été reconstruit ; et chaque reconstruction a encore trouvé un défaut (ici un seul, dans le code neuf). ## 2026-09-30 (47) — La reconstruction remet l'état : chaque rôle propriétaire restaure le sien **Le défaut** (46) : une reconstruction repartait d'un état neuf. Les instantanés se restauraient pour PROUVER qu'ils s'ouvrent, jamais pour être remis en service. **Le principe retenu** : pas d'étape à part dans la chaîne — chaque rôle qui POSSÈDE un état le remet **au moment où il le créerait neuf**, et l'ordre vient des couches. La chaîne des runners (`_amorcer-socle` → `deployer-tout`) et `make reconstruire` n'ont pas changé. | Rôle | Point de remise | |---|---| | `serveur_step_ca` | avant `step ca init` ; refus si la voûte n'ouvre plus les clés restaurées | | `serveur_postgresql` | bases juste créées, chacune rejouée en UNE transaction | | `serveur_openldap` | fin de rôle (le LDIF exige `ppolicy`), puis comptes de service réalignés sur la voûte | | `serveur_nextcloud` | fin de rôle : base + fichiers + config ensemble, `occ upgrade` | | `serveur_rspamd` | avant la génération DKIM — sinon chaque reconstruction changeait la clé publiée | | `serveur_dovecot`, `serveur_forgejo`, `serveur_web_*` | racine encore vide | **Nextcloud se remet en fin de rôle, pas avant l'installation** : le code de `user_oidc` et `richdocuments` n'est pas sauvegardé ; restaurée avant, la base les dirait installés et `occ app:install` refuserait. `serveur_postgresql` exclut donc sa base (et celle d'Icinga, dont l'historique est lié à un environnement non sauvegardé). **Quelle incarnation** : le dernier instantané pris AVANT la naissance de la machine — la date de sa clé d'hôte SSH (`machine-id`, lui, vient du gabarit : tous les nœuds portent le 2026-09-01). Un état en place n'est jamais écrasé d'office ; ce qui est remplacé est mis de côté (`/var/backups/setops-avant-restauration/`) ; chaque jeu laisse un marqueur. **La sauvegarde refuse de déposer tant qu'un état d'avant attend** (code 3). Sans cette garde, `restic forget --keep-daily 7` aurait gardé l'instantané de l'état NEUF et chassé celui d'avant dès qu'ils tombaient le même jour — ce matin, ils étaient à cheval sur minuit par hasard. **Nouveaux gestes** : `make sauvegarder-maintenant` (juste avant `raser`, inscrit au runbook `flotte-refaire`), `make restauration-etat`, `make restauration-renoncer`. Outil de nœud utilisable sans Ansible : `setops-restaurer` (avec des répétitions qui ne touchent à rien : `annuaire --essai`, `base --vers`, `fichiers --vers`). **Garde** : `scripts/tests/test_restauration.py` — chaque détenteur d'état du catalogue doit avoir son point de remise (une liste qui suit une autre prend du retard), et l'outil, rendu depuis son gabarit avec des doublures de `restic`/`psql`, doit choisir l'incarnation d'avant, refuser d'écraser, bloquer puis libérer la sauvegarde, et couper la section d'une base avant le `DROP DATABASE postgres;`. Le test a trouvé un défaut avant tout déploiement : une apostrophe dans `${c:-…}`, que bash lit comme un guillemet ouvrant. **Déployé sur Technolibre** (runner, `deployer-tout`) : 13 hôtes, 0 échec. Chaque jeu vivant reconnu `en_place` (rien écrasé) ; les deux serveurs web, vides, ont pris le chemin `a_restaurer` depuis leur instantané d'avant. `make sauvegarder-maintenant` : les 8 dépôts passent la garde. Répétitions sur les vraies données, sans rien toucher : annuaire à blanc (12 entrées rejouables depuis `71fb5481`), courriel remis dans un répertoire jetable. **La répétition a trouvé un défaut** : `psql` tourne en `postgres`, qui ne lit pas le répertoire jetable de root (0700) — en reconstruction, la base serait restée vide et le déploiement aurait échoué. La section passe désormais par l'entrée standard ; le test l'exige. **Pas encore éprouvé par une reconstruction réelle** : l'épreuve est la prochaine. ## 2026-09-30 (46) — Technolibre : l'état d'avant la reconstruction remis en place, sans reconstruire **Le constat** : après (45), le mot de passe de `sysadmin` était revenu à celui de l'amorçage. La reconstruction n'avait rien restauré — **aucune étape de Set-OPS ne réinjecte les instantanés**. « Restauration vérifiée » (`make valider`) veut dire : restaurée dans un répertoire temporaire et lue, pas remise en service. Chezlepro est dans le même cas (racine d'AC et annuaire nés le 2026-09-13). **Perdu, mesuré contre l'instantané de 23:50** (`restic-tech`) : `sysadmin` (changé le 2026-09-16), racine et intermédiaire de l'AC (nouvelle empreinte), bases Keycloak et Nextcloud, fichiers Nextcloud (197 → 108), une boîte aux lettres. **Remis en place, sur les machines existantes** (l'état neuf mis de côté avant chaque geste, dans `/root/avant-restauration-2026-09-30` de chaque nœud) : - **annuaire** (`idm-01`) : `slapadd -u` à blanc, puis base remplacée ; empreinte du mot de passe = celle d'avant, sans changement forcé. `amorcage_acces` ne touche qu'un compte absent : il ne le réécrasera pas ; - **Keycloak** (`data-sql-01`) : section `keycloak` du `pg_dumpall` rejouée dans une base vide, Keycloak arrêté ; sonde `identite` verte (fédération LDAP authentifiée) ; - **Nextcloud** (`collab-01` + `data-sql-01`) : base, `data/` et `config/` ensemble (les secrets d'instance voyagent avec les données), cache Redis purgé, `occ upgrade` (richdocuments) ; - **courriel** (`infra-mail-01`) : `/var/vmail`. **Le piège du runbook s'est présenté** : la section `nextcloud` se terminait par `DROP DATABASE postgres;`. La découpe coupe désormais aussi sur `DROP DATABASE`, et la garde « hors-périmètre » l'avait vu. **Non remis** : l'AC (aucune confiance extérieure ne s'y rattache — le poste ne fait confiance à aucune des deux racines ; la remettre imposerait de réenrôler les 13 machines) ; `icingadb` (historique de supervision) ; `forgejo` (base orpheline, plus de forge au plan). **Contrôle** : `make valider` depuis le runner de Technolibre, 0 échec ; sondes identité, passerelle, vigie, collaboration, boîtes, tableaux vertes. **Reste à coder** : l'étape de réinjection dans la reconstruction elle-même, dans l'ordre AC → confiance de la flotte → annuaire → bases → fichiers et courriel. Chezlepro n'est pas reconstruite tant qu'elle n'existe pas. ## 2026-09-30 (45) — Technolibre reconstruit depuis le runner du site, puis par son propre runner **La preuve visée** : la limite 2 du jalon de reconstruction autonome — un locataire refait PAR LES RUNNERS, pas depuis le poste. Le runner du site rase, recrée les VM et amorce le runner du locataire (sans aucun de ses secrets) ; le poste l'arme (sa voûte et sa clé, le seul geste humain) ; le runner du locataire déploie et valide sa flotte. **Déroulé** : sauvegarde fraîche des 8 machines qui portent un état (vérifiée) ; `raser` (13 VM, toutes `123…`) et `flotte-creer` depuis le runner du site ; `inseminer` (socle + moteur) ; armement depuis le poste ; sur le runner de Technolibre : amorçage du socle (AC, DNS), `deployer-tout`, `make valider` — **0 échec, valider vert, restaurations vérifiées**. Puis le pare-feu Proxmox VM par VM (procédure éprouvée : 13/13, flux identiques avant et après, Icinga à 0 critique), et la recette. **Quatre défauts trouvés — c'est le rôle de l'épreuve — tous corrigés dans le code :** 1. **L'identité SSH du runner changeait à chaque reconstruction** : il fabriquait sa paire à sa naissance, la flotte naissait avec la clé DÉCLARÉE — refus sur les 13 machines. Chez Chezlepro, la clé déclarée était déjà celle d'un runner disparu. Désormais la clé privée vit dans la voûte du locataire, l'armement la remet, et refuse si sa clé publique n'est pas déclarée au plan (garde éprouvée en défaut). Pour cette passe, les 12 VM encore vierges ont été recréées avec la bonne clé. 2. **`make flux` sur le runner d'un locataire amputait les pare-feux** : sans `underlay.yml` ni plan du site, les paires qui en dépendent ne se résolvaient à rien — ni transfert de zone depuis le DNS public du site, ni collecte de la fabric, ni insémination. Le générateur s'abstient désormais (les règles committées font foi). Vu sur le runner avant tout dommage durable ; les fichiers committés ont été remis pendant le déploiement. 3. **Keycloak : un client absent faisait échouer les mappers de groupes et de rôles** — sous `set -euo pipefail`, `grep` sans correspondance sortait avant la branche « client absent ». Jamais vu tant qu'un client `forgejo` subsistait d'une époque où le locataire avait une forge. 4. **(conception — corrigé en (49))** Un Icinga neuf marque aussitôt `sauvegarde` et `restauration` critiques (« jamais rapporté ») ; la restauration n'étant vérifiée que le dimanche, elle resterait rouge jusque-là après chaque reconstruction. Ici : sauvegarde, vérification du dépôt et contrôle de restauration déclenchés à la main sur la flotte reconstruite. **Et une garde qui a fait son travail** : le runner a refusé de déployer un génome en retard d'un commit sur la forge du site — publié pendant qu'il travaillait. **Recette** : Proxmox 0 écart ; frontière mesurée conforme ; depuis l'Internet sur `.60` : 404 du frontal, SMTP (bannière de son `edge-mta-01`), 465/587/993 en TLS 1.3 ; zone `technolibre.ca` republiée et reprise par le DNS public du site (nouveau numéro de série). ## 2026-09-29 (44) — La page du PSPBT devient la page 404 du frontal de Chezlepro Précision de l'exploitant sur (43) : c'est le **frontal** qui doit servir cette page, comme **page 404 par défaut** — pas le dorsal comme page d'un site. - **Le rôle `serveur_web_frontal`** accepte une page 404 du locataire (`serveur_web_frontal_page_404`, un chemin dans le dépôt du locataire) : le serveur par défaut la sert, statut 404, à tout nom qu'il ne publie pas ; sans page déclarée, il ferme toujours sans réponse (444). La page est `internal` (pas de lecture directe de `/404.html` : 404 aussi), avec sa propre CSP (styles et scripts en ligne, rien d'extérieur). Le nom inconnu n'obtient toujours aucun service. - **Chezlepro**, puis **Technolibre** à la demande de l'exploitant : `contenus/frontal-404.html` (le fichier `site web.html` de l'exploitant), déclaré dans leurs variables du frontal. Technolibre éprouvé de même sur `69.70.26.60` (racine, chemin quelconque, nom inconnu → 404). - **`essai.chezlepro.ca` revient à sa page de test**, CSP par défaut (la CSP propre au site, posée en (43), est retirée). Éprouvé depuis l'Internet : `http://69.70.26.61/`, un chemin quelconque, `/404.html` et un nom inconnu → 404 avec la page du PSPBT ; `essai.chezlepro.ca` → 200, la page de test. ## 2026-09-29 (43) — `essai.chezlepro.ca` sert la page du « Parti de la Sainte-Paix et du Bon Temps » À la demande de l'exploitant : le fichier `site web.html` (le seul du dossier `parti politique du bonheur national`) devient `public/index.html` du dépôt `essais/essai-web` sur la forge du site (mise à jour par l'API, depuis le runner du site), puis le dorsal de Chezlepro le tire. **Une CSP propre à ce site.** La page porte ses styles et ses scripts dans le HTML (`