# CHANGELOG — Set-OPS ## 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 (`