Rendre le parc déclaratif — 2026-09-04
Nature : chantier de trois jours, parti d'une comparaison avec un homelab public et fini sur trois mécanismes qui n'existaient pas. Au passage, une fuite de données ouverte depuis huit jours, et quatorze erreurs de fait dans le plan qui pilotait le chantier — c'est cette dernière partie qui a le plus appris.
Ce qui a déclenché le chantier
Une comparaison avec le dépôt public de Christian Lempa, qui sert de support à sa chaîne YouTube. La lecture a été instructive dans les deux sens.
Son dépôt a ce que le nôtre n'avait pas : de l'infrastructure déclarative
(Terraform, Packer) et de la gestion de configuration (Ansible). Le nôtre a ce
que le sien n'a pas du tout — on peut le vérifier par un grep sur son arbre
entier, qui ne rend aucune occurrence de restic, borg, sops,
backup, alert, loki, crowdsec ou authelia. Il documente comment
mettre un service debout ; on documente comment le garder debout et le
récupérer. La seconde moitié est absente de chez lui.
À noter avant de s'en inspirer : son dernier commit de contenu date du 4 novembre 2024. Les commits de 2026 ne touchent que son financement. C'est une référence de structure, pas de versions.
Trois écarts ont été retenus. Un quatrième a été écarté à la reconnaissance :
Packer construit des templates de VM, et le cluster n'en héberge aucune —
qm list est vide sur les deux nœuds. Il a quand même été écrit, pour que la
première VM ne soit pas montée à la main, mais il n'a aucun consommateur et son
build n'a jamais tourné.
La fuite trouvée en chemin
En construisant le manifeste Ansible, les empreintes de
/root/lynis-weekly.sh divergeaient entre les deux nœuds Proxmox.
galahad exécutait encore la version d'avant le 25/08 : NTFY_SERVER pointait
https://ntfy.sh, topic gabin-homelab — le ntfy public. Cron actif,
0 5 * * 0. Chaque dimanche à 05:00, l'indice de durcissement et le nom d'hôte
d'un hyperviseur partaient sur un topic lisible par quiconque le connaît.
Le correctif du 25/08 avait couvert lancelot. Pas galahad. Et la cause de
l'oubli est écrite noir sur blanc dans les notes de l'époque : elles
affirmaient que « galahad n'en a aucun » — pas de cron lynis. C'était faux, il
avait exactement le même. Le constat erroné n'a jamais été revérifié parce
qu'il était écrit.
Corrigé le 03/09 et vérifié de bout en bout : audit_rc=0 prouve que l'audit
tourne réellement — l'ancienne version avait aussi un bug de PATH qui
l'empêchait de tourner du tout — et report_age_s=0 prouve la fraîcheur du
rapport. Plus aucun appel réseau : le résultat part dans le journal, qu'Alloy
expédie vers Loki.
Un détail qui a coûté 24 heures : la commande de correction a d'abord été
refusée, et le refus a été attribué au classificateur de sécurité du harnais.
En réalité ssh galahad était autorisé depuis toujours ; c'est ssh root@<IP> qui ne correspondait à aucune règle. Un blocage se teste avant
d'être déclaré.
Les trois mécanismes livrés
Le détail opérationnel — quelle commande, ce qu'elle couvre, ce qu'elle ne couvre pas — est sur sa propre page : Dérive de configuration. En résumé.
Un manifeste Ansible explicite : 34 scripts et 47 units systemd sur la Pi,
5 scripts sur les nœuds. --check --diff répond enfin à « le dépôt et les
copies live sont-ils d'accord ? ». C'est la question qui n'avait pas de réponse
quand la sonde SMART est morte 114 runs, et quand galahad a publié huit jours
de plus que lancelot.
Dix conteneurs LXC déclarés en OpenTofu, un fichier par conteneur. tofu plan rend No changes. — et le jour où il ne le rendra plus, quelqu'un aura
modifié un conteneur à la main. Le token est en PVEAuditor lecture seule, et
c'est un garde-fou matériel et non une discipline : une écriture sur l'API rend
HTTP 403.
Un nommage symétrique là où plusieurs instances coexistent :
logs/logs-prod-1/ et logs/logs-replica-1/, adguard/adguard-prod-1/. Coût
réel : 77 secondes de coupure DNS, mesurées, pour le renommage d'AdGuard qui
est le résolveur de tous les LXC.
Quatorze erreurs de fait dans le plan
C'est la partie qui mérite d'être écrite. Le plan qui pilotait ce chantier comptait 1500 lignes et douze tâches. Quatorze de ses affirmations étaient fausses, et les quatorze ont été trouvées à l'exécution, pas à la relecture.
Les plus instructives :
Deux fois le même angle mort. Le recensement des scripts non versionnés et
le manifeste des nœuds ont tous deux été construits en appariant des noms de
base contre scripts/ uniquement. Chaque fois qu'une variante existait dans
system/scripts/, le mauvais fichier était retenu. Conséquence concrète : le
manifeste allait déployer sur les nœuds la variante Docker d'egress-audit.sh,
alors que la chaîne DOCKER-USER n'existe pas sur un hyperviseur — le script
aurait cassé au lancement suivant. Et il allait écraser le hook vzdump v4
anti-race par une copie périmée de 78 lignes.
Un critère d'acceptation rouge avant tout changement. Une tâche devait se
valider par curl /ready sur loki-replica. Cette sonde rend 503 en
permanence sur ce conteneur, qui sert pourtant normalement.
Une sauvegarde supposée. Le plan affirmait que l'état Terraform vivait
« dans le périmètre restic du dépôt principal ». Faux :
homelab_backup.sh sauvegarde une liste explicite de sous-répertoires, et
iac/ n'y était pas. L'état — gitignoré, donc sans protection git non plus —
a vécu une journée sans aucune copie. Le plan disait pourtant « à vérifier
explicitement, pas à supposer ». La bonne instruction était accompagnée d'une
supposition fausse.
Un secret qui devait passer par un affichage. Le plan faisait afficher le token API puis le recopier à la main. Un secret qui traverse un terminal atterrit dans un historique et un scrollback, et celui d'un token API ne s'affiche qu'une fois. Refait : le secret est canalisé du générateur vers le fichier scellé, contrôlé sur sa longueur et jamais sur son contenu.
Un outil dont le nom promet plus qu'il ne fait. Le plan disait de sceller
le token par homelab-seal-secrets.sh. Ce script ne traitait que
authelia/secrets/ et deux fichiers CrowdSec — pas le .env principal,
alors que son nom le laisse croire. Quiconque éditait le .env et lançait ce
script en pensant l'avoir scellé perdait sa modification au redémarrage
suivant, le clair vivant en tmpfs. Il le fait désormais, avec un contrôle de
déchiffrement avant de remplacer le fichier scellé : un .env.enc
illisible serait pire que pas de .env.enc, parce qu'il aurait l'air d'une
sauvegarde.
Ce que ça dit sur la manière d'écrire un plan
Le plan a été écrit en une passe, à partir de mesures prises sur les machines mais jamais recroisées. Toutes les erreurs sont de ce type : une transcription approximative, une variante non cherchée, une supposition glissée à côté d'une instruction correcte.
Aucune n'a atteint la production, et c'est le seul point rassurant : la boucle de relecture les a toutes arrêtées, et trois fois un exécutant a refusé d'appliquer en constatant que ce qu'il allait écraser valait mieux que ce qu'il allait écrire. Le refus a plus servi que l'obéissance.
Mais la relecture a payé une dette qui n'aurait pas dû exister. La règle qui en sort : un plan qui produit un inventaire doit le vérifier ligne à ligne avant de l'écrire, pas après.
La règle qui vaut au-delà de ce chantier
Un renommage, dans ce parc, ne casse pas les garde-fous : il les rend muets. Quatre occurrences en trois jours, toutes manifestées par un vert.
repo-drift-check.shcherchait ses fichiers dans un répertoire devenu parent.findne trouvait rien,2>/dev/nullavalait l'erreur, le contrôle rendait « OK » sans plus rien comparer.ci.ymletpre-merge-check.shvalidaient les fichiers compose derrière unif [ -f ]sans branche d'échec. Un chemin mort y était sauté en silence.- Un motif
.gitignorene matchait plus, laissant les fichiers temporaires d'AdGuard prêts à remonter dans un commit.
Le remède qui a tenu est celui posé dans repo-drift-check.sh : une garde qui
vérifie que ce qu'elle surveille existe encore, et remonte une dérive
sinon. Les autres ont reçu la même chose — sortir en erreur plutôt que passer.
Quand on écrit un contrôle, la question n'est pas « détecte-t-il le problème ? » mais « que rend-il si sa cible disparaît ? ». S'il rend vert, ce n'est pas un contrôle. C'est le même motif que le témoin orphelin et que la sonde SMART, sous une troisième forme.
Ce qui reste ouvert
Sept scripts tournent sur penny sans exister dans git — une réinstallation les
perdrait. La liste est dans homelab-config/ansible/NON-VERSIONNES.md, leur
tri demande un arbitrage.
setup-node-exporter.sh porte au dépôt un correctif nfsd/systemd du 27/08 que
les nœuds n'ont pas. Le déployer est une décision, pas une réparation.
Deux doublons de nom subsistent : scripts/vzdump-permfix-hook.sh est une
copie périmée de la vraie source dans system/scripts/, et fix-emmc.sh est
proxmox-fix-emmc.sh sous un autre nom. Rien ne relie les paires — c'est
exactement le terrain du témoin orphelin.
Et la symétrie d'AdGuard reste nominale : la configuration du secondaire, qui tourne en natif sur le LXC dns-failover, n'est pas au dépôt.