Je faisais une revue générale de mon projet, à la recherche de choses oubliées. J'en ai trouvé une dont je me serais bien passé : un fichier de configuration de déploiement contenant la clé qui signe les jetons de connexion, le mot de passe administrateur et le mot de passe d'envoi des e-mails. En clair, dans l'historique git, sur trois branches.
Comment c'est arrivé
Le fichier figurait dans le .gitignore. Le problème, c'est qu'il y a été ajouté après avoir été commité une première fois, et .gitignore n'ignore que les fichiers qui ne sont pas encore suivis : un fichier déjà suivi le reste, modification après modification, même si son nom est dans la liste.
Le dépôt était privé, mais privé ne veut pas dire sûr : les collaborateurs, les services de CI, les sauvegardes et chaque ordinateur sur lequel il a été cloné y ont accès.
Le supprimer de tout l'historique
Supprimer le fichier avec un nouveau commit ne sert à rien : il reste dans tous les commits précédents. Il faut réécrire l'historique de chaque branche. J'ai utilisé git filter-repo sur un clone miroir, puis j'ai forcé le push et vérifié qu'aucun commit ne le contenait plus :
# 1. Stop tracking the file (it stays on disk)
git rm --cached .env.deploy
git commit -m "Stop tracking .env.deploy"
# 2. Rewrite the history of EVERY branch on a mirror clone
git clone --mirror git@github.com:me/my-repo.git
cd my-repo.git
git filter-repo --path .env.deploy --invert-paths
git push --force --mirror
# 3. Verify: no commit on any branch may still contain it
git log --all --oneline -- .env.deploy
Après une telle réécriture, toute personne ayant une copie locale doit la réaligner sur le nouveau dépôt distant : les anciens commits ne doivent plus jamais être poussés.
Pourquoi ça ne suffit pas
C'est la leçon la plus importante. Même après la réécriture, les anciens commits peuvent rester accessibles : GitHub, par exemple, conserve des références internes aux pull requests qu'un push forcé ne peut pas effacer. Et on ne sait pas qui a téléchargé le dépôt par le passé.
Retirer un secret de l'historique évite d'aggraver la situation ; la seule chose qui la règle, c'est de le changer. Concrètement :
- Une nouvelle clé de signature des jetons : elle invalide toutes les sessions existantes, donc tout le monde devra se reconnecter. C'est le prix à payer.
- Un nouveau mot de passe administrateur.
- La révocation du mot de passe d'application de messagerie et la création d'un nouveau.
- La mise à jour des variables dans chaque environnement où elles sont utilisées.
La deuxième fois, le même schéma
Quelques semaines plus tard, j'ai retrouvé le même problème sous une autre forme : un script d'installation du serveur, très pratique, qui écrivait les identifiants directement dans le fichier de configuration. C'est le schéma récurrent : le script de confort qui contient les secrets pour ne rien avoir à configurer. Je l'ai réécrit pour qu'il génère les clés sur le moment et demande les mots de passe à la personne qui l'exécute :
# Before: secrets written into a script tracked by git
# JWT_SECRET=hardcoded-value...
# After: generated or asked at runtime, never committed
JWT_SECRET="$(openssl rand -base64 48)"
read -rsp "SMTP password: " SMTP_PASS; echo
umask 077 # the .env file is readable by its owner only
printf 'JWT_SECRET=%s\nSMTP_PASS=%s\n' "$JWT_SECRET" "$SMTP_PASS" > .env
Les habitudes qui évitent que cela se reproduise
- Un
.env.exampleversionné, avec les noms des variables et aucune valeur ; le vrai.envn'entre jamais dans le dépôt. - Un scanner de secrets avant chaque commit, comme gitleaks dans un hook pre-commit : il bloque le commit avant que le mal soit fait.
- La protection des push proposée par GitHub et GitLab, qui refuse les push contenant des secrets reconnaissables.
- Jamais de secrets dans les scripts : les générer à l'exécution ou les lire dans l'environnement.
- Une vérification périodique de l'historique : un secret que vous trouvez vous-même est un petit incident ; un secret trouvé par d'autres, non.
En résumé
Un secret dans un dépôt doit être considéré comme compromis dès l'instant où vous le trouvez. Réécrire l'historique est nécessaire mais pas suffisant : la vraie correction consiste à changer chaque identifiant exposé. Ensuite, il vaut mieux rendre le problème impossible à répéter, avec des scanners et des règles claires. J'ai rassemblé d'autres pratiques de sécurité dans ce guide.