guest@6admin.io:~$

Le hostgroup cloné qui refusait de se sauvegarder

Un hostgroup Windows cloné dans Foreman et toute tentative de le modifier via l’interface renvoie une erreur 500. Pas une fois sur deux, toujours. Le hostgroup frère, non cloné, s’enregistre sans broncher. Au début j’ai cru à une corruption de l’enregistrement et j’ai fini par lire le code source de Foreman pour comprendre, parce que l’erreur pointait vers un champ qui n’avait, en apparence, aucune raison d’être en cause.

Le symptôme

La sauvegarde échoue sur une erreur Postgres sans ambiguïté:

PG::StringDataRightTruncation: value too long for type character varying(255)

et l’audit désigne le champ grub_pass. Premier réflexe, vider grub_pass. Ça débloque une fois, puis le problème revient au clonage suivant, ce qui veut dire que grub_pass n’est pas la cause mais le symptôme et qu’il faut remonter.

En décodant la valeur auditée, deux passes de base64 vers de l’UTF-16LE, je tombe sur une chaîne en clair de la forme <motdepasse>AdministratorPassword, c’est-à-dire le contenu d’un bloc <AdministratorPassword> d’unattend.xml Windows. Autrement dit, ce n’est pas un mot de passe GRUB qui déborde, c’est le mot de passe administrateur Windows, porté par root_pass avec le hash Base64-Windows dont l’unattend a besoin, qui a fini dans grub_pass et qui grossit.

Lire la source plutôt que deviner

La seule façon honnête de conclure, c’était d’aller voir ce que Foreman fait à la sauvegarde. Deux lignes suffisent à tout expliquer:

self.root_pass = crypt_pass(root_pass, :root)
self.grub_pass = crypt_pass(self[:grub_pass] || root_pass, :grub)

grub_pass n’est pas un champ indépendant, il est dérivé de root_pass quand il est vide et crypt_pass est censé ne pas ré-encoder une valeur déjà chiffrée. Pour le hash Base64-Windows, ce test d’idempotence repose sur une heuristique qui devient fausse dès que root_pass_changed? est vrai et root_pass_changed? est précisément vrai au clonage, puisque la valeur est ré-affectée sur le nouvel enregistrement. Conséquence: à chaque clonage, Foreman considère une valeur déjà encodée comme non chiffrée et ajoute une couche de base64(UTF-16LE). La valeur gonfle clone après clone, grub_pass accumulant ses couches indépendamment, jusqu’à dépasser la limite varchar(255) de la colonne et la sauvegarde casse.

Ce n’est donc ni une configuration isolée sur grub_pass, ni un secret rangé dans le mauvais champ: c’est un comportement structurel de la gestion des mots de passe Base64-Windows de Foreman, réveillé par le clonage. Je ne l’aurais jamais déduit des symptômes, seulement de la source.

Le correctif, qui tient à l’héritage

La contrainte est que root_pass doit rester renseigné, il porte le mot de passe administrateur consommé par l’unattend, donc le supprimer n’est pas une option. Heureusement, root_pass est résolu par héritage dans Foreman: valeur propre du hostgroup, sinon premier ancêtre qui en a une, sinon le réglage global et la validation autorise explicitement une colonne vide.

Le correctif consiste donc à centraliser sans supprimer: poser root_pass une seule fois sur le parent porteur de l’OS et laisser la colonne vide sur les feuilles et les clones, qui en hériteront. La raison pour laquelle cela tue l’overflow est nette: le callback n’agit que sur la colonne propre de l’enregistrement, donc sur une feuille à colonne vide il ne ré-encode rien et cloner une feuille ne peut plus rien faire gonfler. Le template continue de recevoir le mot de passe par héritage et le clonage des feuilles, l’opération courante, redevient sûr.

Déblocage concret, une fois le parent renseigné proprement:

docker exec "$(docker ps -qf name=foreman_webapp)" bundle exec rails runner \
 'Hostgroup.unscoped.where(id:[19,23]).update_all(root_pass: nil, grub_pass: nil)'

Et surtout, ne pas élargir la colonne en text pour faire taire l’erreur: cela masquerait le ré-encodage et laisserait la valeur grandir indéfiniment, ce qui est soigner le thermomètre.

La vraie trouvaille était ailleurs

En décodant cette valeur pour diagnostiquer, j’ai mis le doigt sur un problème bien plus gênant que l’erreur 500. Le mot de passe administrateur local Windows était stocké de façon réversible, décodable sans clé, dans la base Foreman et recopié en clair dans les logs d’audit, donc potentiellement dans la collecte de logs. Il était de surcroît faible. L’incident de configuration n’était finalement qu’un révélateur: le vrai sujet, c’est qu’un secret censé rester secret se baladait en clair à deux endroits et la première action corrective n’a pas été de débloquer le hostgroup mais de faire tourner ce mot de passe et de purger son exposition dans les audits.

Le coupable

grub_pass n’est pas un champ indépendant, il dérive de root_pass, donc le vider seul ne corrige jamais durablement; il faut traiter la source. Le hash Base64-Windows combiné au clonage entraîne un ré-encodage parce que la détection d’idempotence s’appuie sur root_pass_changed?, vrai au clonage et la valeur grandit d’une couche à chaque clone. L’héritage de root_pass est la clé: le poser une fois en haut de l’arbre et laisser les feuilles vides garde le mot de passe fonctionnel sans subir l’overflow. Et la leçon la plus utile n’est pas technique: une erreur 500 banale a servi de fil pour remonter jusqu’à un secret stocké de façon réversible et journalisé en clair, ce qui rappelle qu’un incident de disponibilité cache parfois un incident de confidentialité qu’il faut avoir le réflexe de regarder.