DevSecOps Part 1 of 3 · DevSecOps Pipeline
Secrets Detection with Gitleaks
Stop credentials from reaching Git: run Gitleaks in pre-commit hooks and CI, tune rules and allowlists, handle findings without leaking them further, and decide when history rewriting is worth it.
On this page
Overview
A secret committed to Git is compromised the moment it is pushed, even to a private repository: it will be cloned to laptops, CI caches, backup systems and, eventually, somewhere you did not expect. Gitleaks detects secrets by scanning commits and files against regular-expression rules with entropy checks. It is fast, single-binary and has a default ruleset covering cloud providers, SaaS tokens, private keys and generic high-entropy strings.
The goal of this setup is defence in depth:
- Pre-commit blocks the commit on the developer’s machine: fast feedback, nothing leaves the laptop.
- CI scans every merge request and, nightly, the full history, which catches hooks that were skipped.
- Remediation is a process, not a command: rotate first, clean up second.
- Pre-commit hook.
gitleaks git --pre-commit --stagedruns on the staged diff only, so it takes milliseconds and rejects the commit before the secret is ever written to history. - Repository. A commit that gets through (hooks are optional and
--no-verifyexists) lands here. From this point the secret is in every clone. - Merge request pipeline. Scans the commits that are new to the merge request. On GitLab the built-in Secret Detection template does this and posts findings in the merge request widget.
- Nightly scan. Full history, every repository, on a schedule. New rules are added to Gitleaks regularly, and yesterday’s clean history can contain today’s finding.
- Rotate the credential. Any finding in the repository or in CI means the secret is compromised. Rotation is the only step that fixes that; everything after it is hygiene.
- Fix or allowlist. Remove the secret from the working tree, or record a false positive by fingerprint with a reason. Only then decide whether history rewriting is worth its cost.
Everything below was run with Gitleaks 8.30.1 against a throwaway repository; the outputs are real.
Prerequisites
- Git 2.3x or newer
- Gitleaks 8.3x (
brew install gitleaks, the release binary, orghcr.io/gitleaks/gitleaksvia Docker) - Optional: the
pre-commitframework (pipx install pre-commit)
Implementation
Scan a repository
Gitleaks has two main modes. git scans commit history; dir scans the working tree (useful for build outputs and non-Git directories).
# Full history scan of the current repository
gitleaks git --redact --verbose .
# Only the commits on this branch that are not on main (fast, for CI on MRs)
gitleaks git --redact --log-opts="origin/main..HEAD" .
# Working tree only (no history)
gitleaks dir --redact .
--redact prints the finding location but masks the secret itself. Always use it in CI so the log does not become
the second leak. With --verbose, each finding looks like this:
Finding: ...xport GITLAB_TOKEN="REDACTED
Secret: REDACTED
RuleID: gitlab-pat
Entropy: 4.546594
File: deploy.sh
Line: 2
Commit: a50cbe98963a1aae55da6adf9580da6081b327c6
Author: Lab User
Email: lab@example.com
Date: 2026-09-12T21:09:04Z
Fingerprint: a50cbe98963a1aae55da6adf9580da6081b327c6:deploy.sh:gitlab-pat:2
9:09PM INF 3 commits scanned.
9:09PM INF scanned ~300 bytes (300 bytes) in 81.7ms
9:09PM WRN leaks found: 3
The RuleID says which detector matched (gitlab-pat for a glpat- token, aws-access-token for an AKIA… key,
generic-api-key for a high-entropy string next to a keyword). The Fingerprint is
commit:file:rule:line, and it is the key you use to ignore a confirmed false positive.
Two behaviours worth knowing before you test your own setup. First, Gitleaks ships with a built-in allowlist for
documented placeholder credentials, so Amazon’s example key AKIAIOSFODNN7EXAMPLE and its secret produce no
finding; a test with those values proves nothing. Second, entropy matters: a ghp_aaaaaaaa… string that has the
right prefix but no randomness is also skipped. Test with values that have the real shape and random content.
Configuration
Create a .gitleaks.toml that extends the default rules rather than replacing them:
title = "sachinchaurasiya.com gitleaks config"
[extend]
useDefault = true
# Organisation-specific token format
[[rules]]
id = "internal-service-token"
description = "Internal platform service token"
regex = '''svc_[A-Za-z0-9]{40}'''
keywords = ["svc_"]
# Paths that legitimately contain high-entropy strings
[allowlist]
paths = [
'''^pnpm-lock\.yaml$''',
'''^package-lock\.json$''',
'''(^|/)__fixtures__/''',
]
regexes = [
'''EXAMPLE[A-Z0-9]*''', # documentation placeholders
]
Rules for allowlists: prefer path allowlists for lockfiles and fixtures, keep regex allowlists narrow, and never allowlist an entire directory of source code because “it is noisy”.
Ignore a specific finding
When a finding is a confirmed false positive, record its fingerprint (printed with --verbose) in .gitleaksignore
with a comment. A fingerprint pins one occurrence in one commit, which is what you want: the same string appearing in
a new file is a new finding.
# lab fixture, not a real key. Reviewed 2026-09-13
fa913d406dfc1487b8a0c9005ed283279a085c33:settings.py:aws-access-token:2
gitleaks git --redact . # leaks found: 3 → leaks found: 2
Pre-commit hook
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1 # pin; Renovate can bump it
hooks:
- id: gitleaks
pre-commit install && pre-commit run gitleaks --all-files The hook runs gitleaks git --pre-commit --staged, which scans only the staged changes. You can run the same
command by hand to see what the hook sees:
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 .
RuleID: github-pat
Fingerprint: token.py:github-pat:1
9:09PM WRN leaks found: 1
Developers can bypass hooks with --no-verify, which is why the CI scan exists.
GitLab CI
secret-scan:
stage: security
image:
name: ghcr.io/gitleaks/gitleaks:v8.30.1
entrypoint: ['']
variables:
GIT_DEPTH: 0 # full clone so history can be scanned
script:
# Merge requests: only the commits that are new to this branch (fast).
- |
if [ -n "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" ]; then
git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
gitleaks git --redact --exit-code 1 --report-format sarif --report-path gitleaks.sarif \
--log-opts="origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME..HEAD" .
else
gitleaks git --redact --exit-code 1 --report-format sarif --report-path gitleaks.sarif .
fi
artifacts:
when: always
paths: [gitleaks.sarif]
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_PIPELINE_SOURCE == "schedule"
--log-opts is passed to git log, so a range like origin/main..HEAD scans only the commits the merge request
adds (the git fetch makes sure the target branch ref exists in the runner’s clone); the default branch and the
nightly schedule scan everything. The SARIF report contains one result per finding
with the rule, file and commit, and can be uploaded to a code-hosting platform or an IDE.
GitLab also ships a built-in Secret Detection template (Jobs/Secret-Detection.gitlab-ci.yml) that runs
Gitleaks and renders findings in the merge request widget on every tier. Use the template when you want the UI
integration and a custom job when you need full control over rules and output; this site’s
pipeline uses the template alongside Trivy’s secret scanner.
Verification
Create a throwaway branch with a fake but well-formed key and confirm both layers catch it:
git switch -c test/gitleaks
printf 'GITLAB_TOKEN=glpat-%s\n' "$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 20)" > .env.test
git add .env.test
git commit -m "test: trigger gitleaks" # pre-commit hook should reject this
git commit --no-verify -m "test: bypass hook" && git push -u origin test/gitleaks # CI should fail
Use a random value with the real token shape. Amazon’s documented example keys are allow-listed by default and will not trigger anything. Delete the branch afterwards; even a fake key generates noise for anyone auditing the repository later.
Security Considerations
- Store CI reports as artifacts with restricted visibility; a SARIF file without
--redactcontains the secrets it found. - Scan all repositories, including infrastructure and documentation repos — Terraform variables and READMEs leak as often as application code.
- Complement detection with prevention: inject secrets at runtime from Vault or a cloud secret manager so there is nothing to commit.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Hundreds of findings in lockfiles | Integrity hashes match the generic high-entropy rule | Allowlist lockfile paths (see config above) |
| CI scan finds nothing on a merge request | Shallow clone (GIT_DEPTH small) | Set GIT_DEPTH: 0 or scan with --log-opts against the target branch |
| Pre-commit hook is slow on large repos | Scanning all files each time | The hook scans staged files only; run full scans in CI |
| Finding keeps reappearing after fix | Secret still in history | Rotate the secret; optionally rewrite history and force-push with team agreement |
Production Recommendations
- Run a full-history scan nightly on every repository and page on new findings.
- Pin the Gitleaks version in both the pre-commit config and the CI image; update them together. New rules arrive with new versions, and a mismatch means the hook and CI disagree about what a secret is.
- Track allowlist changes in code review — an allowlist entry is a security decision.
- Pair with GitLab push rules (
Prevent pushing secret files) for an additional server-side check.
Conclusion
Secrets detection is cheap to run and expensive to skip. Put Gitleaks in pre-commit for feedback, in CI for enforcement, and make rotation the first step of every finding.
References
Keep reading