← All write-ups

Scanning for secrets before code goes public

Details are generalized: no hostnames, addresses, identifiers or credentials.

How this was built: I design, direct and review the work, and build it hands-on with Claude, an AI coding assistant. The decisions, and the homelab, are mine.

Pattern scanners are good at finding secrets that look like secrets. Before years of homelab scripts went into a repository, and before this site's repository went public, I needed a method for the ones that don't.

The context

I consolidated scattered homelab projects into one version-controlled monorepo, importing their git history. This website's repository is public, so anything pushed to it is published. Every file and every commit had to be clean before the first push. Removing a secret from the latest version of a file doesn't remove it from the history.

Why one scanner isn't enough

Tools like gitleaks recognize secrets by shape: known prefixes such as AKIA… or ghp_…, high-entropy strings, and assignments like password = "…". Real code also carries credentials with no shape at all. A short password passed straight into a function call looks like any other string. So does a key embedded in a URL, or a token left in a comment. A pattern scanner can't flag what it can't recognize.

The method

  1. Pattern scan. gitleaks over the working tree and every commit in history.
  2. Exact-value sweep. Take every secret actually stored on the machine (each service's credential file), of any length, and search for that literal value across all files and the full git log -p. If you already know the secret, you don't need to guess its shape. The sweep reports matches without ever printing the value.
  3. Classify by masked shape. Plenty of hits aren't secrets: IDs, IP addresses and time zones all look suspicious to a scanner. Review them masked, never in full.
  4. Scan what will be committed, not what's on disk. The staged version of a file (git show :path) is what gets pushed, and it can differ from the working copy.
  5. Read the tracked-file list by eye. It's cheap, it's the last step, and it catches whole categories the automated passes are blind to.
  6. Rewrite history for anything found in it. Use git filter-repo --replace-text, then verify the value appears zero times in the rewritten history.
  7. Rotate anything that was ever exposed. Scrubbing a repository doesn't reach backups, sync targets or logs that already hold a copy.

Keeping it clean afterwards

Scan output is a leak path too

A secret scanner that prints what it finds copies the secret into terminal scrollback, CI logs and session transcripts, which are often more widely synced than the code itself. Three rules:

Takeaways