Une clé d’API poussée par mégarde dans un dépôt public, un mot de passe de base de données figé en dur dans le code, une variable « publique » qui se retrouve dans le bundle JavaScript livré au navigateur : la plupart des fuites de secrets ne viennent pas d’une attaque sophistiquée, mais d’une configuration mal rangée. Séparer la configuration du code reste l’un des gestes les plus rentables du développement web, et l’un des plus mal outillés dans les projets qui grossissent vite.
Pourquoi sortir la configuration du code
Le principe est ancien et tient en une phrase : le code décrit ce que fait l’application, la configuration décrit où et comment elle tourne. Une même base de code doit pouvoir démarrer en local, en préproduction et en production sans qu’une seule ligne change — seules les valeurs d’environnement diffèrent. C’est la recommandation formalisée par la méthodologie Twelve-Factor App, et elle a une conséquence directe sur la sécurité : ce qui vit dans l’environnement ne vit pas dans l’historique Git.
La distinction utile n’est pas « secret » contre « non-secret », mais « ce qui change selon l’environnement ». Une URL d’API de préproduction n’est pas confidentielle, mais elle a sa place dans la configuration au même titre qu’un jeton d’accès. Le durcissement d’un serveur, abordé ailleurs sur le socle minimal WordPress, commence d’ailleurs par ce même réflexe : aucun identifiant en clair dans un fichier versionné.
Le fichier .env et la règle du .gitignore
La convention dominante est le fichier .env à la racine du projet : une liste de paires CLÉ=valeur, une par ligne. Sa force est sa simplicité ; son piège est qu’il est trivial de le committer par accident. La première ligne à écrire dans un projet n’est pas une variable, c’est l’exclusion du fichier :
# .gitignore — à faire AVANT le premier commit
.env
.env.*
!.env.exampleLe fichier réellement versionné est un .env.example qui documente les clés attendues sans leurs valeurs. Un nouveau membre de l’équipe le copie en .env et le remplit. Le contenu type ressemble à ceci :
# .env.example — versionné, sans secrets
APP_ENV=local
APP_DEBUG=true
DATABASE_URL=
STRIPE_SECRET_KEY=
MAIL_DSN=Le préfixe qui expose au navigateur. Les outils de build front (Vite, Next.js) n’injectent dans le code client que les variables portant un préfixe convenu : VITE_ pour Vite, NEXT_PUBLIC_ pour Next.js. Tout ce qui porte ce préfixe est public : il finit lisible dans le bundle téléchargé par chaque visiteur. Une clé secrète ne doit jamais porter ce préfixe, sous peine d’être servie au monde entier.
Charger les variables selon la stack
Chaque écosystème a sa manière de lire l’environnement. Les trois plus courants côté web se ressemblent dans l’intention mais diffèrent dans les détails.
Côté PHP, Laravel lit automatiquement le .env au démarrage et expose les valeurs via l’aide env(). Un point mérite attention : dès que la configuration est mise en cache en production (php artisan config:cache), env() ne renvoie plus rien en dehors des fichiers de configuration. La valeur doit donc être lue via config(), pas via env(), partout ailleurs dans l’application :
// config/services.php
return [
'stripe' => [
'secret' => env('STRIPE_SECRET_KEY'),
],
];
// Dans le code applicatif : config(), jamais env()
$key = config('services.stripe.secret');Côté Node.js, la lecture d’un fichier .env ne nécessite plus de dépendance externe : le drapeau natif --env-file suffit, et les valeurs arrivent dans process.env. Le même mécanisme se retrouve dans un déploiement en production, au fil des évolutions récentes du runtime.
# Chargement natif, sans paquet supplémentaire
node --env-file=.env server.js// server.js — les valeurs sont dans process.env
const dsn = process.env.DATABASE_URL;
if (!dsn) {
throw new Error("DATABASE_URL manquante : vérifier le fichier .env");
}Côté build front, Vite n’expose au client que le sous-ensemble préfixé et le rend accessible via import.meta.env. La frontière est nette : le serveur voit tout, le navigateur ne voit que le préfixe public.
// Accessible dans le navigateur — donc PUBLIC
const apiBase = import.meta.env.VITE_API_URL;Comparer les méthodes de stockage
Le fichier .env couvre le cas du poste de développement. Il montre ses limites dès qu’il s’agit de partager des secrets en équipe ou de les faire tourner régulièrement. Le tableau suivant situe chaque approche.
| Méthode | Portée | Secrets sensibles | Partage en équipe |
|---|---|---|---|
Fichier .env | Poste local, un projet | Acceptable si hors dépôt | Manuel, fragile |
| Variables système / shell | Machine ou session | Oui | Aucun |
direnv (fichier .envrc) | Par dossier, automatique | Comme .env | Manuel |
| Gestionnaire de secrets (Vault, 1Password, SOPS) | Équipe, multi-projets | Oui, chiffré et audité | Centralisé, traçable |
direnv : charger et décharger automatiquement
Passer d’un projet à l’autre en réexportant des variables à la main est une source d’erreurs. L’outil direnv résout ce problème en chargeant les variables d’un dossier dès qu’on y entre dans le terminal, et en les déchargeant dès qu’on en sort. Le fichier .envrc peut d’ailleurs déléguer au .env déjà présent :
# .envrc — chargé automatiquement à l'entrée du dossier
dotenv .env
export PATH="$PWD/bin:$PATH"
# Autorisation explicite (sécurité de direnv) :
# direnv allowCette approche complète bien un poste de travail déjà équipé d’outils en ligne de commande bien choisis : les secrets suivent le contexte, sans variable qui traîne d’un projet sur l’autre.
Un secret qui n’est jamais entré dans l’historique Git n’a pas besoin d’être révoqué en urgence.
Rotation, partage et production
Trois réflexes distinguent un projet mûr. D’abord la rotation : une clé compromise, ou simplement ancienne, se remplace, et l’application doit tolérer ce changement sans redéploiement lourd. Ensuite le partage : au-delà de deux ou trois personnes, l’échange de .env par messagerie devient ingérable et dangereux — un gestionnaire de secrets centralise l’accès et laisse une trace de qui a lu quoi. Enfin la production : les valeurs y sont injectées par la plateforme d’hébergement ou l’orchestrateur, jamais par un fichier committé, et le principe du moindre privilège s’applique clé par clé.
Le cas d’une fuite mérite une procédure écrite d’avance : révoquer la clé exposée côté fournisseur, en générer une neuve, purger la valeur de l’historique si elle y est entrée, et vérifier les journaux d’accès. Une clé publiée sur un dépôt public doit être considérée comme compromise dans la minute, même si le dépôt est repassé en privé aussitôt : les robots d’indexation scrutent les commits en continu.
Ce qu’il faut retenir
La gestion des secrets ne demande pas d’outil exotique pour commencer : un .gitignore écrit en premier, un .env.example versionné, et une lecture faite par la couche prévue à cet effet dans chaque framework couvrent l’essentiel des projets. La sophistication — direnv, gestionnaire de secrets, chiffrement des fichiers — s’ajoute quand l’équipe grandit ou que les secrets se multiplient. Le seul geste non négociable reste le premier : rien de sensible dans l’historique Git.
Sur mes projets, la règle que je fais respecter en premier n’est pas technique mais rituelle : le .gitignore et le .env.example sont écrits avant la moindre ligne de code métier. J’ai vu trop de clés Stripe et de mots de passe SMTP fuiter parce que le .env avait été committé « juste une fois » au début du projet, quand personne n’y prêtait attention. La rotation systématique après un départ d’équipe m’a aussi évité plus d’une mauvaise surprise. — Simon Janvier
Pour aller plus loin : le facteur Config de la méthodologie Twelve-Factor App, référence fondatrice sur la séparation entre code et configuration.
