Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Secrets scanning: detecting secrets exposed in code

By updated

Improve this page

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.

VectorExamplePossible detection
Direct commitpassword = "admin123" in the codeYes, repository scan
Example file.env.example with real valuesYes, repository scan
Git historySecret removed but still in the historyYes, history scan
CI/CD logsToken printed in build logsPartial, variable masking
ArtifactsCredentials in a binary or packaged configurationPartial, artifact scan
Merge requestSecret in a diff before mergeYes, merge request scan

Goal: reduce exposure time

The point is not to reach "zero exposed secrets" (utopian), but to:

  1. Detect as early as possible: before the merge, ideally before the push
  2. Standardise the response: a clear rotation and revocation procedure
  3. 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.

FamilyExamples
API tokensGitHub or GitLab tokens, cloud provider tokens (AWS, GCP, Azure)
CredentialsPasswords, database connection strings, SMTP credentials
Cryptographic keysSSH or TLS private keys, encryption keys, JWT signing secrets
Webhooks and URLsURLs 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 xxx or REPLACE_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:

  1. 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.

  2. 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.

  3. 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.

CriterionGitLab Secret DetectionGitleaksTruffleHog
IntegrationNative GitLabGeneric CIGeneric CI
MethodRegex rulesRegex rulesEntropy + regex + validation
HistoryLimitedCompleteComplete
ValidationNoNoYes, for some providers
CostScanner included in every tier; MR reports and vulnerability management in UltimateFreeFree / Enterprise
NoiseMediumConfigurableLow (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

→ Gitleaks lesson

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 --fail

Strengths:

  • 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.

SituationRecommendation
GitLab Ultimate, small teamGitLab Secret Detection
Generic CI, need for controlGitleaks
High volume, problematic false positivesTruffleHog
Defence in depthGitleaks (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.

QuestionOptions
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:

.gitleaks.toml
[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

  1. Real secret?

    False positive (hash, fixture, example): add it to the allowlist.

  2. Publicly exposed?

    Public repository, public merge request, accessible history: urgent rotation.

  3. What scope?

    Limited access (dev) versus wide access (production, admin).

  4. 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.

SituationAction
Valid secret detected1. Revoke/rotate 2. Check access logs 3. Remove from the code
Secret in the historyMandatory rotation (even if removed from the current code)
Publicly exposed secretRotation + 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.

BeliefReality
"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 detect command: use gitleaks git for the history, gitleaks dir for the file tree.
  • TruffleHog validates secrets with the providers and exits with code 183 when --fail is 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

6 questions
6 min
70% to pass

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

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn