Secrets scanning detects tokens, keys and passwords exposed in code, Git history or merge requests, so they can be revoked before an attacker uses them. This lesson shows where to place the checks, how to choose between GitLab Secret Detection, Gitleaks and TruffleHog, how to handle false positives without neutralising the scanner, and how to deal with an alert all the way to rotation. It is for anyone bringing security into an existing CI.
A pair of access keys committed by mistake. An API key in a
.env.example file. A GitLab token in CI logs. These scenarios are not
hypothetical: GitGuardian counted 28.65 million new hard-coded secrets added
to public GitHub in 2025, 34% more than in 2024.
Secrets scanning is not optional. It is a mandatory line of defence in any software factory.
What you will learn
- Tell a real secret from noise (hashes, UUIDs, test fixtures)
- Place the checks at three levels: pre-commit, merge request, branch
- Choose between GitLab Secret Detection, Gitleaks and TruffleHog
- Configure a valid Gitleaks allowlist without neutralising the scanner
- Triage and remediate an alert: qualification, rotation, traceability
Why secrets scanning is non-negotiable
The reasoning has two steps. First, a secret does not leak by accident but through identifiable paths, which can therefore be instrumented. Second, the realistic goal is not to prevent every leak, but to shorten the exposure time between the moment the secret leaves and the moment it is revoked. The same GitGuardian report measures the reverse problem: in January 2026, 64% of the valid secrets detected in 2022 were still active. Detecting is useless without a rotation procedure.
Realistic leak scenarios
Secrets leak through predictable paths. The table lists the most common ones, with what can detect them; the last column shows that some paths escape code scanning and need other checks.
| Vector | Example | Possible detection |
|---|---|---|
| Direct commit | password = "admin123" in the code | Yes, repository scan |
| Example file | .env.example with real values | Yes, repository scan |
| Git history | Secret removed but still in the history | Yes, history scan |
| CI/CD logs | Token printed in build logs | Partial, variable masking |
| Artifacts | Credentials in a binary or packaged configuration | Partial, artifact scan |
| Merge request | Secret in a diff before merge | Yes, merge request scan |
Goal: reduce exposure time
The point is not to reach "zero exposed secrets" (utopian), but to:
- Detect as early as possible: before the merge, ideally before the push
- Standardise the response: a clear rotation and revocation procedure
- Trace incidents: to improve prevention
What counts as a "secret" (and what does not)
Defining the scope before installing a scanner avoids the classic scenario where the team disables the tool after two weeks. A scanner detects patterns, not secrets: you decide what counts as an exploitable credential and what is acceptable noise.
Types of secrets
These four families differ in their rotation procedure, not in their format. An API token is revoked in one request, a TLS private key requires reissuing a certificate and redeploying it, a database password requires coordinating the application and the service. Putting a secret in the right family at detection time therefore sets the remediation delay to announce.
| Family | Examples |
|---|---|
| API tokens | GitHub or GitLab tokens, cloud provider tokens (AWS, GCP, Azure) |
| Credentials | Passwords, database connection strings, SMTP credentials |
| Cryptographic keys | SSH or TLS private keys, encryption keys, JWT signing secrets |
| Webhooks and URLs | URLs carrying a token, Slack or Discord webhooks |
Edge cases: the "noise"
Not every detected pattern is a secret:
- UUIDs: identifiers, not secrets
- Digests: a file's SHA-256 is not a password
- Test fixtures: fake keys for unit tests
- Documentation: examples with
xxxorREPLACE_ME
An effective strategy manages this noise instead of enduring it.
Where to place the checks (defence in depth)
Secret detection must happen at several levels:
-
Before the push (client side)
Pre-commit hooks with Gitleaks. They block the commit before it reaches the server.
Strength: immediate feedback for the developer. Limit: it can be bypassed, since a hook can be disabled.
-
In the merge request
Automatic scan on each merge request. Feedback visible in the diff.
Strengths: a centralised check the developer cannot disable. Limit: detection comes after the commit.
-
On the default branch
Post-merge scan as a safety net.
Strength: full coverage. Limit: you react after the fact.
Recommendation: combine all three levels. Pre-commit speeds up feedback, the merge request guarantees the check, the branch scan ensures coverage.
Implementation options
Three tools dominate the field, and they do not solve the same problem. The useful reading of the comparison below rests on two rows only: History, which decides whether you will see secrets already removed from the current code, and Validation, which decides the volume of false positives your team will have to triage every week.
Tool comparison
The three columns are not mutually exclusive: many teams run Gitleaks locally and TruffleHog in CI. Remember the Method row above all, which explains the rest. Detection by regular expression recognises known formats, misses a home-made token and does not know whether the credential is still active. Detection by entropy spots random strings whatever their format, at the cost of false positives that validation then filters.
| Criterion | GitLab Secret Detection | Gitleaks | TruffleHog |
|---|---|---|---|
| Integration | Native GitLab | Generic CI | Generic CI |
| Method | Regex rules | Regex rules | Entropy + regex + validation |
| History | Limited | Complete | Complete |
| Validation | No | No | Yes, for some providers |
| Cost | Scanner included in every tier; MR reports and vulnerability management in Ultimate | Free | Free / Enterprise |
| Noise | Medium | Configurable | Low (validation) |
GitLab Secret Detection
GitLab's built-in scanner is enabled with a single include line, with no
job to write. It runs on every tier, Free included; on Free and Premium you
get the JSON report as an artifact, while Ultimate brings it into the merge
request widget, the security dashboard and security policies. Its blind
spot is history: the scan covers the commits of the current branch, not the
whole repository.
include: - template: Jobs/Secret-Detection.gitlab-ci.yml- Produces a security report viewable in the MR (Ultimate tier)
- Rules maintained by GitLab
- Limited to the commits of the current branch (not the full history)
→ GitLab Secret Detection documentation
Gitleaks
Gitleaks is a single Go binary, with no dependency, that fits into any
CI. Since version 8, the detect command is gone, replaced by two explicit
subcommands: gitleaks git walks the commit history, gitleaks dir
inspects the file tree as it is on disk. The image is pinned by digest
below so the pipeline runs exactly the audited binary, and not whatever the
registry publishes tomorrow under the same tag.
secrets-scan: image: ghcr.io/gitleaks/gitleaks:v8.30.1@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f script: - gitleaks git --redact --verbose . rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event"Strengths:
- Fine-grained configuration through
.gitleaks.toml - Full history scan (
gitleaks git --log-opts="--all" .) - Exit code 1 as soon as a leak is found, usable for gating
TruffleHog
TruffleHog stands out with active verification: for the providers it knows,
it calls the relevant API to find out whether the credential found still
works. A secret already revoked drops out of the report, which cuts the
triage volume. The --results=verified filter replaces the old
--only-verified, still accepted but removed from the help. Watch out for the
detail that costs hours of CI debugging: with --fail, TruffleHog exits with
code 183, not 1, which trips up scripts that test -eq 1.
secrets-scan: image: trufflesecurity/trufflehog:3.95.9@sha256:59b244249d1a1aef4baa24fe73d3c931616264482580d806d77f6c74d26b3e42 script: - trufflehog git file://. --results=verified --failStrengths:
- Checks whether the secret is still valid (some providers)
- Drastically reduces false positives
- Multi-source scanning (Git, S3, Slack, etc.)
→ TruffleHog guide (in French)
Choice criteria
This table is read by the left column: identify the row that describes your current situation, not the one you aim for in six months. The last row is the one I recommend as soon as the team exceeds about ten developers, because it puts a fast, local check before the push and an exhaustive, non-bypassable check in CI.
| Situation | Recommendation |
|---|---|
| GitLab Ultimate, small team | GitLab Secret Detection |
| Generic CI, need for control | Gitleaks |
| High volume, problematic false positives | TruffleHog |
| Defence in depth | Gitleaks (pre-commit) + TruffleHog (CI) |
Governance decisions
Before implementing, the team must decide on three topics: blocking rules, ownership of alerts, and exception handling. Each one is a decision, not a setting, and it must be written down before the scanner is switched on.
Blocking rules
These three questions decide whether your scanner will be respected or
bypassed. Blocking a pipeline costs the whole team time: the decision must
be made before deployment, not at the first incident. The third row is the
most often neglected, yet it conditions the other two, because a blocking rule
with no documented exception mechanism always ends in widespread --no-verify.
| Question | Options |
|---|---|
| When should the pipeline be blocked? | Always / Only on protected branches / Never (warning) |
| Which kinds of secrets block? | All / High severity only / Explicit list |
| What about false positives? | Baseline + allowlist / Manual review |
Ownership and escalation
An alert with no named owner stays open. Answer the three questions below in writing, in the repository itself, before switching the scanner on: it is this document the on-call person will open at 2 am. The most effective choice in practice is to hand the handling to the commit author, who alone knows what the credential is for, with the security team as escalation for production secrets.
- Who handles an alert? The commit author, or the security team?
- What handling delay? Immediate for real secrets?
- What escalation? A notification in the chat tool, or an automatic incident?
Exception handling
Exceptions are inevitable. Document them:
[extend]useDefault = true
[[allowlists]]description = "Fake keys used by the unit tests"regexes = ['''FAKE_.*_KEY''']paths = ['''tests/''', '''fixtures/''']The [extend] block with useDefault = true is mandatory: without it, your
file replaces Gitleaks' default rule set instead of extending it, and the
scanner detects nothing any more. Global exclusions are written with
[[allowlists]] at root level; the [[rules.allowlist]] form still found in
old examples makes the configuration fail to load with the message
'Rules[0].AllowList' expected a map, got 'slice'. Always check that a change
to the file leaves the scanner working, by deliberately reintroducing a fake
secret in a test branch.
Rules:
- Every exception has an expiry date
- Every exception is justified (comment)
- Exceptions are reviewed quarterly
Triage and remediation process
A scanner raising alerts nobody handles degrades security, because it builds the habit of ignoring warnings. The process below follows a set order: qualify before acting, then apply the minimal action matching the situation, then trace. The question that governs everything comes second, public exposure, because it turns an internal incident into a race against scraping bots.
Qualify the alert
-
Real secret?
False positive (hash, fixture, example): add it to the allowlist.
-
Publicly exposed?
Public repository, public merge request, accessible history: urgent rotation.
-
What scope?
Limited access (dev) versus wide access (production, admin).
-
Rotation possible?
Revocable token: revoke immediately. Hard-coded credential: rotation, then an audit of its use.
Minimal actions
The word "minimal" is to be taken literally: these are the actions below which the incident is not closed. Note that rotation appears in all three rows, including when the secret has been removed from the code. The reason lies in how Git works: a commit removed from the branch remains reachable in the repository objects, in forks and in clones already made. Only revoking the credential really closes access.
| Situation | Action |
|---|---|
| Valid secret detected | 1. Revoke/rotate 2. Check access logs 3. Remove from the code |
| Secret in the history | Mandatory rotation (even if removed from the current code) |
| Publicly exposed secret | Rotation + audit + consider purging the history |
Traceability
Each exposed-secret incident should produce three traces: a follow-up issue that checks the rotation was done, a light post-mortem that explains how it happened, and an improvement, a rule added or a process changed, so that the same path does not leak twice.
Common mistakes
These six beliefs come up in almost every pipeline audit. The first three stem from an incomplete understanding of how Git and CI work, the last three from organisational choices that make an otherwise well-tooled setup fail. The last one is the most structural: confusing detection with secret management leads to stacking scanners without ever fixing the cause.
| Belief | Reality |
|---|---|
| "We masked the variable, so we're fine" | Masking hides the value in logs; it does not stop it from being committed in the code |
| "We removed the file from the repository" | Git history keeps everything: a git log -p or a clone is enough to find the secret. Only rotation protects |
| "We scan the default branch, that's enough" | The secret is already exposed when it reaches main; scanning merge requests blocks before the merge |
| "Too many false positives, we disable it" | The right answer is to tune the rules, keep an allowlist and name an owner |
| "We block everything from day one" | Without exception handling or rotation, you create workarounds: start in warning mode, then tighten |
| "Scanning is managing our secrets" | The scanner detects leaks; a secret manager (Vault, KMS) stores and distributes. The two complement each other |
Checklist
The checklist follows the three phases of a rollout: what you decide before writing a single line of CI, what you install, then what you must have ready the day an alert fires. Go through them in that order: a technical setup installed without a governance decision mostly produces workarounds.
Design (decisions)
These five points are settled in a meeting, not in a YAML file. As long as they are not written down somewhere in the repository, each alert will restart the same discussion. The most debated point is the third, blocking rules: start in warning mode, measure the real volume of detections over a month, then tighten.
- Scan levels defined (pre-commit / MR / branch)
- Tool(s) chosen, with a justification
- Blocking rules documented
- Alert ownership assigned
- Exception process defined
Implementation (checks)
The only truly mandatory item in this list is the CI job on merge
requests: it is the only check a developer cannot disable from their machine.
The pre-commit hook improves comfort and shortens the feedback loop, but it
is bypassed with git commit --no-verify and must never be counted as a
barrier. The initial allowlist is filled by running the scanner once on the
history, before enabling blocking.
- Pre-commit hooks configured (optional but recommended)
- CI scan job on MRs
- Default branch scan as a safety net
- Initial allowlist for known false positives
- Notifications configured (chat, email)
Run (remediation)
This is the part teams prepare last and need first. A rotation procedure per type of secret is essential because revoking a GitHub token and rotating a database password involve neither the same commands, nor the same people, nor the same risk of service interruption. Write them in cold blood, with the exact command and the name of the service involved.
- Rotation procedure documented per type of secret
- Escalation contacts identified
- Issue template for secret incidents
- Quarterly exception review scheduled
Key points
- A committed secret is compromised, even once removed from the code: Git history, forks and clones keep it. Only rotation closes access.
- Place the check at three levels: pre-commit for comfort, merge request for the non-bypassable barrier, default branch as a safety net.
- Gitleaks 8 no longer has a
detectcommand: usegitleaks gitfor the history,gitleaks dirfor the file tree. - TruffleHog validates secrets with the providers and exits with code
183 when
--failis set, not 1. - Pin scanner images by digest: a scanner changing under your feet skews the comparison between two runs.
- Decide on governance (blocking, ownership, exceptions) before deploying the tool, otherwise you will get workarounds rather than fixes.
Knowledge check
Check that the essentials of this lesson are in place. 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: Gitleaks: scan your Git repositories for exposed secrets
A new full quiz with random questions
Work again only on the questions you missed