Aller au contenu principal
own2pwn
Gitleaks : détecter les secrets dans Git (mesure sur des dépôts réels)

Gitleaks : détecter les secrets dans Git (mesure sur des dépôts réels)

Gitleaks scanne l'historique Git à la recherche de clés d'API et de tokens. J'ai lancé le binaire sur des dépôts publics : voici la mesure, la .gitleaks.toml qui coupe le bruit et le hook pre-commit.

Maxime J9 min de lecture

Des secrets qui traînent dans vos dépôts ?

SecAI cherche secrets, dépendances vulnérables et failles de code dans vos pipelines.

Scanner mes secrets

J'ai installé le binaire gitleaks version 8.28.0 et je l'ai lancé sur cinq dépôts open source parmi les plus installés au monde. Résultat brut : 19 alertes. Après revue manuelle des 19, le nombre de vrais secrets de production exposés était de zéro. Dix-sept de ces alertes pointaient une valeur d'exemple dans la documentation ou une constante de test, les deux dernières un certificat de test rangé dans un dossier prévu pour ça. Un scanner de secrets détecte très bien, et c'est le tri du bruit qui fait tout le travail.

On regarde comment gitleaks cherche, ce qu'il trouve sur du code réel, pourquoi la règle générique produit l'essentiel du bruit, et surtout comment écrire une .gitleaks.toml qui fait taire les faux positifs sans jamais cacher une vraie fuite de secrets. Toutes les commandes sont reproductibles, les valeurs sont masquées, et aucun dépôt n'est montré du doigt.

Méthode de la mesure

Corpus : cinq dépôts publics très populaires (langages web courants), agrégés et volontairement non nommés, plus un dépôt de démonstration leaky-repo dont tous les secrets sont faux et plantés exprès. Scans avec --redact=100 (aucune valeur affichée) et classement manuel vrai positif / faux positif fait à la main sur les lignes visibles. Aucun secret réel n'a été exposé ni conservé.

Comment gitleaks cherche un secret

Gitleaks est un binaire Go qui ne fait rien de magique : il lit du texte et le confronte à des règles. Chaque règle combine trois signaux. Un motif (une expression régulière) qui décrit la forme du secret, par exemple le préfixe ghp_ suivi de 36 caractères pour un token GitHub. Un jeu de mots-clés qui pré-filtre les lignes candidates avant même de lancer le regex, pour la vitesse. Et un seuil d'entropie (l'entropie de Shannon) qui mesure le désordre d'une chaîne : un vrai secret ressemble à du bruit aléatoire, un mot du dictionnaire non.

Le jeu de règles par défaut couvre une petite centaine de fournisseurs (AWS, Stripe, Slack, clés SSH, JWT). Quand aucune règle spécifique ne colle, une règle attrape-tout nommée generic-api-key prend le relais : elle cherche une variable au nom évocateur (key, token, secret) assignée à une chaîne à forte entropie. C'est elle qui rattrape ce que les autres ratent, et c'est aussi elle qui génère la quasi-totalité du bruit, on va le voir.

Deux commandes séparent les deux façons de scanner. Le choix entre les deux décide de ce que vous voyez.

bash
# gitleaks 8.28.0 (binaire officiel, releases GitHub)
gitleaks version           # => 8.28.0

# Analyser l'arbre de travail : uniquement les fichiers PRESENTS
gitleaks dir ./mon-repo --redact=100 --no-banner \
  --report-format json --report-path rapport.json

# Rejouer tout l'historique via 'git log -p' : voit aussi les secrets SUPPRIMES
gitleaks git ./mon-repo --redact=100 --no-banner \
  --report-format json --report-path histoire.json

--redact=100 masque intégralement la valeur trouvée dans le rapport : on garde le nom de la règle, le fichier et la ligne, jamais la clé. C'est la première chose à câbler quand un rapport gitleaks doit atterrir dans un ticket ou un log de CI, sinon on recopie le secret là où il ne devrait pas être. Les commandes detect et protect des anciennes versions existent toujours mais sont masquées de l'aide : sur une version récente, on écrit gitleaks git et gitleaks dir.

La mesure : gitleaks sur des dépôts publics, sans réglage

Voici l'agrégat des cinq dépôts réels, scannés avec le jeu de règles par défaut, sur l'arbre de travail. Chaque ligne du tableau est une famille de règle, pas un dépôt : le résultat est volontairement anonyme.

mesure-brute
RègleAlertesVerdict après revue manuelle
generic-api-key17Des valeurs d'exemple tirées de la documentation et des constantes de test.
private-key2Des certificats de test rangés dans tests/ et testdata/.
Total19Aucun secret de production n'était réellement exposé.
Cinq dépôts publics, gitleaks 8.28.0, jeu de règles par défaut. 19 alertes, 0 vrai secret de production après revue manuelle.

Les dix-sept alertes generic-api-key se répartissent ainsi : une valeur de SECRET_KEY montrée dans la documentation officielle d'un framework, répétée cinq fois dans plusieurs pages ; une chaîne base64 qui est en réalité le nonce d'exemple du handshake WebSocket, celui gravé dans la RFC 6455 (il se décode en "the sample nonce") ; une variable Go littéralement nommée key et assignée à un libellé anodin ; des placeholders de test évidents ; et jusqu'à une chaîne de fuseau horaire attrapée au vol. Rien d'exploitable. Les deux alertes private-key, elles, sont de vraies clés RSA, mais des certificats de test rangés dans tests/ et testdata/ : du matériel cryptographique réel, intentionnel, sans rapport avec un secret de production.

D'abord, sur du code open source mûr et relu, la densité de vrais secrets est basse : ces projets ont fait leur travail. Ensuite, la précision de generic-api-key sur ce genre de code tombe à zéro : 17 alertes, 17 non-événements. Ce n'est pas un défaut de gitleaks, c'est la contrepartie d'une règle attrape-tout. Un scanner qui ne lèverait aucune de ces 17 alertes raterait aussi de vrais secrets ailleurs. Le curseur se règle après coup, avec une allowlist.

Pourquoi le bruit vient toujours des mêmes endroits

Regardez où atterrissent les faux positifs : documentation, dossiers de tests, fichiers de fixtures, certificats .pem d'exemple. Ce sont les zones d'un dépôt où l'on écrit des valeurs qui ressemblent à des secrets sans en être. Un tutoriel montre une clé d'API pour illustrer ; un test unitaire a besoin d'un token bidon pour exercer un parseur ; une suite HTTP embarque une clé privée jetable pour monter un serveur TLS local. Tout ça est légitime, et tout ça déclenche l'entropie.

tri-secret
Entrée
Chaîne à forte entropie repérée
generic-api-key lève une alerte sur key, token ou secret + valeur désordonnée.
Cas 1
Docs, tests, fixtures
Placeholder ou constante de test. Faux positif : à mettre en allowlist par chemin.
Cas 2
Certificat .pem de test
Vraie clé, mais jetable et intentionnelle. Bruit : allowlist par extension.
Cas 3
Code applicatif, config, .env
Vrai secret : à révoquer et sortir de l'historique. C'est la seule alerte qui compte.
Le même signal d'entropie élevée sort trois choses très différentes. La règle les traite pareil ; c'est à vous de trancher.

L'idée d'une bonne configuration n'est donc pas de désactiver la règle bruyante, ce serait jeter le bébé, mais de dire à gitleaks où le bruit est attendu. On garde la détection partout, on la neutralise seulement dans les zones non applicatives. C'est le rôle des allowlists par chemin.

La .gitleaks.toml qui coupe le bruit sans cacher les vrais secrets

Voici la configuration que j'ai réellement testée sur le corpus. Elle étend le jeu de règles par défaut (useDefault = true), ajoute deux allowlists globales et une règle maison. La syntaxe suit le format courant de gitleaks 8.28 : [[allowlists]] au pluriel, avec une condition, des paths et des stopwords.

toml
title = "own2pwn baseline"

[extend]
useDefault = true   # herite du jeu de regles standard (~une centaine)

# Le bruit vient presque toujours des memes endroits : docs, tests, fixtures.
[[allowlists]]
description = "Chemins non applicatifs"
condition   = "OR"
paths = [
  '''(^|/)docs?/''',
  '''(^|/)(test|tests|testdata|__tests__|spec|fixtures?)/''',
  '''\.(rst|md|txt)$''',
  '''(^|/)[^/]*\.pem$''',           # certificats de test
  '''_test\.(go|py|js|ts|rb)$''',    # convention Go/JS : suffixe, pas dossier
]

# Placeholders evidents, quel que soit le fichier. regexTarget = "match"
# fait porter le stopword sur le texte matche, pas sur le chemin.
[[allowlists]]
description = "Placeholders"
condition   = "OR"
regexTarget = "match"
stopwords   = ["example", "dummy", "changeme", "placeholder", "sample", "test"]

# Une regle maison : detecte un token interne au format prj_live_<32 alphanum>.
[[rules]]
id          = "own2pwn-internal-token"
description = "Token interne own2pwn"
regex       = '''prj_live_[0-9a-zA-Z]{32}'''
keywords    = ["prj_live_"]

L'effet, mesuré : sur les cinq dépôts réels, les alertes passent de 19 à 1. La seule qui survit est un paramètre de fonction nommé token dans un script utilitaire, un cas qu'on éteint d'un commentaire #gitleaks:allow en fin de ligne, ou d'un stopword supplémentaire. Contre-épreuve indispensable : la même configuration appliquée au dépôt de secrets plantés ne fait tomber que 1 alerte sur 22. Autrement dit, l'allowlist retire le bruit et laisse les 21 vrais secrets plantés bien visibles, parce qu'ils vivent dans des fichiers de credentials, pas dans docs/. C'est le seul test qui valide une allowlist : vérifier qu'elle ne rend pas aveugle.

Un détail piège que la mesure a sorti : une allowlist par nom de dossier tests/ ne suffit pas partout. En Go, un fichier de test s'appelle xxx_test.go et vit à côté du code, pas dans un dossier dédié. Sans la ligne _test\.(go|py|...)$, trois faux positifs restaient.

Le hook pre-commit : bloquer avant que le secret parte

Gitleaks sait scanner l'index Git juste avant qu'un commit parte, avec le drapeau --staged. Deux façons de le câbler. La plus propre passe par le framework pre-commit, qui gère l'installation du binaire et la version :

yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.28.0
    hooks:
      - id: gitleaks   # scanne l'index a chaque 'git commit'

Si vous n'utilisez pas pre-commit, un hook Git natif fait le même travail. On le pose dans .git/hooks/pre-commit (ou, mieux, dans un dossier versionné pointé par core.hooksPath pour qu'il suive l'équipe) :

bash
#!/usr/bin/env bash
# .git/hooks/pre-commit  (chmod +x)
# Bloque le commit si un secret est detecte dans l'index.
if ! gitleaks git --staged --redact=100 --no-banner; then
  echo "Secret detecte : commit refuse. Corrigez, ou #gitleaks:allow si faux positif."
  exit 1
fi

Le code de sortie fait tout : gitleaks renvoie 1 quand il trouve quelque chose, ce qui interrompt le commit. Pour lever un faux positif ponctuel sans toucher la config, on annote la ligne fautive d'un #gitleaks:allow, ou on enregistre son empreinte dans un fichier .gitleaksignore. Ce garde-fou local ne remplace pas un scan côté CI : un développeur peut toujours passer outre avec --no-verify. Les deux couches se cumulent, comme dans une chaîne d'analyse de code bien conçue.

Scanner l'historique, pas seulement l'arbre courant

Sur le dépôt de démonstration, gitleaks dir (arbre de travail) a relevé 22 secrets ; gitleaks git (historique complet) en a relevé 26. Les quatre de différence avaient été supprimés d'un fichier dans un commit ultérieur, mais restaient gravés dans un ancien diff. Toute personne qui clone le dépôt les récupère avec le reste de l'historique.

worktree-vs-histoire
Ce que fait le dépôt
Deux commits
  1. Le commit A ajoute clef.env et y écrit le secret S, qui entre dans l'historique.
  2. Le commit B supprime clef.env, mais le secret S reste gravé dans le commit A.
Ce que voit gitleaks
Deux commandes
  1. gitleaks dir ne voit pas S, puisque le fichier est absent de l'arbre de travail d'aujourd'hui.
  2. gitleaks git voit S, parce qu'il relit le diff du commit A.
Un secret retiré de l'arbre de travail survit dans l'historique. Le supprimer ne le révoque pas.

Supprimer un secret ne le révoque pas

Le premier réflexe après une fuite est souvent de retirer la clé dans un nouveau commit. C'est inutile : la valeur reste lisible dans l'historique, et surtout elle a peut-être déjà été clonée, indexée ou collectée. La seule action qui compte est de révoquer le secret côté fournisseur et d'en générer un nouveau. Réécrire l'historique (filter-repo, BFG) nettoie le dépôt, mais ne rend jamais mort un token déjà parti ailleurs. Le sujet rejoint celui, plus large, des fuites de données.

En pratique : gitleaks git une fois sur tout l'historique pour l'état des lieux (et la liste des secrets à révoquer), puis --staged en pre-commit pour que le problème n'augmente plus. C'est la même logique de fond que le traitement d'une gestion des vulnérabilités sérieuse : on arrête l'hémorragie, puis on remédie l'existant dans l'ordre.

Gitleaks, trufflehog ou detect-secrets ?

Trois outils reviennent dès qu'on parle de secret scanning, et ils ne jouent pas le même rôle. Gitleaks détecte par motif et entropie : rapide, sans dépendance, idéal en pre-commit et en CI. Son angle mort assumé, c'est qu'une alerte ne dit pas si la clé est encore active.

TruffleHog répond à ça : pour chaque secret qu'il sait classer, il tente de s'authentifier auprès du fournisseur pour confirmer si la clé est vivante, et annonce plus de 800 types de détecteurs avec vérification. Un secret "verified" est une urgence au même titre qu'un mot de passe compromis encore actif, un secret non vérifié attend. Cette information de vivacité change l'ordre de la remédiation. En contrepartie, la vérification interroge des API externes, ce qui a un coût et des implications réseau.

detect-secrets (Yelp) vise un troisième objectif : la prévention plutôt que l'archéologie. Son fichier de baseline acte les secrets déjà présents pour se concentrer sur les nouveaux, avec un workflow d'audit qui trie vrais et faux positifs. C'est adapté aux gros dépôts hérités où tout remédier d'un coup est illusoire. Dit autrement : gitleaks pour bloquer et scanner l'historique, un outil à vérification pour prioriser, un outil à baseline pour tenir un legacy. Ils se cumulent bien plus qu'ils ne se remplacent.

À retenir

  • Gitleaks détecte très bien, le bruit est le vrai sujet : sur cinq dépôts publics mûrs, 19 alertes par défaut, 0 secret de production après revue. La règle generic-api-key porte l'essentiel des faux positifs.
  • Une .gitleaks.toml bien réglée divise le bruit : allowlists par chemin (docs, tests, .pem, _test.go) et stopwords de placeholder ont fait passer les alertes de 19 à 1, sans masquer les secrets réellement plantés (22 à 21 sur le dépôt de test).
  • Toujours vérifier qu'une allowlist ne rend pas aveugle : on la teste contre un corpus où l'on sait qu'il y a des secrets.
  • Scanner l'historique, pas que l'arbre courant : gitleaks git a vu 26 secrets contre 22 pour gitleaks dir. Un secret supprimé reste dans l'historique.
  • Supprimer un secret ne le révoque pas : la seule réponse correcte est la rotation côté fournisseur.
  • Le pre-commit arrête l'hémorragie : gitleaks git --staged refuse le commit, à doubler d'un scan CI puisqu'un hook local se contourne.

Un scanner en pre-commit protège vos dépôts. Il ne dit rien des clés qui traînent déjà ailleurs : dans un dépôt public d'un ancien prestataire, un gist oublié, un job de CI verbeux, tout ce qu'une reconnaissance OSINT sait remonter. Cette surface-là se surveille en continu, et c'est le rôle de la détection de secrets exposés de la plateforme EASM own2pwn, qui recoupe fuites publiques et actifs pour pointer ce qui vous appartient. Côté code, l'analyse SAST de SecAI cherche secrets, dépendances vulnérables et failles dans vos pipelines. Et si vous préférez qu'un humain regarde votre périmètre en conditions réelles, le pentest web blackbox est fait pour ça. Pour discuter du contexte, la page contact répond sous 24 h.

Questions fréquentes sur gitleaks

Qu'est-ce que gitleaks ?

Gitleaks est un scanner de secrets open source écrit en Go. Il parcourt un dépôt Git, ou un simple dossier, à la recherche de clés d'API, de tokens et de clés privées, à l'aide d'expressions régulières, d'un seuil d'entropie et de mots-clés. Il tourne en local, en pre-commit ou en CI, et sort son verdict au format JSON, CSV ou SARIF.

Comment lancer gitleaks sur un dépôt ?

Deux commandes principales. gitleaks dir analyse les fichiers présents dans l'arbre de travail, sans regarder l'historique. gitleaks git rejoue tout l'historique via git log -p, donc il voit aussi les secrets supprimés depuis. On ajoute --redact=100 pour ne jamais afficher la valeur du secret, et --report-format json pour un rapport exploitable.

Gitleaks scanne-t-il l'historique Git ?

Oui, et c'est son intérêt principal. La commande gitleaks git lit chaque diff de chaque commit. Sur un dépôt de test, j'ai relevé 26 secrets dans l'historique contre 22 dans l'arbre courant : les quatre de différence avaient été retirés d'un fichier mais restaient inscrits dans un vieux commit, donc toujours exposés à qui clone le dépôt.

Comment réduire les faux positifs de gitleaks ?

Par un fichier .gitleaks.toml qui garde useDefault mais ajoute des allowlists. On exclut les chemins non applicatifs (docs, tests, fixtures, fichiers .pem de certificats de test) et on liste des stopwords de placeholder comme example ou changeme. Sur mon corpus, cette config a fait passer les alertes de 19 à 1 sans masquer le moindre secret réellement planté.

Gitleaks ou trufflehog : lequel choisir ?

Gitleaks détecte par motif et entropie, il est rapide et facile à câbler en pre-commit. TruffleHog va plus loin en vérifiant activement chaque secret classé : il tente de s'authentifier auprès du fournisseur pour savoir si la clé est encore vivante. Les deux sont complémentaires : gitleaks pour bloquer à la source, un scanner à vérification pour prioriser la rotation.

Un secret supprimé d'un commit est-il vraiment parti ?

Non. Supprimer une clé dans un nouveau commit la laisse intacte dans l'historique : n'importe qui peut la retrouver avec git log ou gitleaks git. La seule réponse correcte est de considérer le secret comme compromis et de le révoquer côté fournisseur. Réécrire l'historique aide à nettoyer, mais ne rend jamais valide un token déjà cloné ailleurs.

Veille sécurité

La suite, une fois par mois

Ce qui bouge vraiment sur la surface d'attaque externe, NIS2 et la sécurité applicative, écrit par le pentester qui signe ces articles. Un envoi par mois, désinscription en un clic.

Votre adresse ne sert qu'à cet envoi. Voir la politique de confidentialité.