Leaked credentials remain the number-one entry vector in real incidents — and almost no leak is sophisticated: it's a .env committed to GitHub, a key pasted into a public log, or a token inside the frontend bundle. This article organizes where each secret must live by context, and what to do when (not if) something leaks.
The foundational rule: code never contains secrets
Three layers where secrets must NOT appear:
- Source code: neither hardcoded nor as constants. Repos get replicated, cloned, inherited.
- The repository: not even in old commits. Git remembers everything; deleting the file in the latest commit doesn't erase history.
- The frontend: everything reaching the browser is public by definition. An API key in your JS bundle is a gift-wrapped present for scrapers — bots crawl public repos hunting key patterns within minutes (GitHub scans automatically and notifies vendors: AWS revokes leaked keys without asking).
The correct separation: code reads process.env.DATABASE_URL / os.environ["..."]; real values live outside the code, injected by the runtime environment.
Environment variables: the operating standard
The 12-factor pattern governing practically all modern deployments:
# .env (LOCAL, never in git)
DATABASE_URL=postgres://app:secret@db:5432/app
STRIPE_KEY=sk_live_...
RESEND_API_KEY=re_...
Operating rules:
.envin.gitignorefrom minute zero — before the first commit, not after. Our .gitignore generator includes it by default alongside other sensitive artifacts..env.exampleDOES go to the repo: structure with empty or dummy keys documenting WHAT the app needs without revealing values.- One
.envper environment; production uses native platform variables (Vercel/Netlify/Docker secrets/K8s secrets), never loose files. - Context prefixes (
VITE_,NEXT_PUBLIC_,REACT_APP_) mark what DOES reach the browser: seeing a real key behind those prefixes is an active security bug.
Taxonomy: which secret fits where
| Secret | Frontend-safe? | Correct store |
|---|---|---|
| Payment API key (Stripe secret) | Never | Backend env |
| Publishable API key | Yes (that's its job) | Frontend env |
| JWT signing secret | Never | Backend env / KMS |
| CI/CD service token | Never | CI secrets |
| TLS private key | Never | Server / cert manager |
| Firebase public config | Yes | Frontend (with real security rules) |
Important nuance: "public" keys (Stripe publishable, Firebase config) are designed for exposure — their security relies on server-side restrictions (Firestore rules, allowed domains). Exposing them isn't a leak; exposing their private siblings is.
When it already leaked: emergency protocol
A secret in a public commit means assume it compromised NOW, even deleting it a second later — automated bots capture it in seconds. Protocol:
- Rotate/revoke immediately in the vendor panel. It's the only action that matters; everything else is forensics.
- Review usage logs: any anomalous activity between leak and rotation?
- Clean history ONLY if the repo is private and you keep it anyway (BFG Repo-Cleaner or git filter-repo) — on public repos it's pointless: already crawled. Rotation rules.
- Add preventive detection: gitleaks or trufflehog in pre-commit and CI block pushes containing key patterns.
And the awkward special case: keys inside Docker images. ENV values are baked into layers visible via docker history. Build secrets → build args used-and-deleted in one layer, or BuildKit mount secrets.
Secrets managers: when to level up
Environment variables suffice up to a point; their limits push toward dedicated platforms:
- Centralized rotation: changing one credential in Vault/AWS Secrets Manager/Doppler propagates to every consumer without redeploy.
- Audit: who read which secret when — impossible with files.
- Dynamic secrets: ephemeral DB credentials generated per use (TTL hours), shrinking the value of any theft.
- Encryption at rest and role-based access: secrets don't sleep in plaintext on shared disks.
For personal projects, well-stored env vars + encrypted backup suffice. The ladder climbs with team size and service count.
Cheap daily hygiene
- Never paste tokens into issues, PRs, Slack or commit messages.
- Logs: never log authorization headers or full bodies of sensitive endpoints.
- Keep
.env.exampleupdated when adding new variables. - Periodic review: occasionally run
git log -p -- '*.env*' '*/config/*', and gitleaks over all history (gitleaks detect --log-opts="--all").
FAQ
Is my private repo safe? Relatively: still a single point of failure (stolen laptop, compromised account, departing collaborator). Secrets live better outside the repo even when private.
Encrypting .env inside the repo (git-crypt, SOPS)? A legitimate mature solution for teams: repo carries the encrypted file, key lives in KMS/cloud. Extra complexity justified only with multiple environments and large teams.
What about third-party client keys (maps, analytics)? Restrict by domain/referrer in the vendor panel and budget quota — that's their intended design. Ones that don't allow restriction shouldn't be there.
Make sure your .env never enters the repo with our .gitignore generator, free and right in your browser.