It took me four minutes to push the commit. It took three weeks to find out the commit was a problem.
The commit was innocent-looking: "refactor: move config loading to a shared module." Part of that refactor was a new config.example.env — a file I meant to commit, with placeholder values. What actually got committed was my real config.env, because on that day the file I had open in my editor was the live one, and git add config* matched both, and the diff view scrolled past the 40-line env block before I noticed.
What was in that file
Fourteen credentials. Not fourteen production payment keys — thank the gods — but fourteen live ones:
- Two staging database URLs with real passwords
- Six test-mode payment keys (Stripe, Adyen, and three sandboxes)
- Three internal service tokens
- A Sentry DSN with a project that could receive production events
- Two SMTP credentials for a domain I own
The repo was private. The branch was merged. The merge triggered a deploy. By the time I noticed, the file had been in the history of a repo that four teammates could read, that CI had checked out at least eleven times, and that had been backed up by the platform's standard retention.
The rotation, in order
I want to be precise about this, because "rotate the keys" is not a step, it's a project:
- Revoke first, replace second. I revoked all 14 before generating replacements. A new key alongside a live old key is not rotation — it's two keys.
- Database credentials came with a password policy change. The old password was 16 characters; the new one is 32, generated by a tool I don't type.
- Test-mode keys got rotated too. The instinct is to skip test keys. Don't. The test key for Stripe lets you enumerate test charges, hit test webhooks, and — in my case — the same test account was wired to a demo that a sales rep showed to a prospect.
- The Sentry DSN was the surprise. DSNs are "read-ish" — they let you send events to the project. The right response was still rotation, because a project that can receive events can be used for log injection and quota exhaustion.
- SMTP credentials, last and slowest. The mail provider's rotation flow required re-verifying the domain. Fourteen keys became a four-day effort because of this one.
What I changed afterward
The technical fix was three lines: config.env into .gitignore, config.env.example as the committed contract, and a scanner in CI.
The scanner is dotguard. It's the piece I'd have wanted most that day, because on the day of the incident, a linter ran, a type checker ran, and a dependency audit ran — and nothing looked at the file that contained fourteen live credentials.
# CI step, 30 seconds to add
- name: Scan for exposed secrets
run: npx @wuchunjie/dotguard .
Exit code 1 on a suspected secret, and the merge is impossible. On the day of the incident, that red build would have been the alarm, not a teammate's screenshot three weeks later.
The lesson that isn't "be careful"
"Be careful" was in the postmortem I wrote, and it's the sentence everyone ignores. The real lesson is that a private repo is not a safe place for credentials, and I need to be honest about why:
- Private repos get read. By teammates, by CI, by backup systems, by support tooling.
- Private repos leak. Access reviews lag. Departed employees linger in access lists.
- Private repos are still repos. They have history, and history is forever-ish.
The control that actually worked isn't a rule about care. It's that the scanner made "the file with real values" unmergeable, so the only version of the config file that could exist in the repo was the placeholder one. The four minutes I spent pushing that commit would now be four minutes of a red build and a two-line fix.
Rotate before you explain. Revoke before you replace. And put the scanner in the pipeline while the incident is still fresh enough to hurt.
npx @wuchunjie/dotguard
More Tools
| Tool | What it does | Command |
|---|---|---|
| scaffoldx-cli | Production-ready project templates in seconds | npx scaffoldx-cli |
| dotguard | Scan .env files for exposed secrets | npx @wuchunjie/dotguard |
| gitpulse | Git repo analytics in your terminal | npx @wuchunjie/gitpulse |
| snippetx | Terminal code snippet manager | npx @wuchunjie/snippetx |
If these save you time, consider buying me a coffee. All tools are MIT-licensed, zero-dependency, and run fully offline.
Top comments (0)