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 -50The 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.jsonTruffleHog 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-historyPre-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-credentialsInstall 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-filesEnforcing 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-filesGitHub 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: a1b2c3d4Configure 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.jsonRemediation 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 --allFor 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:allowGlobal 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.