Gestion des secrets

Un secret est toute valeur qui donne accès à une ressource protégée : clé API, mot de passe de base de données, token JWT, certificat, clé de chiffrement. Une mauvaise gestion des secrets est l'une des principales causes de compromission d'application.


Principes fondamentaux

✅ Jamais dans le code source (même privé)
✅ Jamais dans git — un secret commité est un secret compromis, même supprimé ensuite
✅ Un secret différent par environnement (dev ≠ staging ≠ prod)
✅ Principe du moindre privilège — chaque service n'accède qu'aux secrets dont il a besoin
✅ Rotation régulière — surtout après un départ d'équipe ou un incident
✅ Tracer les accès — qui a lu quel secret, quand
❌ API_KEY=sk-1234 dans le code
❌ Secrets dans les URLs (GET /api?token=xxx → apparaît dans les logs)
❌ Secrets dans les messages d'erreur ou les logs applicatifs
❌ Partage de secrets par email, Slack ou chat
❌ Même secret pour dev et prod

Dev local — dotenv

Installation

npm install dotenv

Structure des fichiers

projet/
├── .env              # secrets locaux — JAMAIS commité
├── .env.example      # template sans valeurs — commité dans git
├── .env.test         # variables pour les tests (pas de vraies clés)
└── .gitignore        # doit contenir .env
# .env.example — template partagé avec l'équipe
DATABASE_URL=postgres://user:password@localhost:5432/mydb
JWT_SECRET=
STRIPE_SECRET_KEY=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
# .env — valeurs réelles (local uniquement, non commité)
DATABASE_URL=postgres://alice:monmotdepasse@localhost:5432/mydb
JWT_SECRET=un-secret-fort-de-32-caracteres-minimum
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxx

Chargement dans Node.js

// ✅ Charger dotenv au plus tôt dans le point d'entrée
require('dotenv').config();

// Ou avec ES modules
import 'dotenv/config';
// Valider les variables requises au démarrage
function validerConfig() {
  const requis = ['DATABASE_URL', 'JWT_SECRET', 'STRIPE_SECRET_KEY'];
  const manquants = requis.filter(key => !process.env[key]);

  if (manquants.length > 0) {
    throw new Error(`Variables d'environnement manquantes : ${manquants.join(', ')}`);
  }
}

validerConfig(); // fail fast — l'app ne démarre pas si un secret manque
// Plusieurs fichiers .env selon l'environnement
require('dotenv').config({ path: ['.env.local', '.env'] });
// .env.local prend la priorité sur .env

Pièges courants

// ❌ Exposer tous les secrets dans les logs
console.log(process.env);

// ❌ Exposer les secrets dans les messages d'erreur
res.json({ erreur: err.message, config: process.env.DATABASE_URL });

// ✅ Ne logger que ce qui est nécessaire
console.log('Connexion DB établie sur', new URL(process.env.DATABASE_URL).hostname);
# .gitignore — toujours inclure
.env
.env.local
.env.*.local
*.key
*.pem

Multilignes (certificats, clés RSA)

# .env — clé privée sur plusieurs lignes
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"

# Ou avec \n explicite
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEow...\n-----END RSA PRIVATE KEY-----"

Docker et Docker Compose

Variables d'environnement dans Docker Compose

# ✅ Option 1 : référencer les variables de l'hôte (sans valeur = hérité du shell ou du .env)
services:
  api:
    image: mon-api:latest
    environment:
      - NODE_ENV=production
      - DATABASE_URL          # hérite de l'environnement hôte
      - JWT_SECRET            # hérite de l'environnement hôte

# ✅ Option 2 : fichier .env séparé (non commité)
services:
  api:
    image: mon-api:latest
    env_file:
      - .env.production       # fichier local non versionné
# ❌ Ne jamais COPY le fichier .env dans l'image
COPY . .          # inclut .env si pas dans .dockerignore !
COPY .env .       # encore pire — explicitement dans l'image

# ✅ .dockerignore
.env
.env.*
*.key
*.pem
# .dockerignore
.env
.env.*
.git
*.key
*.pem
node_modules

Docker secrets (mode Swarm / production)

Docker secrets monte les valeurs dans des fichiers à /run/secrets/ — jamais dans les variables d'environnement du processus.

# docker-compose.yml (Swarm mode)
services:
  api:
    image: mon-api:latest
    secrets:
      - db_password
      - jwt_secret

secrets:
  db_password:
    external: true    # créé via : docker secret create db_password ./db_password.txt
  jwt_secret:
    external: true
// Lire un Docker secret depuis Node.js
const fs = require('fs');

function lireSecret(nom) {
  try {
    return fs.readFileSync(`/run/secrets/${nom}`, 'utf8').trim();
  } catch {
    return process.env[nom.toUpperCase()]; // fallback pour le dev local
  }
}

const dbPassword = lireSecret('db_password');

CI/CD — GitHub Actions

Configurer les secrets

Les secrets sont configurés dans Settings → Secrets and variables → Actions du dépôt ou de l'organisation.

# .github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production   # secrets spécifiques à l'environnement

    steps:
      - uses: actions/checkout@v4

      - name: Build et déploiement
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          JWT_SECRET: ${{ secrets.JWT_SECRET }}
          STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
        run: npm run deploy

Masquage automatique

GitHub masque automatiquement les secrets dans les logs (***). Mais attention :

# ❌ Ne jamais afficher explicitement un secret dans un step
- name: Debug
  run: echo "JWT_SECRET=${{ secrets.JWT_SECRET }}"  # masqué mais mauvaise pratique

# ❌ Les secrets ne sont pas disponibles dans les PRs de forks (sécurité intentionnelle)
on:
  pull_request:
    # secrets.* = vide pour les PRs de forks

Environnements (staging vs prod)

jobs:
  deploy-staging:
    environment: staging      # utilise les secrets de l'environnement "staging"
    steps:
      - run: npm run deploy:staging

  deploy-prod:
    environment: production   # utilise les secrets de l'environnement "production"
    needs: deploy-staging
    steps:
      - run: npm run deploy:prod

Passer des secrets à Docker dans la CI

- name: Build Docker image
  run: |
    docker build \
      --build-arg NODE_ENV=production \
      -t mon-app:latest .
  # ✅ Pas de --build-arg avec les secrets — ils resteraient dans les layers de l'image

- name: Deploy
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}
  run: docker run -e DATABASE_URL mon-app:latest

Production — secrets managers

Pour les équipes et les applications en production, un secrets manager centralise la gestion, l'audit et la rotation.

Comparatif

HashiCorp Vault AWS Secrets Manager Doppler
Type Self-hosted ou cloud Cloud AWS SaaS
Rotation auto Oui (avec plugins) Oui (intégration AWS) Oui
Audit des accès Oui Oui (CloudTrail) Oui
Intégration CI/CD Vault Agent, plugins Actions AWS CLI/SDK natif
Complexité Élevée Moyenne Faible
Idéal pour Infrastructure complexe, multi-cloud Stack AWS Startups, équipes dev

Doppler — exemple d'intégration Node.js

# Installation du CLI
npm install -g @dopplerhq/cli

# Authentification et liaison au projet
doppler login
doppler setup   # sélectionner projet + environnement

# Lancer l'app avec les secrets injectés
doppler run -- node server.js
# GitHub Actions avec Doppler
- name: Fetch secrets from Doppler
  uses: dopplerhq/secrets-fetch-action@v1
  id: doppler
  with:
    doppler-token: ${{ secrets.DOPPLER_TOKEN }}
    inject-env-vars: true

- name: Deploy
  run: npm run deploy
  # Les secrets Doppler sont disponibles comme variables d'environnement
// En production : les secrets sont injectés par Doppler comme variables d'env
// Le code reste identique — pas de SDK Doppler requis dans l'app
const dbUrl = process.env.DATABASE_URL;

AWS Secrets Manager — exemple Node.js

npm install @aws-sdk/client-secrets-manager
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');

const client = new SecretsManagerClient({ region: 'eu-west-1' });

async function obtenirSecret(nom) {
  const commande = new GetSecretValueCommand({ SecretId: nom });
  const reponse = await client.send(commande);
  return JSON.parse(reponse.SecretString);
}

// Au démarrage de l'app (mettre en cache — ne pas appeler à chaque requête)
const secrets = await obtenirSecret('mon-app/production');
const { DB_PASSWORD, JWT_SECRET } = secrets;

Détecter les fuites

Outils de détection

# trufflehog — scan de l'historique git pour détecter des secrets
npx trufflehog git file://. --only-verified

# gitleaks — alternative légère
brew install gitleaks
gitleaks detect --source . --verbose

# detect-secrets (Python) — intégrable en pre-commit
pip install detect-secrets
detect-secrets scan > .secrets.baseline

Pre-commit hook

# Installation de pre-commit
pip install pre-commit
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks
pre-commit install   # active le hook
# → gitleaks s'exécute automatiquement avant chaque commit

Que faire si un secret est exposé

1. Révoquer et régénérer immédiatement
   → Ne pas attendre — considérer le secret comme compromis dès la détection

2. Vérifier les accès
   → Consulter les logs : le secret a-t-il été utilisé par quelqu'un d'autre ?

3. Purger l'historique git
   → git filter-repo (remplace BFG Repo-Cleaner)
   → Forcer le push de toutes les branches concernées

4. Notifier
   → L'équipe, les parties prenantes, et si nécessaire les utilisateurs impactés

5. Post-mortem
   → Comment le secret s'est retrouvé là ? Ajouter une règle pour l'éviter
# Purger un secret de l'historique git avec git filter-repo
pip install git-filter-repo

git filter-repo --replace-text <(echo 'sk_live_XXXXXXXX==>***REMOVED***')

# Forcer le push (après coordination avec l'équipe)
git push origin --force --all
git push origin --force --tags

Checklist

# Vérification
1 .env dans .gitignore et .dockerignore
2 .env.example commité avec les clés (sans valeurs)
3 Validation des variables requises au démarrage de l'app
4 Secrets différents par environnement (dev / staging / prod)
5 Secrets GitHub Actions configurés par environnement
6 Aucun secret dans les logs, erreurs ou URLs
7 Aucun COPY .env dans les Dockerfiles
8 Pre-commit hook de détection (gitleaks) activé
9 Secrets manager utilisé en production (Doppler, Vault, AWS SM…)
10 Procédure de rotation documentée et testée