A developer "temporarily" commits a password to run a test. They remove it in the next commit. The problem: Git history keeps everything. Three years later, an attacker scans public repositories and finds that secret, still valid.
But Git is only the starting point. Once pushed, the secret replicates into clones, forks, CI runners, artifacts, backups and analysis systems. This guide covers the three layers you need: prevent, detect and respond.
What you will learn
- Why deleting a commit is not enough, and what is truly irreversible
- How the ecosystem around Git multiplies the leak surface
- The detection tools (Gitleaks, TruffleHog) and push protection
- The prevention measures (hooks, CI, forge)
- How to respond and clean the history without fooling yourself
Prerequisites
The secrets management hub (in French) places this guide among the others: static secrets, lifecycle, machine identity and vaults.
Why deleting a commit is not enough
Git history is immutable (by default)
When you commit a secret and then delete it:
# Commit 1: add the secretgit add .envgit commit -m "Add config"
# Commit 2: remove the secretgit rm .envgit commit -m "Remove config"The file no longer appears in HEAD, but it still exists in the history:
# The secret is still accessiblegit show HEAD~1:.env# DB_PASSWORD=SuperSecret123Even after a rebase or a force push
# Attempt to rewrite historygit rebase -i HEAD~2# Drop the commit that contains the secretgit push --forceA force push rewrites the history of the remote repository, but it does not erase the secret everywhere:
- Existing clones keep the old history
- Forks keep the old history
- Backup systems keep the old history
- CI caches potentially keep the history
The reflog keeps everything (locally)
Even after a rebase, the local reflog keeps a trace:
git reflog# abc1234 HEAD@{0}: rebase: ...# def5678 HEAD@{1}: commit: Add config # ← The secret is still thereBy default, Git keeps reflog entries for 90 days, and 30 days for
those pointing to commits that have become unreachable, such as the commit
dropped by a rebase (gc.reflogExpire and gc.reflogExpireUnreachable).
On the other developers' machines, the old history stays intact.
Git is only the starting point of the leak
A committed secret does not disappear with a simple git rm. Git history is
distributed: every clone, every fork keeps a complete copy. But the
problem goes well beyond the repository itself.
Once the secret is pushed, it can be replicated into:
- developer clones (each one holds a complete copy)
- forks (including those of former collaborators)
- CI/CD runners (which clone the repository on every pipeline)
- build caches (Docker layers, npm cache, etc.)
- pipeline artifacts (logs, reports, archives)
- forge backups (GitHub, GitLab, Gitea)
- mirrors and automatic replicas
- analysis systems connected to the repository (SonarQube, Snyk, etc.)
- temporary indexing by the forge (GitHub search, events API)
| Surface | Persistence | Cleanable? |
|---|---|---|
| Git history (origin repository) | Permanent | Yes, with git-filter-repo |
| Developer clones | Until re-cloned | Requires coordination |
| Forks | Permanent | No, outside your control |
| CI/CD runners | Job duration (or cache) | Manual purge |
| Pipeline artifacts | Depends on retention | Manual deletion |
| Forge mirrors / backups | Variable | Depends on the platform |
| Temporary indexing | A few hours | Automatic |
| Third-party analysis systems | Variable | Depends on the service |
The Cleanable? column separates what is irreversible (forks, external clones) from what you can still control.
Who scans public repositories?
Two kinds of actors need to be told apart:
Defensive detection (to protect you):
- GitHub Secret Scanning: detects known patterns after the commit and alerts the maintainer
- GitHub Push Protection: blocks the push before it enters the repository (see the dedicated section)
- Security researchers: scans with TruffleHog or Gitleaks
Opportunistic exploitation (to attack you):
- Malicious bots: scan public GitHub events in real time and exploit the keys they find automatically
On a public repository, some keys can be detected and exploited very quickly, sometimes within minutes.
Commonly exposed secret types
Some secret patterns are detected automatically by scanners. Malicious bots target cloud keys first, because they give immediate access to paid resources.
| Type | Pattern | Risk |
|---|---|---|
| AWS Access Key | AKIA... (20 characters) | Full cloud access |
| GitHub PAT | ghp_... | Access to repositories |
| Stripe API Key | sk_live_... | Financial transactions |
| JWT Secret | Variable | Token forgery |
| Private Key | -----BEGIN RSA PRIVATE KEY----- | Identity theft |
| Database URL | postgres://user:pass@host | Database access |
.npmrc / .pypirc | _authToken=... | Package publishing |
docker/config.json | "auth": "base64..." | Push to a registry |
| kubeconfig | token: or client certificate | Kubernetes cluster access |
| Terraform Cloud token | *.atlasv1.* | Infrastructure management |
| Client certificate | -----BEGIN CERTIFICATE----- | mTLS authentication |
Compromise scenarios
Scenario 1: the AWS key exploited within minutes
T+0min : Developer pushes a commit with an AWS keyT+2min : Bot scans the public repository through the GitHub APIT+3min : Bot extracts the key and launches EC2 instances to mineT+5min : AWS bill explodesT+1h : Developer notices the problemReal cost: thousands of euros in cloud billing.
Scenario 2: the forgotten "temporary" secret
T-3years : Secret committed for a testT-3years : Secret removed in the next commitT : Attacker scans the historyT+1h : Secret still valid, production access compromisedNobody had revoked the secret: it had only disappeared from the current
code. A scan of the files on disk, gitleaks dir, sees nothing anymore; a scan
of the history, gitleaks git, finds it in the original commit. That is
exactly what the attacker does.
Scenario 3: the former employee's fork
T : Private repository with a secret in its historyT : Employee leaves the company, keeps their forkT+6m : The former employee still has access to the secretEven after cleaning the history of the origin repository, the fork keeps its own complete copy. That is why revoking the secret is the only reliable action.
Preventing leaks
Prevention relies on several complementary layers. None of them is enough on its own.
The diagram follows a commit from the workstation to the main branch: each barrier catches what the previous one let through, and the only one that cannot be bypassed from the workstation is the blocking CI scan.
Pre-commit hooks (first barrier)
Local hooks block the commit when a secret is detected:
repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.30.1 hooks: - id: gitleaks# Installationpip install pre-commitpre-commit install
# Testgit commit -m "Test"# Gitleaks automatically scans the modified filesCustom Gitleaks configuration
Two traps await anyone writing their first .gitleaks.toml, and both were
measured on Gitleaks 8.22.1 and 8.30.1. The allowlist paths are regular
expressions, not file patterns: a pattern such as **/*_test.go makes
Gitleaks crash on load (panic: regexp: Compile), and the hook and the CI
with it. Above all, a file that declares its own [[rules]] replaces the
default rules, unless it carries [extend] useDefault = true: without that
line, the GitHub token in the test repository was no longer detected, and
only the custom rule remained active.
[extend]useDefault = true # keep the default rules in addition to yours
[allowlist]paths = [ '''test/fixtures/''', # regular expressions, not globs '''_test\.go$''',]
[[rules]]id = "custom-api-key"description = "Custom API Key"regex = '''custom_api_key\s*=\s*['"]([^'"]+)['"]'''Forge push protection
GitHub offers push protection, which scans the content at push time and blocks the upload when a known secret is detected. It is a very strong prevention building block, different from after-the-fact secret scanning:
| Mechanism | When | Action |
|---|---|---|
| Push protection | At push time | Blocks the upload |
| Secret scanning | After the commit | Alerts the maintainer |
Benefits of push protection:
- Acts before the secret enters the repository
- Covers known patterns (cloud keys, forge tokens, etc.)
- Complements local hooks (protects even without pre-commit)
Limits:
- Only detects the forge's known patterns
- A developer can force the push through (with a justification)
- Does not replace CI scans of the full history
For organisations, GitHub also lets you define custom patterns for secret scanning and push protection, tailored to internal secret formats.
.gitignore: reducing common mistakes
# Secrets and local configuration.env.env.**.key*.pem*.p12*.jkssecrets/credentials/
# Package registries.npmrc.pypirc
# Dockerdocker/config.json
# Kuberneteskubeconfig*.kubeconfig
# IDE.idea/.vscode/settings.json
# Logs that may contain secrets*.loglogs/Detecting secrets
Gitleaks
Gitleaks is a fast, rule-based scanner (regex). It is particularly well suited to pre-commit hooks and CI integration.
# Scan the full history (all commits)gitleaks git --verbose
# Scan only the files present on diskgitleaks dir --verboseThe difference between the two commands is at the heart of this guide. On a
repository where the secret was removed from the code but remains in an earlier
commit, gitleaks dir finds nothing and gitleaks git finds it: the
"fixed" secret never left the history. Since version 8.19, these two
subcommands replace gitleaks detect, which still works but is no longer
listed in the help.
Strengths:
- Fast, scans large repositories in seconds
- Preconfigured patterns, covers the most common formats
- Easy to integrate, pre-commit, GitHub Actions, GitLab CI
TruffleHog
TruffleHog goes further with active verification: it tests whether the secret it found is still valid by trying to use it. This lets you prioritise the real active secrets and set aside false positives or secrets that were already revoked.
# Scan with active verificationtrufflehog git file://. --only-verified
# OutputFound verified resultDetector Type: AWSRaw result: AKIAIOSFODNN7EXAMPLECommit: abc1234Email: dev@company.comStrengths:
- Active verification, tells an exploitable secret apart from a false positive or an already revoked secret
- Entropy-based detection, spots secrets that do not follow a known pattern
- Classification, groups the results by type and by risk
Blocking CI scan
The CI scan is the last barrier before the code reaches the main branch. It must block the PR when a secret is detected.
name: Secret Scanon: [push, pull_request]
permissions: {}
jobs: gitleaks: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false
- uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}gitleaks: stage: security image: ghcr.io/gitleaks/gitleaks:v8.30.1@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f script: - gitleaks git --verbose --redact allow_failure: falseCI best practices:
- Pin actions by SHA, avoid mutable tags
- Scan the diff on PRs, fast, targets the changes
- Scan the full history periodically, catches old secrets
- Block the PR on detection, no
allow_failure: true
Three tools fill this role: Gitleaks, the course's reference implementation, TruffleHog (in French), which verifies whether a secret it finds is still active, and detect-secrets (in French), which works with a baseline of accepted false positives.
Responding to a secret leak
-
Revoke immediately
Change the secret before cleaning the history. An attacker may already have copied it.
-
Assess the impact
How long was the secret exposed? What access does it grant? Is the repository public or private?
-
Look for malicious use
Check the audit logs of the service concerned (CloudTrail for AWS, audit logs for GitHub, etc.).
-
Clean the history
See the next section on
git-filter-repo. -
Purge the artifacts
- Delete the CI/CD artifacts that contain the secret
- Empty the build caches (Docker, npm, etc.)
- Ask collaborators to re-clone the repository
-
Prevent a recurrence
Install the pre-commit hooks and the CI scans, and enable push protection.
Cleaning the history: what it does, and what it does not
git-filter-repo is the recommended tool to rewrite history (it replaces the
obsolete git filter-branch).
What git-filter-repo does
# Remove a file from the whole historygit filter-repo --invert-paths --path config/secrets.yml
# Replace a pattern across the whole historygit filter-repo --replace-text expressions.txtgit-filter-repo removes the origin remote after the rewrite, to prevent
an accidental push: measured, git remote is empty after the operation.
You therefore have to add it back before pushing the rewritten history.
# Add back the remote removed by git-filter-repogit remote add origin <repository-url>
# Force push on all branchesgit push --force --allgit push --force --tagsWhat the cleanup does NOT do
Rewriting the history of the origin repository is necessary, but not enough:
| Surface | Cleaned automatically? |
|---|---|
| Origin repository | Yes (after force push) |
| Developer clones | No, each one must re-clone |
| Forks | No, outside your control |
| CI/CD artifacts | No, manual deletion |
| Build caches | No, manual purge |
| Forge backups | No, depends on the platform |
| Third-party analysis systems | No, depends on the service |
Consequence: cleaning the history is useful for repository hygiene, but revoking the secret remains the only truly reliable action. If a fork or an external clone captured the secret, no rewrite on your side will erase it.
Troubleshooting
The problems below come up when setting up the controls or cleaning a
repository. Three of them were reproduced in a container: the Gitleaks
configuration that crashes, the one that switches off the default rules,
and git-filter-repo refusing to run.
| Symptom | Likely cause | Solution |
|---|---|---|
| Gitleaks reports false positives | Pattern too broad or test file | Add the path, as a regular expression, to [allowlist] in .gitleaks.toml |
git-filter-repo refuses: "does not look like a fresh clone" | The repository is not a fresh clone: history in the reflog, or modified files | Work on a fresh, clean clone (git clone --no-local), or force with --force knowingly |
Gitleaks crashes on startup: panic: regexp: Compile | An allowlist path written as a glob (**/*_test.go) | Write the paths as regular expressions (_test\.go$) |
| No more alerts after adding a custom rule | [[rules]] without [extend] replace the default rules | Add [extend] useDefault = true to .gitleaks.toml |
| Secret detected but already revoked | No filtering by validity | Use trufflehog --only-verified to prioritise |
| Pre-commit hook bypassed | Developer used --no-verify | Add a blocking CI scan as a safety net |
| Push protection bypassed | Developer forced the push through | Check the justifications in the forge logs |
| Collaborators get errors after a force push | Commit SHAs were rewritten | Ask the team to re-clone the repository |
Key points
- As soon as a secret reaches Git, it is compromised, revocation is the only reliable action
- Git is only the starting point, the secret spreads into clones, forks, CI runners, artifacts and backups
- Revoke before cleaning, cleaning the history does not erase external copies
- Three prevention layers, pre-commit hooks + push protection + blocking CI scan
- No layer is enough on its own, hooks can be bypassed, push protection only covers known patterns, CI only protects after the push
- Cleaning the history has its limits,
git-filter-reporewrites the repository, but not the forks or the external caches
Knowledge check
Check that you have the essentials of this guide. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Secrets scanning: detecting secrets exposed in code
Continue the course: Responding to a compromised secret: revoke, rotate, analyse
A new full quiz with random questions
Work again only on the questions you missed