Secrets scanning
Catch leaks at commit and PR time: rule sets, history scans, and rotation playbooks after true positives.
The SKILL should define scan scope (LFS, submodules, large-file exclusions), pinned rule versions, and CI failure policy; prioritize vendor-shaped secrets over generic high-entropy blobs.
When history hits, assess public exposure, revoke/rotate credentials, and retain audit logs; agent outputs should link to your incident runbooks.
gitleaks full configuration and pre-commit hook
The .gitleaks.toml below covers three custom rule types (AWS key, GitHub token, DB password), along with path exclusions and a false-positive allowlist.
# .gitleaks.toml — pin gitleaks v8.x
[extend]
useDefault = true
[[rules]]
id = "aws-access-key-custom"
description = "AWS Access Key ID"
regex = '''(?i)(aws[_\-\. ]?)(access[_\-\. ]?key[_\-\. ]?id|aki)[^a-z0-9]*(=|:|:=)[\s"']*([A-Z0-9]{20})'''
tags = ["key", "aws"]
severity = "critical"
[[rules]]
id = "github-personal-token"
description = "GitHub Personal Access Token (classic or fine-grained)"
regex = '''ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{82}'''
tags = ["token", "github"]
severity = "critical"
[[rules]]
id = "db-password-in-url"
description = "Database password in connection string"
regex = '''(postgres|mysql|mongodb)://[^:]+:([^@\s"']{8,})@'''
tags = ["password", "database"]
secretGroup = 2
severity = "high"
[allowlist]
description = "Global exclusion: test fixtures, documentation examples"
regexes = [
'''AKIAIOSFODNN7EXAMPLE''',
'''wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY''',
]
paths = [
'''.*_test\.go$''',
'''.*\.md$''',
'''tests/fixtures/.*''',
]
pre-commit hook configuration (.pre-commit-config.yaml)
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
args: ["--config=.gitleaks.toml", "--baseline-path=.gitleaks-baseline.json"]
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ["--baseline", ".secrets.baseline"]
exclude: package-lock\.json
- Pin tool versions in CI; explicitly declare whether submodules and LFS pointers are in scope.
- Failure policy: true positives block merges; allowlist changes require ticket linkage within a time-boxed window.
detect-secrets scan commands and baseline file
detect-secrets uses a baseline file to manage known false positives; new secrets cause CI to fail.
# Initialize baseline (first run — capture existing false positives)
detect-secrets scan --baseline .secrets.baseline \
--exclude-files '\.lock$' \
--exclude-files 'node_modules/'
# Audit new findings (interactive — mark false positives)
detect-secrets audit .secrets.baseline
# CI validation (non-zero exit if new secrets detected)
detect-secrets scan | \
python -c "import sys,json; d=json.load(sys.stdin); sys.exit(1 if d['results'] else 0)"
Baseline file format (.secrets.baseline key fields)
{
"version": "1.5.0",
"plugins_used": [
{"name": "AWSKeyDetector"},
{"name": "JwtTokenDetector"},
{"name": "KeywordDetector", "keyword_exclude": "example|test|fake|dummy"},
{"name": "HexHighEntropyString", "limit": 3.0},
{"name": "Base64HighEntropyString", "limit": 4.5}
],
"results": {
"config/database.yml": [
{
"type": "Secret Keyword",
"filename": "config/database.yml",
"hashed_secret": "3d20558fe59359...",
"is_verified": false,
"line_number": 12
}
]
},
"generated_at": "2025-01-01T00:00:00Z"
}
False-positive allowlist examples
False positives are managed via allowlists: each entry must include reason, owner, review cadence, and a traceable PR; global rule disables without comments are forbidden.
# gitleaks.toml — rule-level allowlist (minimum granularity)
[[rules]]
id = "github-personal-token"
# ... rule definition ...
[rules.allowlist]
description = "Placeholder token in test fixtures, reviewed in PR#421, re-review 2026-01-01"
regexes = ['ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX']
paths = ['tests/fixtures/auth_test\.go$']
commits = ["abc123def456"] # Exempt specific commit only (historical fix)
# gitleaks per-commit inline allowlist (line-level)
const API_KEY = "test-key-for-unit-tests"; // gitleaks:allow
- Keep granularity minimal: path-scoped, rule-ID-scoped, or reviewed regex—avoid
*wildcards. - Version with code and review; write expiration dates into entries or tickets to prevent "forever" exemptions.
- After fixing a true positive, remove the corresponding allowlist line—do not leave a "fixed" comment as a placeholder.
Secret leakage SOP (5 steps) and GitHub Actions
5-step emergency SOP after a true positive is confirmed:
# Step 1: Immediately revoke the secret (AWS example)
aws iam delete-access-key --access-key-id AKIAIOSFODNN7ABCDEF
# Step 2: Audit usage during the exposure window
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAIOSFODNN7ABCDEF \
--start-time $(date -d '30 days ago' --iso-8601) \
--query 'Events[*].{Time:EventTime,Name:EventName,Source:SourceIPAddress}'
# Step 3: Rotate new key (generate + store in Secrets Manager)
NEW_KEY=$(aws iam create-access-key --user-name deploy-bot)
aws secretsmanager put-secret-value \
--secret-id prod/deploy-bot-key \
--secret-string "$NEW_KEY"
# Step 4: Remove from git history (using BFG Repo-Cleaner)
bfg --replace-text secrets-to-remove.txt --no-blob-protection
git reflog expire --expire=now --all && git gc --prune=now --aggressive
git push --force --all && git push --force --tags
# Step 5: Notify + retain audit record
# In the security ticket: record exposure window, impact assessment, revocation timestamp, rotation confirmation
GitHub Actions secret scanning workflow
# .github/workflows/secret-scan.yml
name: Secret Scan
on:
push:
branches: ["main", "develop"]
pull_request:
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # scan full history
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_CONFIG: .gitleaks.toml
GITLEAKS_ENABLE_COMMENTS: true # comment on PR
detect-secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install detect-secrets==1.5.0
- run: |
detect-secrets scan --baseline .secrets.baseline
git diff --exit-code .secrets.baseline || \
(echo "::error::New secret detected, run detect-secrets audit" && exit 1)
Fixed order: classify true vs false positive, then rotate or allowlist; historical leaks follow incident response separately.
[ Local: pre-commit / pre-push scan ]
│
▼
[ CI / PR: full tree + history (per policy) ]
│
┌────────┴────────┐
▼ ▼
[ Rules: vendor shapes ] [ Heuristics: entropy / broad match ]
│ │
└────────┬────────┘
▼
[ Human / agent triage ]
│
┌────────┴────────┐
▼ ▼
[ True positive: rotate / revoke / audit ] [ False positive: allowlist proposal + PR review ]
│ │
└────────┬────────┘
▼
[ Merge: pin rule version + archive report ]
False-positive record template
The form below only assembles text in your browser—nothing is uploaded. Use it for PR descriptions or security ticket attachments.
Fill the fields, click Generate, then Copy; redact sensitive values before pasting.
SKILL snippet
---
name: secrets-scan
description: Configure gitleaks/detect-secrets, handle true positives, govern allowlists, with secret leakage SOP
---
# Steps
1. Verify .gitleaks.toml or equivalent: custom rules covering AWS/GitHub/DB secret types
2. Verify .pre-commit-config.yaml: gitleaks + detect-secrets hooks are configured
3. Verify .secrets.baseline: exists and has been reviewed with detect-secrets audit
4. CI/CD: secret-scan workflow runs on PR with fetch-depth=0 to scan full history
5. Allowlist: each entry has reason, owner, review date; no wildcard (*) global disables
6. High-entropy false positives: use path-scoped exclusions rather than disabling the whole rule
7. True positive response: revoke immediately → audit usage → rotate → clean history → retain audit record
8. Git history cleanup: use bfg or git-filter-repo; cannot rely on git reset
9. Allowlist expiry reminders: write into ticket or use scheduled review
10. Consolidate with SBOM and dependency scan alerts on a security dashboard
# Anti-patterns
- Do NOT commit real secrets to any repository, even private repos
- Do NOT use uncommented global allowlists (*)
- After fixing a true positive, the allowlist entry MUST be removed