Security

How Exposed Secrets Sneak Into Your Repo (and How Audits Find Them)

Nobody commits a secret on purpose

Every leaked credential we’ve ever found in an audit had the same origin story: a reasonable engineer, in a hurry, doing something that felt harmless at the time. Nobody opens a pull request titled “add production database password to repo.” And yet secrets in git are among the most common (and most immediately exploitable) findings in a code audit.

They’re exploitable because the attack is fully automated. Bots scan every public GitHub push within seconds of it landing; leaked AWS keys are routinely used within minutes. Private repos buy you time, not safety: every contractor, ex-employee, CI system, and laptop with a clone has a copy of your history, and repos have a way of becoming public later, through acquisition due diligence, open-sourcing a component, or a fork pushed to the wrong remote.

Here’s how secrets actually get in, and how an audit finds them.

The five ways secrets end up in git

1. The committed .env file

The classic. The project has a .env file, the .gitignore entry was added in commit 47, and the .env file was committed in commit 12. Removing it later doesn’t remove it from history. Variants: .env.local, .env.production, config/secrets.yml, terraform.tfvars.

2. The hardcoded fallback

const stripeKey = process.env.STRIPE_KEY || 'sk_live_51Hx...';

Written during a debugging session at 11pm to “just get it working,” then committed along with the actual fix. The fallback pattern is insidious because the code looks like it does the right thing (the environment variable is right there) and the literal after || slides past review.

3. Test fixtures and example configs

config.example.json that someone filled with real values instead of placeholders. Integration tests pointed at a real third-party sandbox using a real key. Seed scripts with a real admin password because “it’s just staging,” and staging shares credentials with production.

4. Documentation and comments

The README with a “quick start” section containing a working database URL. The code comment that says // temp token for testing: ghp_.... Jupyter notebooks are a special hazard: they save cell output, so a printed environment variable gets committed even when the code never contains it.

5. Git history itself

The one everyone forgets. The secret was committed in 2023, someone noticed, deleted the file, committed the deletion, and considered it handled. But git log --all still has it. So does every fork, every clone, and every CI cache. A deleted secret is a published secret. The only real remediation is rotation; the history cleanup (via git filter-repo or BFG) is secondary hygiene, useful mainly so the next scan comes back clean.

How an audit hunts for them

A manual audit layers three techniques, because each catches things the others miss.

Pattern scanning across full history. Tools like gitleaks and trufflehog scan every commit ever made, not just the current tree, for known key formats (AKIA... for AWS, sk_live_ for Stripe, ghp_ for GitHub, xoxb- for Slack) and for high-entropy strings that look like credentials. This is the automated pass, and it’s table stakes.

Reading the config surface by hand. Scanners have blind spots: internal service passwords with no recognizable format, base64-encoded values, secrets split across two variables and concatenated. So the auditor reads everything config-shaped: env handling, deploy scripts, CI YAML, Dockerfiles (a COPY .env . or a build ARG with a default value bakes secrets into image layers), and infrastructure-as-code.

Verifying the live ones. A finding of “string that looks like a key” is noise; “live production Stripe key, currently valid” is a fire alarm. Where it’s safe to do so (a read-only identity call, never anything mutating), an audit distinguishes active credentials from dead ones, because that changes the response from “clean up eventually” to “rotate today.” This triage is a big part of why manual audits find what scanners miss.

Found one? Do these in order

  1. Rotate first. Revoke the credential and issue a new one. Do this before any git surgery; the git surgery doesn’t un-leak anything.
  2. Check for use. Audit logs on the affected service: any access you can’t explain, from IPs you don’t recognize?
  3. Scrub history if warranted with git filter-repo, coordinating with everyone who has a clone, and remembering forks and CI caches exist.
  4. Fix the intake path so the same secret doesn’t come back next sprint; see below.

Keeping them out

Prevention is cheap and mostly friction-free:

  • Pre-commit scanning. gitleaks as a pre-commit hook catches the secret before it becomes history. Seconds per commit; the highest-ROI control on this list.
  • CI scanning as backstop. The same scan in CI, run against the diff on every PR, catches whoever skipped the hook.
  • A secrets manager, not env-file sharing. AWS Secrets Manager, Vault, Doppler, or even your platform’s built-in config (Fly, Vercel, Railway): anything beats Slacking .env files around, which is how secrets end up in laptops, message history, and eventually commits.
  • No fallback literals, ever. Fail loudly at startup if a required variable is missing. A crash on boot is a feature; a silent fallback to a live key is an incident.
  • Short-lived credentials where the platform supports them. OIDC-based cloud auth for CI instead of long-lived keys removes the most dangerous category entirely.

Most of these are an afternoon of setup. In our experience, the teams that leak secrets aren’t careless; they’re just teams where nobody was ever assigned that afternoon. It’s the same pattern behind most technical debt: not bad engineers, just unowned work.

Get a free code audit

Secret scanning across your full git history (plus the manual config review that catches what pattern-matching misses) is part of every audit we run, and the audit is genuinely free. A Webisoft engineer reviews your repository by hand and sends you a written findings report: what leaked, whether it’s live, what to rotate, and how to keep it from recurring. If you’ve never scanned your history, there’s a real chance something is in there from 2023. Get a free code audit and find out before someone else does.

FAQ

Frequently asked questions

We deleted the leaked file and committed the removal. Are we safe?

No. Git history still contains the secret, and so does every fork, clone, and CI cache. A deleted secret is a published secret; the only real remediation is rotating the credential, with history cleanup as secondary hygiene so the next scan comes back clean.

Does keeping the repo private make leaked secrets a non-issue?

It buys time, not safety. Every contractor, ex-employee, CI system, and laptop with a clone holds a copy of the history, and private repos have a way of becoming public later, through due diligence, open-sourcing a component, or a fork pushed to the wrong remote.

What should I do first when I find a live key in the repo?

Rotate it before anything else; git surgery does not un-leak anything. Then check the affected service's logs for access you cannot explain, scrub history if warranted while coordinating with everyone who has a clone, and fix the intake path so the same secret does not come back next sprint.

Will a scanner like gitleaks or trufflehog catch everything?

It catches known key formats and high-entropy strings across full history, which is table stakes, but it misses secrets with no recognizable format, base64-encoded values, and credentials split across variables and concatenated. That is why an audit also reads the config surface by hand: env handling, CI YAML, Dockerfiles, and deploy scripts.

What is the cheapest way to keep secrets out of git going forward?

A pre-commit scanning hook, which catches the secret before it becomes history and costs seconds per commit, backed by the same scan in CI for whoever skips the hook. Use a secrets manager instead of sharing env files around, and fail loudly at startup when a required variable is missing rather than falling back to a hardcoded literal.

Free · no strings shown later

Get your free code audit

A repo URL and two minutes of your time. A Webisoft engineer sends back written findings (security, tech debt, performance) with file-and-line specifics.

Request my free audit