Secrets Scanning: Stop Leaking Credentials Before They Hit Production

Secrets Scanning: Stop Leaking Credentials Before They Hit Production

A leaked API key costs on average $1.2 million to remediate when you include breach response, customer notification, regulatory fines, and reputational damage. Secrets end up in git repositories constantly — not because developers are careless, but because the workflow makes it easy: you add a credential temporarily to test something, forget to remove it, and commit. The credential is now in git history forever, even if you delete the file in a subsequent commit.

Secrets scanning is the automated control that catches this. Run it at the right points in your pipeline and you stop the leak before it ever reaches a remote repository.

Why Git History Is Permanent

This is the fundamental thing developers don't fully internalize until they experience it: removing a file from git does not remove it from history. If someone clones your repository after the "fix" commit, they can still see the credential:

# An attacker or automated scanner can trivially do this:
git log --all --full-history -- config/database.yml
git show abc123:config/database.yml  # Show the file at that commit

# Or search all history for patterns
git log -p --all | grep -E 'password|secret|key' | head -50

The only complete remediation for a leaked secret is to revoke it. Every other approach — deleting the file, force-pushing, using git filter-repo — buys you time but doesn't guarantee the credential wasn't already scraped. GitHub, GitLab, and major git hosting providers have automated scanners that scan pushes in real-time; so do security researchers running tools against public repos.

Prevention is the only reliable strategy.

The Scanning Toolchain

Gitleaks

Gitleaks is the current gold standard for secrets scanning. It's written in Go, ships as a single binary, runs in under 5 seconds for most repositories, and maintains an excellent rule set covering 150+ secret types.

# Install
brew install gitleaks

# Or via Docker
docker pull zricethezav/gitleaks:latest

# Scan the current repo (all history)
gitleaks detect --source . --report-path=gitleaks-report.json

# Scan only staged files (for pre-commit)
gitleaks protect --staged

# Scan specific commits
gitleaks detect --source . --log-opts="HEAD~5..HEAD"

Configuration via .gitleaks.toml:

[extend]
# Extend the default rules rather than replace them
useDefault = true

[[rules]]
id = "custom-internal-api-key"
description = "Internal API key for our microservices"
regex = '''INTERNAL_API_KEY_[A-Z0-9]{32}'''
tags = ["api-key", "internal"]
severity = "CRITICAL"

[[rules]]
id = "postgres-connection-string"
description = "PostgreSQL connection string with credentials"
regex = '''postgres://[^:]+:[^@]+@[^/]+/\w+'''
tags = ["database", "postgresql"]
severity = "CRITICAL"

[allowlist]
description = "Global allowlist"
regexes = [
  # Test fixtures and examples
  '''EXAMPLE_KEY_DO_NOT_USE''',
  '''test_secret_for_unit_tests''',
]
paths = [
  "tests/fixtures/",
  "docs/examples/",
  "**/testdata/**",
]
commits = [
  # Already remediated — credential revoked 2026-01-15
  "abc123def456",
]

The output:

{
  "Description": "AWS Access Key",
  "StartLine": 12,
  "EndLine": 12,
  "StartColumn": 15,
  "EndColumn": 35,
  "Match": "AKIAIOSFODNN7EXAMPLE",
  "Secret": "AKIAIOSFODNN7EXAMPLE",
  "File": "config/aws.js",
  "SymlinkFile": "",
  "Commit": "a1b2c3d4",
  "Entropy": 3.684,
  "Author": "dev@example.com",
  "Email": "dev@example.com",
  "Date": "2026-05-15T10:23:45Z",
  "Message": "Add AWS config",
  "Tags": ["aws", "access-key"],
  "RuleID": "aws-access-key-id",
  "Fingerprint": "a1b2c3d4:config/aws.js:aws-access-key-id:12"
}

TruffleHog

TruffleHog's strength is its active verification — it doesn't just find patterns, it actually tests whether the credentials it finds are still valid. Finding a leaked secret is one thing; knowing it's still active is critical for prioritization.

# Install
pip install trufflehog3

# Or use the newer trufflehog v3 (Go binary)
brew install trufflesecurity/trufflehog/trufflehog

# Scan a git repo (with verification)
trufflehog git https://github.com/your-org/your-repo --only-verified

# Scan local filesystem
trufflehog filesystem /path/to/directory --only-verified

# Scan a Docker image
trufflehog docker --image your-image:tag

# Scan an S3 bucket
trufflehog s3 --bucket your-bucket-name

# CI-friendly JSON output
trufflehog git https://github.com/your-org/your-repo \
  --json \
  --only-verified \
  2>&1 | tee trufflehog-results.json

TruffleHog v3 supports over 700 detectors and verifies findings against 150+ services — AWS, GitHub, Slack, Stripe, SendGrid, and more. A verified finding means the credential was accepted by the target service as of scan time.

git-secrets

AWS Labs' git-secrets is the oldest tool in this category, designed primarily as a pre-commit hook. It's simpler than gitleaks but still useful for specific patterns:

# Install
brew install git-secrets

# Configure with AWS patterns
git secrets --register-aws

# Add custom patterns
git secrets --add 'PRIVATE_KEY=[A-Za-z0-9+/]{40}'
git secrets --add 'DB_PASSWORD=\S+'

# Install hooks in current repo
git secrets --install

# Scan current history
git secrets --scan-history

Pre-Commit Hooks: The First Line of Defense

Pre-commit hooks catch secrets before they're committed. This is fundamentally different from CI scanning — it happens on the developer's machine, before any remote sees the code.

Using the pre-commit Framework

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4
    hooks:
      - id: gitleaks
        name: Gitleaks Scan
        description: Detect secrets in staged files
        language: golang
        entry: gitleaks protect --staged --redact --config .gitleaks.toml
        pass_filenames: false
  
  - repo: https://github.com/trufflesecurity/trufflehog
    rev: v3.68.0
    hooks:
      - id: trufflehog
        name: TruffleHog
        language: golang
        entry: trufflehog git file://. --since-commit HEAD --only-verified --fail
        pass_filenames: false
        stages: ["commit"]
  
  # Also scan for common credential patterns
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: detect-private-key
      - id: detect-aws-credentials

Install and activate:

# Install pre-commit
pip install pre-commit

# Install the hooks defined in .pre-commit-config.yaml
pre-commit install

# Run against all files (initial setup)
pre-commit run --all-files

Enforcing pre-commit hooks across the team: The pre-commit framework is opt-in per developer. To enforce it organization-wide, add a check to CI:

# In CI: verify that pre-commit config exists and was run
- name: Verify pre-commit hooks installed
  run: |
    if [ ! -f .pre-commit-config.yaml ]; then
      echo "ERROR: .pre-commit-config.yaml not found"
      exit 1
    fi
    # Run pre-commit in CI as a check
    pip install pre-commit
    pre-commit run --all-files

GitHub Secret Scanning

GitHub's native secret scanning runs automatically on all public repositories and optionally on private repositories (GitHub Advanced Security). It covers 200+ token types from major providers.

Enable it in repository settings, or via API:

# Enable via GitHub API
curl -X PATCH \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  https://api.github.com/repos/your-org/your-repo \
  -d '{"security_and_analysis": {"secret_scanning": {"status": "enabled"}, "secret_scanning_push_protection": {"status": "enabled"}}}'

Push protection is the critical feature: it blocks pushes that contain known secrets before they're accepted by GitHub. The developer receives an immediate error:

remote: error: GH013: Repository rule violations found for refs/heads/main.
remote:
remote: - GITHUB PUSH PROTECTION
remote:   —————————————————————————————————————————
remote:   Resolve the following secrets before pushing:
remote:
remote:   AWS Access Key ID
remote:     Location: config/aws.js:12
remote:     Commit:   a1b2c3d4

Configure which secret types to scan for, and set up webhooks to receive notifications when secrets are found:

# List secret scanning alerts
curl -H "Authorization: Bearer $GITHUB_TOKEN" \
  https://api.github.com/repos/your-org/your-repo/secret-scanning/alerts

# Dismiss a false positive
curl -X PATCH \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  https://api.github.com/repos/your-org/your-repo/secret-scanning/alerts/42 \
  -d '{"state": "dismissed", "resolution": "false_positive", "resolution_comment": "This is a test fixture, not a real credential"}'

CI/CD Pipeline Integration

Beyond pre-commit hooks, run secrets scanning in CI as a hard gate:

# .github/workflows/security.yml
name: Security Scanning
on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  secrets-scan:
    name: Secrets Scanning
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for gitleaks
      
      - name: Run Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}  # For org-level scanning
        with:
          args: "--config .gitleaks.toml"
      
      - name: Run TruffleHog
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}
          head: HEAD
          extra_args: --only-verified --json
      
      - name: Upload results
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: secrets-scan-results
          path: |
            gitleaks-report.json
            trufflehog-results.json

Remediation Workflow

When a secret is found, follow this process without exception:

Step 1: Revoke immediately. Before anything else, rotate or revoke the credential. Assume it has been compromised. Go to the service (AWS console, GitHub settings, Stripe dashboard) and revoke the specific key found. Issue a replacement.

Step 2: Assess exposure window. Check your git history for when the credential was first committed. Check access logs for the affected service covering that window. For AWS keys, this means CloudTrail. For database credentials, check query logs.

Step 3: Clean git history (if needed). For credentials in private repos that were never exposed publicly, cleaning history reduces future risk:

# Install git-filter-repo (preferred over git filter-branch)
pip install git-filter-repo

# Remove specific string from all commits
git filter-repo --replace-text <(echo "ACTUAL_SECRET_VALUE==>REDACTED")

# Remove a file from all history
git filter-repo --path config/secrets.env --invert-paths

# Force-push (coordinate with team — everyone needs to re-clone)
git push --force --all

For public repos or repos shared with third parties: assume exposure regardless of history cleaning. The credential was already revoked in Step 1. History cleaning is cosmetic at this point.

Step 4: Document and post-mortem. Add the finding to your security incident log. Identify what allowed the secret to enter the codebase — was it a missing hook? A bypass (--no-verify)? Update your controls to prevent recurrence.

Managing False Positives

Secrets scanners generate false positives. Test data, example keys, and documentation snippets all look like real credentials to pattern matchers.

Inline suppression (gitleaks):

API_KEY = "test-key-for-unit-tests"  # gitleaks:allow
EXAMPLE_TOKEN = "example_token_replace_before_use"  # gitleaks:allow

Global allowlist in .gitleaks.toml:

[allowlist]
regexes = [
  # Test fixture patterns
  '''test[-_]?(key|secret|password|token)''',
  # Placeholder patterns
  '''(your|my|example|placeholder|dummy|fake)[-_](key|secret|token)''',
  # All-same-character strings (unlikely real credentials)
  '''[A-Za-z0-9]{1}(\1{19,})''',
]
paths = [
  "tests/",
  "**/testdata/",
  "docs/",
  "*.md",
]

Track your false positive rate. If more than 20% of findings are false positives, your rules need tuning — developers will start ignoring the scanner.

Measuring Effectiveness

Track these metrics to know whether your secrets scanning program is working:

  • Pre-commit catch rate: What percentage of secrets are caught before git push?
  • CI catch rate: What percentage reach CI before being blocked?
  • Time to detection: How long between a secret appearing in history and detection?
  • Time to remediation: How long from alert to revocation?
  • False positive rate per tool: Are developers suppressing legitimate findings?

For teams running HelpMeTest to monitor CI pipeline health, adding secrets scan job results to your monitoring gives you a complete picture — including trend data on whether your detection is improving or whether bypass patterns are emerging.

The goal is zero secrets reaching any remote repository. With pre-commit hooks, CI gates, and push protection, you can get there. The tooling exists; the main challenge is organizational discipline in maintaining the controls.

Read more

Start now free