- Sécurité
- Git
- Secrets
Secrets exposés dans l’historique Git : détecter, révoquer, prévenir
Supprimer une clé d’API commitée ne l’efface pas de l’historique Git. Détecter les fuites, révoquer d’abord, réécrire l’historique et éviter la prochaine.
· 7 min de lecture · Lina Source LLC
Quelqu’un commite un fichier .env, ou colle une vraie clé d’API dans un module de configuration pour tester rapidement quelque chose. Un relecteur s’en aperçoit, la clé est supprimée au commit suivant, et tout le monde passe à autre chose. Pourtant, la clé est toujours là. Git conserve chaque version de chaque fichier, et quiconque peut cloner le dépôt peut lire le commit qui l’a ajoutée.
Les identifiants codés en dur sont répertoriés sous CWE-798. Ce guide explique quoi faire lorsqu’un secret s’est déjà retrouvé dans l’historique : trouver tous les secrets, les révoquer avant toute autre chose, décider si la réécriture de l’historique en vaut la peine, et mettre en place les garde-fous qui empêcheront la prochaine fuite.
Pourquoi supprimer la ligne ne suffit pas
Un commit qui retire un secret ajoute un nouvel instantané qui ne le contient plus. L’instantané précédent, avec le secret, reste référencé par l’historique de la branche. git log -p l’affiche, un git checkout de l’ancien commit le restaure, et chaque clone ou fork existant en possède déjà une copie. Squasher la branche avant le merge n’aide pas non plus si les commits d’origine ont déjà été poussés : le dépôt distant peut encore les conserver, et quiconque les a récupérés les a en local.
Sur un dépôt public, partez du principe que le secret a été vu. Des scrapers automatisés surveillent les push publics à la recherche de motifs d’identifiants, et le délai entre un push et le premier abus peut être très court. Sur un dépôt privé, l’exposition est moindre mais pas nulle : chaque employé, prestataire, système de CI et intégration disposant d’un accès en lecture l’a entre les mains.
Étape 1 : révoquer d’abord
La rotation est la seule étape qui supprime réellement le risque. Réécrire l’historique cache le secret aux futurs clones ; cela ne change rien aux copies qui existent déjà. Révoquez et remplacez donc l’identifiant avant de toucher au dépôt.
Planifiez la rotation pour éviter une interruption de service. De nombreux fournisseurs permettent d’avoir deux clés valides en même temps : créez la nouvelle clé, déployez-la partout où l’ancienne était utilisée, vérifiez que le trafic a basculé, puis révoquez l’ancienne. Si le fournisseur n’autorise qu’une seule clé, acceptez une brève interruption : quelques minutes d’indisponibilité valent mieux que de laisser active une clé dont on sait qu’elle a fuité le temps d’orchestrer une bascule parfaite.
- Générez une nouvelle clé dans la console du fournisseur et déployez-la par votre circuit de configuration habituel.
- Révoquez l’ancienne clé. Ne vous contentez pas de cesser de l’utiliser : une clé valide inutilisée reste une clé valide.
- Consultez les journaux d’audit du fournisseur pour repérer toute activité avec l’ancienne clé depuis la date du commit : appels d’API, nouveaux utilisateurs, permissions modifiées, facturation inattendue.
- Si le secret était un mot de passe de base de données ou une clé de signature, réfléchissez à ce qu’il aurait pu déverrouiller. Un secret de signature JWT exposé signifie que n’importe quel jeton a pu être forgé : invalidez les sessions existantes.
- Consignez ce qui a été exposé, pendant combien de temps et ce que vous avez modifié. Vous en aurez besoin si un client ou un auditeur pose la question.
Étape 2 : trouver tout ce qui a fuité
Là où il y a un secret commité, il y en a souvent d’autres. Fouillez tout l’historique, pas seulement l’arborescence actuelle, et incluez chaque branche et chaque tag.
# Commits that added or removed a string anywhere in history
git log -p -S "sk_live_" --all
# Regex search across the diffs of all commits
git log -p -G "AKIA[0-9A-Z]{16}" --all
# Files that ever existed at a suspicious path
git log --all --oneline -- .env config/production.json
# Dedicated scanners check hundreds of known credential formats
gitleaks git -v .
trufflehog git file://. --only-verifiedgitleaks et trufflehog sont des scanners open source conçus pour cette tâche. Ils connaissent les formats des clés des fournisseurs courants, et trufflehog peut vérifier si un identifiant trouvé est toujours actif. Les anciennes versions de gitleaks utilisent gitleaks detect --source . au lieu de la sous-commande git. Lancez un scanner une fois sur tout l’historique, puis laissez-le tourner sur les nouveaux commits. Attendez-vous à quelques faux positifs, comme des fixtures de test et des clés d’exemple dans la documentation. Examinez chacun d’eux, puis enregistrez les faux positifs confirmés dans la liste d’autorisation ou le fichier de référence du scanner, afin que la prochaine exécution n’affiche que les nouveaux résultats.
N’oubliez pas les emplacements autour du dépôt : les logs de CI qui ont affiché des variables d’environnement, les commentaires d’issues et de pull requests, les pages de wiki, les couches d’images Docker construites à partir du dépôt, et les gists ou pastes partagés pendant un débogage.
Étape 3 : décider s’il faut réécrire l’historique
Une fois le secret révoqué, il est inutilisable : réécrire l’historique relève donc du nettoyage, pas du confinement. Cela reste utile lorsque le dépôt est public, lorsque le secret révèle autre chose que lui-même (un nom d’hôte interne, le nom d’un client) ou lorsque la conformité l’exige. Les coûts sont réels : chaque collaborateur doit recloner, les pull requests ouvertes sont perturbées et les hachages de commits changent. Tout ce qui référence un commit par son hachage, comme les notes de version, les historiques de déploiement, les liens d’issues ou les dépendances épinglées dans d’autres dépôts, pointera vers des commits qui n’existent plus sur la branche. Annoncez la réécriture à l’avance, choisissez un moment calme, et fusionnez ou fermez d’abord les pull requests ouvertes.
git filter-repo est l’outil recommandé par le projet Git pour cette opération, en remplacement de l’ancien git filter-branch. Il travaille sur un clone neuf et supprime le remote origin par mesure de sécurité : vous devez donc le rajouter avant de pousser.
# Start from a fresh clone
git clone [email protected]:acme/api.git api-clean
cd api-clean
# Option A: remove a file from every commit
git filter-repo --invert-paths --path config/production.env
# Option B: replace secret strings everywhere they appear
# replacements.txt contains one rule per line, for example:
# PASTE_THE_OLD_KEY_HERE==>REDACTED
git filter-repo --replace-text ../replacements.txt
# filter-repo removes origin; add it back and force-push
git remote add origin [email protected]:acme/api.git
git push --force --all
git push --force --tagsCe que la réécriture ne peut pas faire
- Elle ne peut pas atteindre les clones, forks, caches de CI ou sauvegardes existants. Quiconque a récupéré le dépôt avant la réécriture conserve les anciens commits.
- Sur GitHub, les références créées pour les pull requests sont en lecture seule et peuvent garder d’anciens commits accessibles. Supprimer les vues en cache et ces références nécessite de contacter le support GitHub.
- Les collaborateurs qui font un pull ou un merge depuis un ancien clone peuvent repousser directement l’ancien historique. Demandez à chacun de recloner, et protégez la branche pendant ce temps.
- Elle ne retire pas le secret des autres endroits où il a été copié : logs, tickets, messages de chat ou artefacts de build.
Voilà pourquoi l’ordre compte. Révoquez, puis nettoyez. Un historique réécrit avec une clé encore active donne un faux sentiment de sécurité. Après le push, lancez votre scanner sur un clone neuf pour confirmer que le secret a vraiment disparu de chaque branche et de chaque tag.
Étape 4 : empêcher la prochaine fuite
L’objectif est de rendre difficile le commit d’un secret et rapide sa détection. Aucun contrôle ne fait les deux à lui seul : superposez-les.
Tenir les secrets à l’écart du code
Lisez les identifiants depuis des variables d’environnement ou un gestionnaire de secrets à l’exécution, et vérifiez au démarrage que ceux dont vous avez besoin sont bien présents. Commitez un fichier .env.example avec des valeurs fictives et ajoutez .env au .gitignore dès le premier commit. En production, un gestionnaire de secrets comme AWS Secrets Manager, Google Secret Manager, HashiCorp Vault ou les variables d’environnement chiffrées de votre plateforme vous apporte contrôle d’accès, journaux d’audit et rotation facilitée.
Bloquer les secrets avant le commit
Un hook pre-commit intercepte l’erreur sur la machine du développeur, avant qu’elle n’atteigne le dépôt distant. Le framework pre-commit réduit cela à quelques lignes de configuration que chaque contributeur peut installer.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: vX.Y.Z # pin to the latest release tag
hooks:
- id: gitleaks
# Install once per clone
# pip install pre-commit
# pre-commit installLes hooks sont locaux et peuvent être contournés : exécutez donc le même scanner en CI à chaque push, comme filet de sécurité. Sur GitHub, activez le secret scanning et la push protection lorsque votre offre les inclut ; la push protection rejette côté serveur les push contenant des jetons de fournisseurs reconnus.
Limiter les dégâts des fuites qui vous échappent
- Limitez chaque clé aux permissions minimales et, lorsque le fournisseur le permet, à des adresses IP ou des référents précis.
- Privilégiez les identifiants de courte durée, comme la fédération OIDC entre votre CI et votre fournisseur cloud, plutôt que des clés statiques de longue durée.
- Utilisez des clés distinctes par environnement, afin qu’une clé de test exposée ne puisse pas toucher la production.
- Tenez à jour une procédure de rotation pour chaque secret critique, afin qu’une rotation sous pression soit une routine et non une improvisation.
La place de la revue de code
Les scanners reconnaissent bien les formats connus ; ils sont plus faibles sur le contexte, comme un mot de passe assemblé à partir de deux constantes ou un identifiant par défaut livré dans une valeur de repli de la configuration. Une relecture du code détecte ces cas. CodeAuditAgent signale les identifiants codés en dur sous CWE-798, preuve citée à l’appui, lorsqu’il audite la branche par défaut d’un dépôt public ou un extrait de code collé. Il lit le code actuel, pas l’historique des commits : gardez donc un scanner d’historique pour les commits passés.
En résumé : un secret commité est un secret exposé. Révoquez-le aujourd’hui, nettoyez l’historique si cela vaut la perturbation, et faites en sorte que la prochaine fuite échoue au hook pre-commit plutôt que sur une page publique.