Jour 52 Day 52 · jeudi 22 octobre 2026 Thursday 22 October 2026 Sécurité Intermédiaire

Gestion des secrets Secrets management

Clés API, mots de passe de DB, tokens : où les mettre, où ne JAMAIS les mettre, et quoi faire quand ça fuite — la question sécurité la plus concrète qu'on puisse vous poser en entretien. API keys, DB passwords, tokens: where to put them, where NEVER to put them, and what to do when they leak — the most concrete security question you can get in an interview.

L’essentiel

Un secret, c’est toute donnée qui donne un accès : mot de passe de base de données, clé API (Stripe, AWS, OpenAI…), token OAuth, clé privée SSH ou TLS, secret de signature JWT. La règle numéro un est simple et non négociable : un secret ne vit jamais dans le code ni dans le repo. Pas dans une constante, pas dans un fichier de config commité, pas « juste pour tester ».

Pourquoi si strict ? Parce que git n’oublie rien. Un secret commité puis retiré au commit suivant reste dans l’historique, récupérable par n’importe qui clone le repo (git log -p, git reflog). Sur GitHub, des bots scannent les commits publics en continu : une clé AWS poussée sur un repo public est exploitée en quelques minutes, pas en quelques jours. Un secret commité est un secret grillé : la seule réponse correcte est de le révoquer et d’en générer un nouveau (rotation).

Le code doit donc lire ses secrets depuis l’extérieur au démarrage : variables d’environnement, fichier local non versionné, ou coffre-fort dédié.

Comment ça marche

Le secret suit l’application dans trois contextes, avec un mécanisme différent à chaque étape :

 DEV                    CI                    PROD
 .env (gitignoré)  Secrets chiffrés du   Env vars injectées
 lu au démarrage   pipeline (GitHub/     par la plateforme,
 par l'app         GitLab), masqués      ou coffre (Vault,
        │          dans les logs         cloud secrets mgr)
        └──────── .env.example versionné ────────┘
                  (les NOMS, jamais les valeurs)

Variables d’environnement : le standard de fait (piliers du 12-factor app). L’app lit process.env.DATABASE_URL ou os.environ["DATABASE_URL"] — le même code tourne en dev, CI et prod avec des valeurs différentes. Mais connaissez leurs limites : elles sont visibles dans /proc/<pid>/environ et via ps e pour les processus du même utilisateur, elles sont héritées par tous les processus enfants (y compris ce script npm tiers), et elles finissent trop facilement dans les logs d’erreur ou les crash reports.

Fichiers .env : la version dev-friendly des env vars. Un fichier clé=valeur à la racine, chargé par dotenv (ou nativement par Node 20+, Vite, Next.js). Deux règles absolues : .env est gitignoré, et un .env.example versionné documente les noms de variables attendus (valeurs bidon) pour qu’un nouveau dev sache quoi remplir.

Coffres (vaults) : pour la prod sérieuse. HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault : les secrets sont stockés chiffrés, l’accès est authentifié, audité, et la rotation peut être automatique. sops (Mozilla) est l’option légère : chiffrer les fichiers de secrets dans le repo avec une clé externe (age, KMS). En stage, savoir que ça existe et à quoi ça sert suffit.

ContexteSolution adaptée
Dev local.env gitignoré + .env.example versionné
CI/CDSecrets chiffrés du pipeline (GitHub/GitLab), masqués dans les logs
Prod simple (PaaS, VPS)Env vars injectées par la plateforme (Coolify, Heroku…)
Prod sérieuse / multi-servicesVault, AWS/GCP/Azure secrets manager (audit, rotation)
Config chiffrée versionnéesops + age/KMS
Préventiongitleaks en pre-commit et en CI

Secrets en CI : jamais dans le YAML du pipeline. GitHub Actions et GitLab CI ont un store de secrets chiffrés, injectés en env vars au run, et masqués dans les logs (***). Le masquage est un filet, pas une garantie : un secret encodé en base64 ou coupé en deux passe à travers.

⚠️ Le secret commité puis retiré est toujours dans l’historique — git rm + nouveau commit ne supprime rien : l’ancien blob reste accessible via l’historique, les reflogs, les forks et les clones existants. C’est pour ça que la rotation est obligatoire même si vous nettoyez l’historique ensuite.

Concepts clés à maîtriser

  • Rotation : changer un secret régulièrement (ou immédiatement après une fuite). Un secret conçu pour être facilement rotaté (lu au démarrage, jamais en dur) rend l’incident bénin ; un secret impossible à changer sans redéployer trois services est une bombe.
  • Moindre privilège : un token n’a que les droits dont il a besoin. La clé du service de mails ne doit pas pouvoir supprimer des buckets S3. Fuite = dégâts proportionnels aux droits.
  • Scanner avant de commiter : gitleaks détecte les patterns de secrets (clés AWS, tokens GitHub, entropie élevée). Branché en hook pre-commit, il bloque le commit fautif avant qu’il n’entre dans l’historique — le seul moment où c’est encore réparable gratuitement. En CI, il attrape ce qui a échappé au hook.
  • Séparer par environnement : le secret de dev n’est pas celui de prod. Une base de dev compromise ne doit rien donner sur la prod.
  • Procédure de fuite — l’ordre compte : 1) révoquer/rotater le secret immédiatement (c’est ce qui stoppe l’hémorragie), 2) vérifier les logs d’accès pour évaluer l’exploitation, 3) nettoyer l’historique git (git filter-repo, ou BFG) et forcer le push, 4) comprendre comment c’est arrivé et ajouter le garde-fou (gitleaks, review). Nettoyer l’historique sans révoquer ne sert à rien : les clones existants ont toujours le secret.

Le trio de fichiers qui structure tout ça :

# .gitignore — le .env n'entre JAMAIS dans le repo
.env
.env.*            # .env.local, .env.production…
!.env.example     # exception : l'exemple, lui, est versionné

# .env.example — versionné : les NOMS, des valeurs bidon
DATABASE_URL=postgres://user:password@localhost:5432/app
STRIPE_SECRET_KEY=sk_test_xxx
JWT_SECRET=change-me

# .env — local, gitignoré : les VRAIES valeurs
DATABASE_URL=postgres://app:S3cr3t!@localhost:5432/app
STRIPE_SECRET_KEY=sk_live_51Mq...
JWT_SECRET=b1946ac92492d2347c6235b4d2611184

💡 Réflexe à montrer — le .env.example n’est pas un détail : c’est lui qui rend le .env gitignorable sans casser l’onboarding. cp .env.example .env, on remplit, l’app démarre. Sans lui, quelqu’un finira par commiter le vrai .env « pour que ça marche chez les autres ».

En entretien

« Un secret a fuité dans un commit poussé, tu fais quoi ? » — Dans l’ordre : je révoque le secret immédiatement (nouvelle clé côté provider) — c’est l’étape critique, car le secret est déjà considéré comme compromis. Je vérifie les logs d’accès du service concerné pour voir s’il a été exploité. Ensuite seulement, je nettoie l’historique (git filter-repo) et je préviens l’équipe (force push = re-clone). Enfin, j’ajoute gitleaks en pre-commit pour que ça ne se reproduise pas.

« Pourquoi ne pas mettre les secrets dans le code, si le repo est privé ? » — Un repo privé n’est pas un coffre : accès de tous les devs (et ex-devs via leurs clones), intégrations tierces (CI, outils d’analyse), risque de passage en public, laptop volé. Et un secret dans le code est couplé au déploiement : impossible de le rotater sans re-release. Externaliser le secret, c’est aussi pouvoir le changer en 30 secondes.

« Variables d’environnement : limites ? » — Visibles dans /proc/<pid>/environ et ps e (même user), héritées par tous les processus enfants (dépendances, scripts tiers), souvent dumpées dans les logs de crash ou les error reporters. C’est un bon transport, pas un stockage : en prod sérieuse, la source de vérité est un secrets manager qui les injecte.

« C’est quoi la différence entre .env et .env.example ? » — .env contient les vraies valeurs, il est gitignoré et local à chaque machine/environnement. .env.example est versionné et ne contient que les noms de variables avec des valeurs factices : c’est la documentation du contrat de configuration. Nouveau dev : cp .env.example .env et remplir.

« Comment gères-tu les secrets dans une CI GitHub Actions ? » — Store de secrets chiffrés du repo/org (Settings → Secrets), référencés via ${{ secrets.MY_KEY }}, injectés en env vars au run et masqués dans les logs. Jamais en clair dans le YAML. Bonus : les workflows déclenchés par des forks n’ont pas accès aux secrets — c’est voulu.

Pièges & idées reçues

  • « Je l’ai supprimé au commit suivant, c’est bon » — non : l’historique garde tout. Révocation obligatoire, nettoyage d’historique en second.
  • « Le masquage des logs CI protège le secret » — il masque la chaîne exacte : echo $KEY | base64 la fait ressortir en clair. Le masquage limite les accidents, il n’arrête pas une exfiltration.
  • « Un .env.production sur le serveur, c’est comme un vault » — c’est mieux que le repo, mais pas chiffré, pas audité, pas rotaté. Acceptable pour un side project, insuffisant dès qu’il y a des données clients.
  • Le frontend n’a pas de secrets : tout ce qui part dans le bundle JS (NEXT_PUBLIC_*, VITE_*) est public par définition. Une « clé API secrète » côté client n’existe pas — l’appel sensible passe par votre backend.
  • Secrets dans les images Docker : un COPY .env ou un ARG SECRET reste lisible dans les couches de l’image (docker history). Utiliser les secrets de build (--secret) ou l’injection au runtime.

🎤 En entretien — « un secret a fuité, tu fais quoi ? » est LA question type. La réponse attendue tient en un mot d’ordre : révoquer d’abord, nettoyer ensuite. Le candidat qui commence par « je réécris l’historique git » a raté l’essentiel : le secret est déjà dans la nature.

Pour aller plus loin

The essentials

A secret is any piece of data that grants access: database password, API key (Stripe, AWS, OpenAI…), OAuth token, SSH or TLS private key, JWT signing secret. Rule number one is simple and non-negotiable: a secret never lives in the code or in the repo. Not in a constant, not in a committed config file, not “just for testing”.

Why so strict? Because git never forgets. A secret committed then removed in the next commit stays in the history, retrievable by anyone who clones the repo (git log -p, git reflog). On GitHub, bots scan public commits continuously: an AWS key pushed to a public repo gets exploited within minutes, not days. A committed secret is a burned secret: the only correct response is to revoke it and generate a new one (rotation).

So the code must read its secrets from the outside at startup: environment variables, an unversioned local file, or a dedicated vault.

How it works

The secret follows the application through three contexts, with a different mechanism at each step:

 DEV                    CI                    PROD
 .env (gitignored)  Encrypted pipeline    Env vars injected
 read at startup    secrets (GitHub/      by the platform,
 by the app         GitLab), masked       or a vault (Vault,
        │           in the logs          cloud secrets mgr)
        └──────── versioned .env.example ────────┘
                  (the NAMES, never the values)

Environment variables: the de facto standard (a pillar of the 12-factor app). The app reads process.env.DATABASE_URL or os.environ["DATABASE_URL"] — the same code runs in dev, CI and prod with different values. But know their limits: they’re visible in /proc/<pid>/environ and via ps e for same-user processes, they’re inherited by every child process (including that third-party npm script), and they end up in error logs and crash reports far too easily.

.env files: the dev-friendly flavor of env vars. A key=value file at the project root, loaded by dotenv (or natively by Node 20+, Vite, Next.js). Two absolute rules: .env is gitignored, and a versioned .env.example documents the expected variable names (with dummy values) so a new dev knows what to fill in.

Vaults: for serious production. HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault: secrets are stored encrypted, access is authenticated and audited, and rotation can be automated. sops (Mozilla) is the lightweight option: encrypt secret files inside the repo with an external key (age, KMS). For an internship, knowing these exist and what they’re for is enough.

ContextRight solution
Local devGitignored .env + versioned .env.example
CI/CDEncrypted pipeline secrets (GitHub/GitLab), masked in logs
Simple prod (PaaS, VPS)Env vars injected by the platform (Coolify, Heroku…)
Serious prod / multi-serviceVault, AWS/GCP/Azure secrets manager (audit, rotation)
Encrypted versioned configsops + age/KMS
Preventiongitleaks in pre-commit and CI

Secrets in CI: never in the pipeline YAML. GitHub Actions and GitLab CI have an encrypted secrets store, injected as env vars at run time and masked in the logs (***). Masking is a safety net, not a guarantee: a secret encoded in base64 or split in two slips right through.

⚠️ A committed-then-removed secret is still in the history — git rm + a new commit deletes nothing: the old blob stays reachable through the history, reflogs, forks and existing clones. That’s why rotation is mandatory even if you clean the history afterwards.

Key concepts to master

  • Rotation: change a secret regularly (or immediately after a leak). A secret designed to be easy to rotate (read at startup, never hardcoded) makes an incident harmless; a secret you can’t change without redeploying three services is a bomb.
  • Least privilege: a token gets only the rights it needs. The email service’s key must not be able to delete S3 buckets. Leak = damage proportional to the rights granted.
  • Scan before committing: gitleaks detects secret patterns (AWS keys, GitHub tokens, high entropy). Wired as a pre-commit hook, it blocks the offending commit before it enters the history — the only moment when the fix is still free. In CI, it catches whatever escaped the hook.
  • Separate per environment: the dev secret is not the prod secret. A compromised dev database must reveal nothing about prod.
  • Leak procedure — order matters: 1) revoke/rotate the secret immediately (that’s what stops the bleeding), 2) check access logs to assess exploitation, 3) clean the git history (git filter-repo, or BFG) and force-push, 4) understand how it happened and add the guardrail (gitleaks, review). Cleaning the history without revoking is pointless: existing clones still have the secret.

The trio of files that structures all of this:

# .gitignore — the .env NEVER enters the repo
.env
.env.*            # .env.local, .env.production…
!.env.example     # exception: the example IS versioned

# .env.example — versioned: the NAMES, dummy values
DATABASE_URL=postgres://user:password@localhost:5432/app
STRIPE_SECRET_KEY=sk_test_xxx
JWT_SECRET=change-me

# .env — local, gitignored: the REAL values
DATABASE_URL=postgres://app:S3cr3t!@localhost:5432/app
STRIPE_SECRET_KEY=sk_live_51Mq...
JWT_SECRET=b1946ac92492d2347c6235b4d2611184

💡 Reflex to show — the .env.example is not a detail: it’s what makes the .env gitignorable without breaking onboarding. cp .env.example .env, fill it in, the app starts. Without it, someone will eventually commit the real .env “so it works for everyone”.

In an interview

“A secret leaked in a pushed commit, what do you do?” — In order: I revoke the secret immediately (new key on the provider’s side) — that’s the critical step, because the secret is already considered compromised. I check the affected service’s access logs to see whether it was exploited. Only then do I clean the history (git filter-repo) and warn the team (force push = re-clone). Finally, I add gitleaks as a pre-commit hook so it doesn’t happen again.

“Why not put secrets in the code if the repo is private?” — A private repo is not a vault: every dev has access (and ex-devs via their clones), third-party integrations too (CI, analysis tools), plus the risk of going public or a stolen laptop. And a secret in the code is coupled to the deployment: impossible to rotate without a re-release. Externalizing the secret also means being able to change it in 30 seconds.

“Environment variables: limitations?” — Visible in /proc/<pid>/environ and ps e (same user), inherited by every child process (dependencies, third-party scripts), often dumped in crash logs or error reporters. They’re a good transport, not a storage: in serious production, the source of truth is a secrets manager that injects them.

“What’s the difference between .env and .env.example?” — .env holds the real values, is gitignored and local to each machine/environment. .env.example is versioned and contains only variable names with fake values: it documents the configuration contract. New dev: cp .env.example .env and fill it in.

“How do you handle secrets in GitHub Actions CI?” — The repo/org encrypted secrets store (Settings → Secrets), referenced via ${{ secrets.MY_KEY }}, injected as env vars at run time and masked in the logs. Never in plain text in the YAML. Bonus: workflows triggered from forks don’t get secrets — by design.

Pitfalls & misconceptions

  • “I deleted it in the next commit, we’re fine” — no: the history keeps everything. Revocation is mandatory; history cleanup comes second.
  • “CI log masking protects the secret” — it masks the exact string: echo $KEY | base64 prints it back in the clear. Masking limits accidents, it doesn’t stop exfiltration.
  • “A .env.production on the server is basically a vault” — better than the repo, but not encrypted, not audited, not rotated. Acceptable for a side project, insufficient as soon as customer data is involved.
  • The frontend has no secrets: anything that ships in the JS bundle (NEXT_PUBLIC_*, VITE_*) is public by definition. A “secret API key” on the client side doesn’t exist — the sensitive call goes through your backend.
  • Secrets in Docker images: a COPY .env or an ARG SECRET stays readable in the image layers (docker history). Use build secrets (--secret) or runtime injection.

🎤 In an interview — “a secret leaked, what do you do?” is THE classic question. The expected answer fits in one motto: revoke first, clean up second. The candidate who starts with “I rewrite the git history” has missed the point: the secret is already out in the wild.

Going further

S'entraîner sur ce sujet → Practice this topic →