All checks were successful
Dev CI / Backend — Lint (push) Successful in 10m32s
Dev CI / Frontend — Lint & Type-check (push) Successful in 11m20s
Dev CI / Backend — Unit Tests (push) Successful in 10m17s
Dev CI / Frontend — Unit Tests (push) Successful in 10m45s
Dev CI / Notify Failure (push) Has been skipped
CD Preprod / Backend — Lint (push) Successful in 10m24s
CD Preprod / Frontend — Lint & Type-check (push) Successful in 11m18s
CD Preprod / Backend — Unit Tests (push) Successful in 10m16s
CD Preprod / Frontend — Unit Tests (push) Successful in 10m45s
CD Preprod / Backend — Integration Tests (push) Successful in 9m56s
CD Preprod / Build Frontend (push) Successful in 56s
CD Preprod / Build Log Exporter (push) Successful in 32s
CD Preprod / Build Backend (push) Successful in 6m48s
CD Preprod / Deploy to Preprod (push) Successful in 23s
CD Preprod / Notify Failure (push) Has been skipped
CD Preprod / Notify Success (push) Successful in 2s
133 lines
4.6 KiB
JavaScript
133 lines
4.6 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.
|
||
*
|
||
* Le hash est appliqué à CHAQUE lancement du backend (AdminBootstrapService) :
|
||
* - au compte BOOTSTRAP_ADMIN_EMAIL s'il ne s'est encore jamais connecté ;
|
||
* - à tout compte si BOOTSTRAP_ADMIN_RESET_PASSWORD=true (retirez ensuite
|
||
* les deux variables, sinon un mot de passe changé depuis l'interface
|
||
* serait remplacé au lancement suivant).
|
||
*
|
||
* Sans hash, le compte est créé sans mot de passe utilisable : il faut alors
|
||
* « mot de passe oublié » (SMTP requis) ou, sans SMTP, la commande de secours :
|
||
* docker exec -it <conteneur-backend> node admin-password.js <email>
|
||
*/
|
||
|
||
'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);
|
||
});
|