Sachin Chaurasiya

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.

Author
Sachin Chaurasiya
Sachin Chaurasiya
Published
Reading time
8 min read
Difficulty
beginner

Reviewed Tested with Gitleaks 8.30.1, Git 2.50, GitLab.com Secret Detection template

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:

  1. Pre-commit blocks the commit on the developer’s machine: fast feedback, nothing leaves the laptop.
  2. CI scans every merge request and, nightly, the full history, which catches hooks that were skipped.
  3. Remediation is a process, not a command: rotate first, clean up second.
Diagram · Where secrets detection runs
Where secrets detection runsA developer commits. A pre-commit hook runs Gitleaks on the staged changes and rejects the commit if a secret matches. If the hook is skipped, the push reaches the repository and a merge request pipeline scans the new commits; a nightly job scans full history. A finding in the repository is treated as a leak: the credential is rotated first, then the finding is fixed or allow-listed with a reason, and only then is history rewriting considered.commitpushMRschedulefindingrejectDeveloperPre-commit hookgitleaks protect1RepositoryGitLab2MR pipelinegitleaks git (range)3Nightly scanfull history4Rotatecredentialfirst, always5Fix or allowlistwith a reason6

A developer commits. A pre-commit hook runs Gitleaks on the staged changes and rejects the commit if a secret matches. If the hook is skipped, the push reaches the repository and a merge request pipeline scans the new commits; a nightly job scans full history. A finding in the repository is treated as a leak: the credential is rotated first, then the finding is fixed or allow-listed with a reason, and only then is history rewriting considered.

  1. Pre-commit hook. gitleaks git --pre-commit --staged runs on the staged diff only, so it takes milliseconds and rejects the commit before the secret is ever written to history.
  2. Repository. A commit that gets through (hooks are optional and --no-verify exists) lands here. From this point the secret is in every clone.
  3. 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.
  4. 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.
  5. 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.
  6. 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, or ghcr.io/gitleaks/gitleaks via Docker)
  • Optional: the pre-commit framework (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
Installs the hook and runs it once across the repository.

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 --redact contains 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

SymptomCauseFix
Hundreds of findings in lockfilesIntegrity hashes match the generic high-entropy ruleAllowlist lockfile paths (see config above)
CI scan finds nothing on a merge requestShallow clone (GIT_DEPTH small)Set GIT_DEPTH: 0 or scan with --log-opts against the target branch
Pre-commit hook is slow on large reposScanning all files each timeThe hook scans staged files only; run full scans in CI
Finding keeps reappearing after fixSecret still in historyRotate 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