Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
130 lines
4.4 KiB
JavaScript
130 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Génère un hash Argon2id pour BOOTSTRAP_ADMIN_PASSWORD_HASH.
|
||
*
|
||
* cd apps/backend && node scripts/setup/generate-admin-hash.js
|
||
*
|
||
* Le mot de passe est saisi sans écho et ne quitte jamais votre poste : ni
|
||
* argument de ligne de commande (visible dans `ps` et dans l'historique du
|
||
* shell), ni variable d'environnement, ni fichier temporaire.
|
||
*
|
||
* RAPPEL — le mode SANS mot de passe est préférable.
|
||
* Si votre chaîne SMTP fonctionne, ne renseignez que BOOTSTRAP_ADMIN_EMAIL :
|
||
* le compte est alors créé sans mot de passe utilisable et vous le définissez
|
||
* via « mot de passe oublié ». Aucun secret n'existe nulle part, il n'y a donc
|
||
* rien à faire fuiter. Ce script n'est utile que si vous devez pouvoir vous
|
||
* connecter avant que l'envoi de courriels ne soit opérationnel.
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const argon2 = require('argon2');
|
||
const readline = require('readline');
|
||
|
||
// Mêmes paramètres que auth.service.ts : un hash produit ici est vérifiable
|
||
// par l'application sans aucune adaptation.
|
||
const ARGON2_OPTIONS = {
|
||
type: argon2.argon2id,
|
||
memoryCost: 65536, // 64 Mo
|
||
timeCost: 3,
|
||
parallelism: 4,
|
||
};
|
||
|
||
const MIN_LENGTH = 16;
|
||
|
||
/** Saisie masquée sur un terminal ; lecture directe si l'entrée est redirigée. */
|
||
function readSecret(prompt) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!process.stdin.isTTY) {
|
||
let data = '';
|
||
process.stdin.setEncoding('utf8');
|
||
process.stdin.on('data', chunk => (data += chunk));
|
||
process.stdin.on('end', () => resolve(data.replace(/\r?\n$/, '')));
|
||
process.stdin.on('error', reject);
|
||
return;
|
||
}
|
||
|
||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||
const onKeypress = () => {
|
||
// Réécrit la ligne sans révéler la longueur de la saisie.
|
||
readline.clearLine(process.stdout, 0);
|
||
readline.cursorTo(process.stdout, 0);
|
||
process.stdout.write(prompt);
|
||
};
|
||
|
||
process.stdout.write(prompt);
|
||
process.stdin.on('data', onKeypress);
|
||
|
||
rl.question('', answer => {
|
||
process.stdin.removeListener('data', onKeypress);
|
||
rl.close();
|
||
process.stdout.write('\n');
|
||
resolve(answer);
|
||
});
|
||
});
|
||
}
|
||
|
||
function checkStrength(password) {
|
||
const problems = [];
|
||
if (password.length < MIN_LENGTH) {
|
||
problems.push(`au moins ${MIN_LENGTH} caractères (${password.length} fournis)`);
|
||
}
|
||
if (!/[a-z]/.test(password)) problems.push('une minuscule');
|
||
if (!/[A-Z]/.test(password)) problems.push('une majuscule');
|
||
if (!/[0-9]/.test(password)) problems.push('un chiffre');
|
||
if (!/[^A-Za-z0-9]/.test(password)) problems.push('un caractère spécial');
|
||
return problems;
|
||
}
|
||
|
||
async function main() {
|
||
console.log('');
|
||
console.log('Génération du hash Argon2id pour le premier administrateur.');
|
||
console.log('La saisie n’est pas affichée.');
|
||
console.log('');
|
||
|
||
const password = await readSecret('Mot de passe : ');
|
||
if (!password) {
|
||
console.error('Aucun mot de passe saisi.');
|
||
process.exit(1);
|
||
}
|
||
|
||
if (process.stdin.isTTY) {
|
||
const confirmation = await readSecret('Confirmation : ');
|
||
if (confirmation !== password) {
|
||
console.error('Les deux saisies diffèrent.');
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
const problems = checkStrength(password);
|
||
if (problems.length > 0) {
|
||
console.error('');
|
||
console.error('Mot de passe refusé. Il manque : ' + problems.join(', ') + '.');
|
||
console.error('Ce compte a tous les droits sur la plateforme : générez plutôt une');
|
||
console.error('phrase longue et aléatoire depuis votre gestionnaire de mots de passe.');
|
||
process.exit(1);
|
||
}
|
||
|
||
const hash = await argon2.hash(password, ARGON2_OPTIONS);
|
||
|
||
console.log('');
|
||
console.log('Hash à placer dans le Secret Kubernetes (jamais dans le ConfigMap) :');
|
||
console.log('');
|
||
console.log(' BOOTSTRAP_ADMIN_PASSWORD_HASH: ' + JSON.stringify(hash));
|
||
console.log('');
|
||
console.log(' cd infra/prod && sops k8s/base/03-secrets.sops.yaml');
|
||
console.log('');
|
||
console.log('Après votre première connexion :');
|
||
console.log(' 1. changez le mot de passe depuis l’interface ;');
|
||
console.log(' 2. retirez BOOTSTRAP_ADMIN_PASSWORD_HASH du Secret et réappliquez.');
|
||
console.log('');
|
||
console.log('Un hash reste attaquable hors ligne : il n’a plus aucune raison');
|
||
console.log('de rester stocké une fois le compte opérationnel.');
|
||
console.log('');
|
||
}
|
||
|
||
main().catch(error => {
|
||
console.error('Échec :', error.message);
|
||
process.exit(1);
|
||
});
|