A previous article here covered setting up gitleaks for homelab secret scanning - the setup, the pre-commit hook, getting CI to fail on new commits that contain secrets. The setup was correct. The tool was running. The CI was green. And it had been quietly suppressing a live production credential for months.
This is the follow-on story: not about getting gitleaks running, but about the specific way a baseline file breaks the guarantees you think you have once it’s in place.
View the complete homelab infrastructure source on GitHub 🐙
What a Baseline File Does (and Is Supposed to Do)
When gitleaks first runs on an existing repo, it finds every secret-shaped string in the full git history - including secrets that were introduced years ago, rotated long since, and are completely inert. Flagging those in CI creates noise that causes developers to tune out gitleaks entirely, which is worse than not having it.
The baseline workflow is the standard answer: run gitleaks on the current state, export all findings to a JSON file, commit that file to the repo, and tell gitleaks to suppress any finding that already appears in the baseline. Future commits that introduce new secrets still fail; old known-inert findings don’t.
# Generate baseline from current HEAD
gitleaks detect --report-format json --report-path .gitleaks-baseline.json
# Tell gitleaks to use it
gitleaks detect --baseline-path .gitleaks-baseline.json
The assumption embedded in this workflow: findings that appear in the baseline are inert. They were there before the baseline was generated; they’ve been there; they’re known.
The Assumption That Broke It
The baseline was generated at a point when the repo contained Garage’s rpc_secret and admin_token committed in a YAML file. Those were real production values - the cluster was live, using those exact secrets - but the baseline suppression treated them as “known, reviewed, not a problem.”
The commit that introduced them had happened a few weeks before the baseline was generated. The baseline capture happened to include them. From that point forward, gitleaks considered them baseline findings and did not alert on them.
// .gitleaks-baseline.json - what it actually contained
{
"Description": "Generic API Key",
"StartLine": 8,
"Match": "rpc_secret: \"REDACTED\"",
"Secret": "REDACTED",
"File": "kubernetes/system/garage/garage.yml",
...
}
The finding was correctly identified as a secret. It just wasn’t being acted on because the baseline said it was known.
How the Finding Was Caught
The finding surfaced during a security audit pass - specifically, a manual re-review of the baseline file itself to check what it was suppressing, not a gitleaks run. The review question was: “for each finding in this baseline, has the secret actually been rotated, or did we just suppress it?”
Garage’s rpc_secret and admin_token had not been rotated. The values in the baseline were the same values currently running in production. The baseline was suppressing an active, live credential committed to a public repository.
The audit entry:
SEC-013: Garage rpc_secret and admin_token committed to public repo
Status: CRITICAL - values unrotated since 2026-06-03 commit
Suppressed by: .gitleaks-baseline.json (line 47, line 89)
Action: rotate both via Vault, force ExternalSecret resync, restart Garage
The Remediation
Rotation via Vault (where the secrets were already supposed to live, per the ExternalSecret setup):
# Write new secrets to Vault
vault kv patch secret/garage \
rpc_secret="$(openssl rand -hex 32)" \
admin_token="$(openssl rand -hex 32)"
# Force ExternalSecret to re-sync
kubectl annotate externalsecret garage-secret -n system \
force-sync="$(date +%s)" --overwrite
# Verify new secret picked up
kubectl get secret garage-secret -n system -o jsonpath='{.data.rpc_secret}' | base64 -d
# Restart Garage to pick up new RPC secret
kubectl rollout restart deployment/garage -n system
# Verify RPC handshake succeeded on new secret
kubectl logs -l app=garage -n system | grep -i "rpc\|handshake\|connect" | tail -20
After rotation, the baseline file was updated to remove the (now-rotated) findings. The new baseline only contains findings where the secret is confirmed rotated or confirmed not a real secret.
The Structural Fix: Baseline Review as Part of the Rotation Workflow
The root problem isn’t that baseline files exist - they’re genuinely useful. The problem is that “add to baseline” and “rotated and safe” are two completely different states that the workflow treats identically. Every finding in a baseline should have a documented reason it’s suppressed, and “it was already there when I set up gitleaks” is not a sufficient reason.
The updated workflow:
# Never auto-add to baseline without a verified status
# Each entry in .gitleaks-baseline.json now has a comment in the adjacent audit doc:
# - ROTATED: secret value changed, old value in history is inert
# - FALSE_POSITIVE: not actually a secret (test value, placeholder, etc.)
# - PENDING_ROTATION: known issue, rotation scheduled, date logged
The last category - PENDING_ROTATION - is the honest version of what the original baseline was doing. The difference is that it’s explicit, it has a date, and it shows up in the audit document as an open finding rather than a silently-closed one.
The concrete check to run against any existing gitleaks baseline:
# For each secret in the baseline, check if it's still in the live cluster:
# 1. Get the secret value from the baseline
# 2. Check Vault / ExternalSecret / live Secret for the current value
# 3. If they match → not rotated, baseline is suppressing a live credential
jq -r '.[].Secret' .gitleaks-baseline.json | while read secret; do
echo "Checking: ${secret:0:8}..."
# grep your Vault / k8s secrets for this value
done
This check doesn’t fit neatly into automated CI - it requires access to the live secret values, not just the git history. It belongs in a periodic manual audit, not a commit hook. But it’s the only check that answers the actual question: not “does gitleaks find new secrets” but “are the secrets it’s ignoring actually safe to ignore.”
The same class of problem appears in Azure at scale: Azure Policy compliance reports show a resource as “Compliant” because it was exempted when the policy was first assigned, not because the underlying condition was remediated. The exemption and the fix look identical in the dashboard. Periodic review of exemptions - exactly like periodic review of a gitleaks baseline - is the only way to tell the difference.