Hands-on lab
Scan Git Repositories with Gitleaks
Seed a repository with realistic fake secrets, catch them with Gitleaks in history and working-tree modes, ignore a confirmed false positive by fingerprint, and block the next one at commit time.
Before you start
- Git
- Gitleaks 8.3x installed, or Docker to run ghcr.io/gitleaks/gitleaks
- jq (for the report step)
Interactive environment
Practice this lab in a temporary browser-based environment. Nothing needs to be installed on your machine.
Steps on this page
Goal
By the end of this lab you will know what Gitleaks detects and what it deliberately ignores, how a secret survives the “remove it in a new commit” fix, how findings are reported and suppressed, and how to stop the next one before it reaches history. Every command below was run with Gitleaks 8.30.1; the output shown is what you should see, give or take timestamps and commit hashes.
Environment
Work in a throwaway directory. If Gitleaks is not installed, define a shell function that runs it from the official image; the rest of the lab is identical:
mkdir -p ~/labs/gitleaks && cd ~/labs/gitleaks
gitleaks version # installed binary
# or, without installing:
gitleaks() { docker run --rm -v "$PWD:/repo" -w /repo ghcr.io/gitleaks/gitleaks:v8.30.1 "$@"; }
Step 1: create a repository with leaks
The values are generated with the right shape and random content. That matters: Gitleaks ships with a built-in
allowlist for documented placeholders such as Amazon’s AKIAIOSFODNN7EXAMPLE, and it skips strings with the right
prefix but no entropy (ghp_aaaaaaaa…). A lab seeded with those would prove nothing. Generating them also keeps
token-shaped literals out of this site’s own repository, where its scanners would flag them.
rand() { LC_ALL=C tr -dc "$1" < /dev/urandom | head -c "$2"; }
AKID="AKIA$(rand 'A-Z2-7' 16)" # AWS access key IDs are base32 after the prefix
ASEC="$(rand 'A-Za-z0-9' 40)"
git init -q -b main
cat > settings.py <<PY
DATABASE_URL = "postgres://app:s3cr3t-pa55@db.internal:5432/app"
AWS_ACCESS_KEY_ID = "$AKID"
AWS_SECRET_ACCESS_KEY = "$ASEC"
PY
git add settings.py && git commit -qm "add settings"
# "Fix" it the wrong way: remove the keys in a new commit. They stay in history.
sed -i.bak "s/$AKID/REDACTED/; s#$ASEC#REDACTED#" settings.py
rm settings.py.bak && git commit -qam "remove keys"
printf '#!/bin/sh\nexport GITLAB_TOKEN="glpat-%s"\n' "$(rand 'A-Za-z0-9' 20)" > deploy.sh
git add deploy.sh && git commit -qm "deploy script"
Step 2: scan history, then the working tree
gitleaks git --redact --verbose --no-banner .
Finding: ...xport GITLAB_TOKEN="REDACTED
Secret: REDACTED
RuleID: gitlab-pat
Entropy: 4.546594
File: deploy.sh
Line: 2
Commit: a50cbe98963a1aae55da6adf9580da6081b327c6
Fingerprint: a50cbe98963a1aae55da6adf9580da6081b327c6:deploy.sh:gitlab-pat:2
Finding: AWS_SECRET_ACCESS_KEY = "REDACTED"
RuleID: generic-api-key
Entropy: 5.171928
File: settings.py
Line: 3
Commit: fa913d406dfc1487b8a0c9005ed283279a085c33
Fingerprint: fa913d406dfc1487b8a0c9005ed283279a085c33:settings.py:generic-api-key:3
Finding: ...WS_ACCESS_KEY_ID = "REDACTED
RuleID: aws-access-token
Entropy: 4.021928
File: settings.py
Line: 2
Commit: fa913d406dfc1487b8a0c9005ed283279a085c33
Fingerprint: fa913d406dfc1487b8a0c9005ed283279a085c33:settings.py:aws-access-token:2
9:09PM INF 3 commits scanned.
9:09PM WRN leaks found: 3
Three findings, and two of them are in the first commit even though HEAD no longer contains the keys. That is the
whole point of history scanning. Now the working tree only:
gitleaks dir --redact --verbose --no-banner .
Finding: ...xport GITLAB_TOKEN="REDACTED
RuleID: gitlab-pat
File: deploy.sh
Line: 2
Fingerprint: deploy.sh:gitlab-pat:2
9:09PM WRN leaks found: 1
dir mode sees only what is on disk right now: one finding. Notice what neither mode reported: the database
password in DATABASE_URL. It is short, low-entropy and matches no rule. Gitleaks is a pattern-and-entropy scanner,
not a mind reader; a connection string with a weak password is something a reviewer catches, or a custom rule.
Step 3: report formats
gitleaks git --redact --no-banner --report-format sarif --report-path gitleaks.sarif .
jq '.runs[0].results | length' gitleaks.sarif
jq -r '.runs[0].results[0] | [.ruleId, .locations[0].physicalLocation.artifactLocation.uri, .partialFingerprints.commitSha] | @tsv' gitleaks.sarif
3
aws-access-token settings.py fa913d406dfc1487b8a0c9005ed283279a085c33
SARIF is what code-hosting platforms and IDEs ingest; --report-format json is easier for scripts. Because
--redact was used, neither file contains the secret values, only the locations.
Step 4: suppress one confirmed false positive
Suppose the AKIA… value in the first commit were a fixture that a test needs. Suppress that occurrence, by
fingerprint, with a reason. Do not allowlist the file, the rule, or the directory.
FP=$(gitleaks git --redact --verbose --no-banner . 2>/dev/null | awk '/aws-access-token:2$/ {print $2}')
printf '# test fixture, not a real key. Reviewed %s\n%s\n' "$(date +%F)" "$FP" > .gitleaksignore
gitleaks git --redact --no-banner .
9:09PM WRN leaks found: 2
The count drops from 3 to 2. The generic-api-key finding for the secret key on the next line is still reported,
which is correct: nothing was decided about it.
Step 5: scan only new commits
CI on a merge request should not re-scan a year of history on every push. --log-opts passes a range to git log:
gitleaks git --redact --no-banner --log-opts="HEAD~1..HEAD" .
9:09PM INF 1 commits scanned.
9:09PM WRN leaks found: 1
Only the deploy.sh commit is scanned. In a pipeline the range is origin/main..HEAD after fetching the target
branch; the full-history scan belongs on the default branch and on a nightly schedule.
Step 6: block the next leak at commit time
The pre-commit hook scans the staged diff. Run what the hook runs, first with a low-entropy fake to see it ignored, then with a random one to see it caught:
printf 'GITHUB_TOKEN = "ghp_%s"\n' "$(head -c 36 /dev/zero | tr '\0' 'a')" > token.py
git add token.py
gitleaks git --pre-commit --staged --redact --no-banner .
9:09PM INF no leaks found
printf 'GITHUB_TOKEN = "ghp_%s"\n' "$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 36)" > token.py
git add token.py
gitleaks git --pre-commit --staged --redact --verbose --no-banner .
RuleID: github-pat
Fingerprint: token.py:github-pat:1
9:09PM WRN leaks found: 1
To make Git run this automatically, install the hook through the pre-commit framework:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
pre-commit install
git commit -m "add token" # expect: the gitleaks hook fails and the commit is rejected
Verification
-
gitleaks gitreports 3 findings, two of them in a commit that is no longer atHEAD -
gitleaks dirreports 1 finding - With the
.gitleaksignoreentry, the history scan reports 2 -
--log-opts="HEAD~1..HEAD"scans exactly 1 commit - The staged scan ignores the all-
atoken and catches the random one
Cleanup
cd ~ && rm -rf ~/labs/gitleaks
unset -f gitleaks 2>/dev/null # if you used the Docker function
Troubleshooting
| Problem | Fix |
|---|---|
| A test key is not reported | Documented example keys are allow-listed and low-entropy strings are skipped; use random values with the real shape |
gitleaks: command not found in the pre-commit hook | pre-commit builds the hook from source on first run; install Go, or use the release binary on your PATH |
No findings in git mode on a fresh repository | There must be at least one commit; dir mode scans uncommitted files |
| Docker function fails with a permission error | The container writes reports into the mounted directory; make sure it is writable by your user |
Too many generic-api-key findings in lockfiles | Add path allowlists for lockfiles and fixtures in .gitleaks.toml; do not disable the rule |