Cours NestJS · Module 12: Conteneurs et CI/CD

Secrets Management - les dépêches scellées de l'Empire

6 min de lecture
Dans cette leçon6

Tu as rencontré le fichier .env dans le module d'introduction et, en local, il fait parfaitement l'affaire. En production, c'est autre chose - et il vaut la peine de savoir pourquoi, avant que quelqu'un te persuade qu'il suffit de l'ajouter au .gitignore.

Quatre raisons à la fois. Le risque d'un commit accidentel - un git add -f, ou un nouveau développeur sans .gitignore configuré, et le mot de passe est dans l'historique du dépôt pour toujours. L'absence de rotation - changer le mot de passe de la base signifie éditer le fichier à la main sur chaque serveur. L'absence d'audit - personne ne sait qui a lu un secret, ni quand. Et le plain text - les mots de passe reposent en clair sur le disque, lisibles par tout processus disposant des droits.

Note ce qui ne figure pas parmi ces raisons : les fichiers .env ne sont pas lents, ne consomment pas de mémoire et fonctionnent parfaitement dans Node.js. Le problème n'est pas technique mais organisationnel.

Rome n'envoyait pas ses dépêches scellées par un messager ordinaire. Elles passaient par une chancellerie, qui les délivrait contre reçu et savait qui avait retiré quoi.

L'ordre des fichiers .env

Avant d'en venir à la chancellerie, un mot sur les fichiers eux-mêmes. ConfigModule en accepte une liste :

1ConfigModule.forRoot({
2  isGlobal: true,
3  envFilePath: ['.env.local', '.env.development', '.env'],
4});

L'ordre d'enregistrement est fixe : ConfigModule, .forRoot({, les options avec isGlobal: true,, et enfin }).

Et voici ce qui induit le plus souvent en erreur : c'est le fichier cité en premier qui l'emporte. Quand la même variable apparaît dans plusieurs fichiers, la valeur vient de .env.local - et non de .env, malgré son air de fichier « principal ».

La logique va du plus spécifique au plus général. .env.local contient tes surcharges personnelles, que personne d'autre n'a ; .env.development les réglages de tout un environnement ; .env les valeurs par défaut. Le plus spécifique masque le plus général.

La chancellerie - HashiCorp Vault

HashiCorp Vault est un outil de gestion centralisée des secrets, avec audit et rotation. Ce n'est pas un framework frontend, ni un système de tests, ni un compresseur d'images - c'est un coffre qui délivre des valeurs à la demande et consigne chaque délivrance.

Il résout exactement les manques énumérés plus haut : le secret ne repose pas dans un fichier (rien à committer), il peut être changé en un seul endroit (rotation), et chaque lecture est notée (audit).

L'application récupère les secrets au démarrage, avant que quoi que ce soit n'en ait besoin :

1export async function loadSecrets(): Promise<Record<string, string>> {
2  const vault = new VaultClient({
3    endpoint: process.env.VAULT_ADDR,
4    token: process.env.VAULT_TOKEN,
5  });
6
7  const { data } = await vault.read('secret/data/legion-api');
8
9  return {
10    DATABASE: data.data.DATABASE,
11    JWT_SECRET: data.data.JWT_SECRET,
12    REDIS_PASSWORD: data.data.REDIS_PASSWORD,
13  };
14}

Note le paradoxe visible ici : pour atteindre le coffre, il te faut aussi une clé - une adresse et un token, qui arrivent toujours par des variables d'environnement. Vault ne supprime pas ce problème, il le réduit à un secret au lieu de vingt. Ce secret unique est souvent délivré en plus pour une courte durée par l'infrastructure elle-même.

La priorité des sources

Dans une configuration mûre, les secrets peuvent venir de plusieurs endroits à la fois. De la priorité la plus haute vers la plus basse :

  1. HashiCorp Vault - production ; il offre audit et rotation.
  2. AWS Secrets Manager - cloud ; la même chose, mais intégrée à la plateforme du fournisseur.
  3. Les variables d'environnement - le repli ; elles fonctionnent toujours, y compris en local et dans les tests.

La règle est simple : plus haut, plus de contrôle. Le repli sur les variables d'environnement reste en bas non parce qu'il est mauvais, mais parce qu'il n'offre ni audit ni rotation - tout en fonctionnant sans aucune infrastructure, si bien qu'un lancement local n'exige pas de monter un Vault.

La lecture dans l'application

Quelle que soit la source, le code va chercher un secret de la même façon :

1const dbUrl = this.configService.get<string>('DATABASE');

L'ordre des éléments est fixe : this, .configService, .get<string>, ('DATABASE'). C'est la méthode rencontrée avec la configuration - et c'est bien le but. Le service ignore si la valeur vient de Vault, du cloud ou d'une variable d'environnement ; changer de source ne touche pas une ligne de logique.

Les secrets dans Kubernetes

Un cluster ajoute une difficulté : nous décrivons la configuration dans des fichiers YAML conservés dans un dépôt - et un secret dans un tel fichier, c'est de nouveau un mot de passe dans Git.

Les Sealed Secrets permettent de conserver en toute sécurité des secrets chiffrés dans un dépôt Git. Tu chiffres une valeur avec la clé publique d'un contrôleur tournant dans le cluster ; le fichier obtenu peut être committé sans crainte, car seul ce contrôleur peut le déchiffrer, avec une clé privée qui ne quitte jamais le cluster.

Cela résout la contradiction entre « tout dans le dépôt » et « pas de secrets dans le dépôt ». Les Sealed Secrets ne mettent pas les pods à l'échelle, ne compressent pas les images et ne surveillent pas la consommation - ils chiffrent les secrets pour qu'ils puissent voyager avec le code.

Récapitulatif

La chancellerie délivre les dépêches contre reçu :

  • un .env en production est dangereux pour quatre raisons à la fois : risque de commit accidentel, absence de rotation, absence d'audit, plain text - non par lenteur ni par consommation mémoire,
  • dans envFilePath, c'est le fichier cité en premier qui l'emporte : .env.local masque .env.development, qui masque .env,
  • enregistrement dans l'ordre : ConfigModule, .forRoot({, isGlobal: true,, }),
  • HashiCorp Vault est un outil de gestion centralisée des secrets, avec audit et rotation,
  • atteindre Vault exige aussi un secret - mais un seul au lieu de vingt,
  • priorité des sources depuis le sommet : Vault (production) → AWS Secrets Manager (cloud) → variables d'environnement (repli),
  • la lecture est toujours identique : this + .configService + .get<string> + ('DATABASE') - la logique ignore l'origine de la valeur,
  • les Sealed Secrets permettent de conserver des secrets chiffrés dans un dépôt Git - seul le contrôleur du cluster peut les déchiffrer.

Dans la prochaine leçon, nous reviendrons au pipeline - cette fois avec les matrices de build, les artefacts et le cache. En attendant, retiens ceci : un fichier .env dit quel est le mot de passe ; une chancellerie dit en plus qui l'a retiré et quand il a été changé.

Code de cette leçon : src/secrets-management.ts
1// Secrets Management - Dépêches secrètes de l'Empire
2console.log("=== SECRETS MANAGEMENT ===\n");
3
4interface SecretProvider {
5  name: string;
6  type: string;
7  features: string[];
8  bestFor: string;
9}
10
11const providers: SecretProvider[] = [
12  {
13    name: 'HashiCorp Vault',
14    type: 'Self-hosted / Cloud',
15    features: ['Secrets dynamiques', 'Rotation', 'Audit log', 'Politiques d\'accès'],
16    bestFor: 'Grandes organisations, multi-cloud',
17  },
18  {
19    name: 'AWS Secrets Manager',
20    type: 'Cloud (AWS)',
21    features: ['Intégration native AWS', 'Auto-rotation', 'Déclencheurs Lambda'],
22    bestFor: 'Applications sur AWS',
23  },
24  {
25    name: 'Kubernetes Secrets',
26    type: 'Kubernetes-native',
27    features: ['Encodage base64', 'RBAC', 'Montés comme env/fichiers'],
28    bestFor: 'Applications dans Kubernetes',
29  },
30  {
31    name: 'Sealed Secrets',
32    type: 'Kubernetes + Git',
33    features: ['Chiffrement par la clé du cluster', 'Sûr dans Git', 'GitOps'],
34    bestFor: 'Workflow GitOps',
35  },
36];
37
38providers.forEach((p, i) => {
39  console.log(`${i + 1}. ${p.name} (${p.type})`);
40  console.log(`   Idéal pour : ${p.bestFor}`);
41  console.log(`   Fonctions :`);
42  p.features.forEach(f => console.log(`     - ${f}`));
43  console.log();
44});
45
46// Pourquoi PAS de .env en production
47console.log("=== POURQUOI PAS DE .env EN PRODUCTION ===\n");
48const risks = [
49  'Commit accidentel dans le dépôt',
50  'Pas de rotation - les mots de passe ne changent jamais',
51  'Pas d\'audit - qui a eu accès ?',
52  'Texte en clair sur le disque - pas de chiffrement',
53  'Copie manuelle sur chaque serveur',
54];
55
56risks.forEach((r, i) => console.log(`  ${i + 1}. ${r}`));
57console.log("\nUtilise : Vault, AWS Secrets Manager ou K8s Secrets");
58

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Pourquoi est-il dangereux de stocker des secrets dans des fichiers .env en production ?

  2. 2. Qu'est-ce que HashiCorp Vault ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Définissez un loader de config asynchrone : se connecter à Vault, lire les secrets (DATABASE, JWT_SECRET, REDIS_PASSWORD) et les retourner sous forme d'objet de config

  • Tri horizontal

    Ordonnez les éléments de la lecture d'une variable d'environnement via ConfigService :

  • Clique dans l'ordre

    Ordonnez les éléments de l'enregistrement de ConfigModule dans le bon ordre :