Backend Node + Fastify avec login Basic Auth. Je stocke le hash scrypt du mot de passe dans une variable d’environnement, lue par compose et injectée dans le conteneur :
# .env ou Environment Variables du PaaS
APP_PASSWORD_HASH=scrypt$16384$8$1$67eeyJldFrZtCScDvEy6eA==$pALJGOYxdvMma/45xQTTDyTfA...
L’auth refuse en 401 sur des creds que je sais corrects. Aucune erreur visible côté backend — juste un 401 « Invalid credentials » silencieux.
Symptôme
Dans les logs de build / compose :
warning msg="The \"pALJGOYxdvMma\" variable is not set. Defaulting to a blank string."
pALJGOYxdvMma est un fragment de mon hash. Bizarre.
Dans le conteneur, le hash semble présent — startsWith('scrypt') vrai, longueur > 100 chars. Mais en comptant les segments :
process.env.APP_PASSWORD_HASH.split('$').length
// → 5 au lieu de 6 attendus pour scrypt$N$r$p$salt$hash
Cause
Le format scrypt utilise $ comme séparateur : scrypt$N$r$p$saltB64$hashB64. Quand cette valeur passe par un mécanisme d’interpolation de variables (cas pour .env lu par compose, mais AUSSI pour les Environment Variables stockées dans certains PaaS via leur UI), chaque $<nom-valide> est traité comme une référence à substituer.
Dans le hash, après scrypt$16384$8$1$67ee...== (jusque-là OK car $16384, $8, $1 ne sont pas des noms de variables valides — ils commencent par un chiffre), arrive le segment $pALJGOYxdvMma.... pALJGOYxdvMma est un nom de variable valide (commence par une lettre). L’interpolateur le remplace par chaîne vide.
Résultat : le hash arrive dans le conteneur silencieusement tronqué, sans $ séparateur entre le salt et le hash. La verify échoue logiquement, mais sans erreur lisible.
Longueur observée = longueur attendue − longueur du segment mangé. Le len = 116 vs len = 130 attendu colle au calcul.
Solution
Single quotes dans .env désactivent l’interpolation pour compose :
APP_PASSWORD_HASH='scrypt$16384$8$1$salt==$hash=='
Mais ça ne marche pas dans tous les contextes :
- Certains PaaS gardent les quotes littéralement dans la valeur → hash préfixé/suffixé par
'→ cassé différemment - L’utilisateur ne contrôle pas toujours le mécanisme exact de pose des env vars
La solution robuste : ne pas utiliser $ dans la valeur stockée. Pour scrypt, remplacer le séparateur :
// Avant — fragile
return `scrypt$${N}$${r}$${p}$${saltB64}$${hashB64}`;
// Après — `:` n'est pas dans le base64 standard, ni interpolé
return `scrypt:${N}:${r}:${p}:${saltB64}:${hashB64}`;
Et au verify, split(':') au lieu de split('$'). Breaking change pour les hash existants — les régénérer.
Vérification post-déploiement, sans exposer le hash :
docker exec <container> node -e "
const h = process.env.APP_PASSWORD_HASH;
console.log('parts:', h.split(':').length); // doit être 6
"
Lessons learned
- Docker compose interpole
$VARet${VAR}dans les valeurs.env— pas juste dans le YAML — et certains PaaS reproduisent ce comportement même quand la var est saisie dans leur UI - Single quotes dans
.envdésactivent l’interpolation côté compose, mais sont parfois incluses littéralement par certains PaaS → résultat encore plus cassé - Le warning
"X variable is not set"où X ressemble à un fragment de ton secret = symptôme typique ; chercher$Xdans tes valeurs - Vaut pour TOUT secret avec
$: bcrypt ($2b$...), scrypt, argon2 ($argon2id$...), password de certificats, etc. - La solution robuste : ne pas utiliser
$dans les valeurs stockées en env. Changer le séparateur (:est sain car absent du base64 standard) plutôt que dépendre de l’échappement - Diag rapide :
node -e "console.log(process.env.X.split('$').length)"dans le conteneur — si != 6 pour un hash scrypt, victime de l’interpolation