We Deleted the Key. Gitleaks Found It Anyway. Here's Why.
A developer panics. They committed a private key by mistake. They delete the file, push the commit, breathe a sigh of relief. They were never safe. We built exactly this scenario — 10 commits, 8 planted secrets, one RSA key deleted in the next commit after it was added — and ran SecurityClaw + Gitleaks against it. 13.2 milliseconds. 9 findings. 6 secrets found — including the one that "didn't exist anymore."
The Setup
The demo repo had 10 commits simulating a real small-team development workflow: initial setup, adding configuration files, committing credentials "temporarily", trying to clean up, and pushing code that still carried secrets in its history.
We planted 8 secrets covering the most common real-world exposure categories:
| Secret Type | File | Special Factor |
|---|---|---|
| AWS Access Key ID | config/aws.yml | Classic credential exposure |
| AWS Secret Access Key | config/aws.yml | Paired with Access Key ID above |
| PostgreSQL password | config/database.yml | YAML config pattern |
| GitHub PAT / JWT secret | src/auth/config.py | Dual-use credential |
| Slack Webhook URL | config/notifications.json | Dedicated detection rule |
| RSA Private Key | keys/deploy_key.pem | Deleted in the next commit |
| Stripe Secret Key | src/payments.py | Multi-rule match |
| Base64-encoded API key | .env | Obfuscated — entropy catch |
The scan command:
gitleaks detect --source . --report-format json --report-path results.json --verbose
Runtime: 13.2 milliseconds. 9 raw findings. 6 unique secrets identified.
What Gitleaks Found
| # | Secret Type | Found? | Rule | Why Notable |
|---|---|---|---|---|
| 1 | AWS Access Key ID | ✅ YES | aws-access-token | AKIA... prefix triggers dedicated rule |
| 2 | AWS Secret Access Key | ❌ MISSED | — | No rule for random 40-char strings |
| 3 | PostgreSQL password (YAML) | ❌ MISSED | — | password: in YAML not in default rules |
| 4 | GitHub PAT / JWT secret | ✅ YES | generic-api-key | Entropy + variable name context |
| 5 | Slack Webhook URL | ✅ YES | slack-webhook-url | Dedicated Slack rule fires immediately |
| 6 | RSA Private Key (DELETED) | ✅ YES | private-key | Found in git history — file gone from working tree |
| 7 | Stripe Secret Key | ✅ YES | stripe-access-token | 3 raw findings = 1 secret (multi-rule match) |
| 8 | Base64-encoded API key | ✅ YES | generic-api-key | Entropy analysis saw through the obfuscation |
Score: 6/8 unique secrets. 0 false positives (1 low-risk OAuth client ID flagged for triage).
The Two Moments That Matter
Moment 1: The Deleted Key Was Not Gone
The RSA private key was committed in commit 8172da4 —
the developer added it to the deploy configuration. In the very next commit, a182f88,
they deleted it with the message: "Remove deploy key from repo (oops, was committed by mistake)."
The file does not exist in the current working tree. ls keys/ returns nothing.
git status shows a clean repository.
Gitleaks found the key in commit 8172da4's diff. It's in the history.
It will always be in the history. Every past and future clone of this repo has it.
The lesson is not subtle: git rm does not remove secrets. git push already made them permanent.
The only remediation is to do two things simultaneously:
- Rotate the key immediately — assume it's compromised. It has been accessible to anyone with repo access since the original push.
- Rewrite git history — using
git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch keys/deploy_key.pem'or the faster BFG Repo-Cleaner. This requires a force-push and coordination with every team member who has a clone.
Most teams only do step 1. Step 2 is operationally painful. Which means the commit history stays poisoned, and any future clone pulls the deleted-but-present secret along with it.
Moment 2: Base64 Is Not Encryption
One .env entry was encoded:
INTERNAL_API_KEY=aW50ZXJuYWwtc2VjcmV0LWFwaS1rZXktMTIzNDU2Nzg5MA==
The developer had added a comment: "This is base64-encoded but still a secret (obfuscation != encryption)" —
demonstrating self-awareness that the encoding was cosmetic.
Gitleaks caught it anyway via entropy analysis. Base64 has characteristic
entropy around 5.7 bits per character — clearly distinct from natural language
(3.5–4.5 bits/char) or even hex strings (4.0 bits/char). Combined with the variable name
INTERNAL_API_KEY, the rule fired.
No pattern match needed. The obfuscation was transparent to entropy analysis. This matters because developers sometimes think encoding a secret reduces detection risk. It doesn't. It often increases it, because encoded secrets have higher entropy than plaintext ones.
The Honest Gaps
Two secrets were missed. Both are genuine limitations of gitleaks's default ruleset:
AWS Secret Access Key — No Pattern Rule
The AWS Key ID (AKIA... prefix) was caught by a dedicated rule.
The AWS Secret Access Key — a random 40-character alphanumeric string — was not.
There's no distinctive pattern in the secret itself. Gitleaks has no default rule for it
because random 40-character strings appear throughout codebases as hashes, tokens, and
other non-secret identifiers — a rule would generate enormous false positives.
TruffleHog's verified AWS detector handles this by checking both the key ID and secret together and verifying them against AWS's API. It's slower but catches what gitleaks misses here.
PostgreSQL Password in YAML — Not in Default Rules
A plaintext password in a YAML config file (password: Sup3rS3cr3t123)
was not flagged. Gitleaks's default rules don't cover the password: pattern
in YAML because it generates false positives on placeholder values and documentation examples.
The fix: add a custom rule to a .gitleaks.toml committed to your repo:
[[rules]]
id = "yaml-password"
description = "Password in YAML config file"
regex = '''(?i)password\s*:\s*.{8,}'''
path = '''.*\.(yml|yaml)$'''
entropy = 3.5
This rule combines pattern matching with a minimum entropy threshold to reduce false positives on placeholder text like "changeme" or "your_password_here".
Gitleaks vs. TruffleHog: When to Use Each
D1 was TruffleHog. D13 is gitleaks. They're not competing — they're complementary. SecurityClaw runs both because they catch different secrets in different ways.
| Feature | TruffleHog (D1) | Gitleaks (D13) |
|---|---|---|
| Speed (same repo) | ~2 seconds | 13.2ms |
| Detectors | 700+ specialized | ~150 rules |
| Live secret verification | Yes — checks if key still active | No |
| Custom rules | Config-based | .toml file |
| Git history scan | Yes | Yes |
| Entropy detection | Limited | Strong |
| CI/CD pipeline fit | Retrospective audits | Pre-commit hooks |
| D1/D13 result | 4/5 planted secrets | 6/8 planted secrets |
SecurityClaw's dual-scanner workflow: run gitleaks first (13ms, catches high-entropy and
pattern-based secrets, perfect for CI gates). Run TruffleHog for deeper retrospective audits
where live verification matters. Add custom rules to your .gitleaks.toml
for organisation-specific credential patterns.
For teams building security into their development workflow, the concepts underlying these tools — entropy analysis, regex-based detection, git internals — are covered well in Black Hat Python. And The Web Application Hacker's Handbook covers credential exposure in the broader context of web application assessment — where git history secrets often lead directly to application-layer compromise.
SecurityClaw Scorecard: D13
D13 adds a second campaign to SecurityClaw's supply-chain-security category. Together with D1 (TruffleHog) and D9 (supply-chain-scanner), the supply chain coverage now spans three complementary approaches: package integrity, malicious behaviour detection, and secrets in version control history.
| Metric | Value |
|---|---|
| Tool | gitleaks v8.18.0 |
| Target | 10-commit controlled git repo |
| Secrets planted | 8 (across 6 file types) |
| Secrets found | 6/8 unique (75%) |
| Scan time | 13.2ms |
| False positives | 0 (1 low-risk OAuth ID flagged for triage) |
| Campaign result | PASS |
If your git history has never been scanned for secrets, the question isn't whether there
are exposed credentials — it's how many and how old the exposure is.
gitleaks detect --source . --verbose answers that question in under 30 seconds
for most repositories. It's free, it's fast, and it reads history that your developers
thought was safely deleted.
Also worth reading alongside this demo: Penetration Testing by Georgia Weidman, which covers credential discovery as part of a complete assessment methodology — context for understanding where git history scanning fits in a real engagement.