Practical credential hygiene for real projects — why secrets don't belong in source control, how .gitignore and environment variables actually protect you (and where they don't), and what to do if a secret is committed by mistake.
A committed secret doesn't just risk being seen by the wrong person today — once it's in Git history, it's there permanently unless you deliberately rewrite history (a genuinely painful, error-prone process), even if you delete it in a later commit. Treating credential hygiene as a first-class habit from day one is far cheaper than cleaning up after a leak.
Why .gitignore alone isn't a complete defense, how environment variables keep secrets out of source code, and the concrete steps to take if a secret does get committed by mistake.
Adding .env to .gitignore prevents Git from picking it up if it isn't already tracked. But if .env was ever explicitly git add-ed before the ignore rule existed, it's already in history — adding it to .gitignore afterward stops future changes to it from being tracked, but does not remove it from past commits. This is why checking git status and git diff --cached before every commit (see the Git workflow guide) matters even with a solid .gitignore in place — the ignore file is a safety net, not a substitute for looking.
Instead of writing a credential directly into source code, store it in an environment variable (commonly via a .env file, itself gitignored) and read it at runtime:
# .env (never committed)
DASHBOARD_ADMIN_USER=someuser
DASHBOARD_ADMIN_PASS=somesecretvalue
const user = process.env.DASHBOARD_ADMIN_USER;
const pass = process.env.DASHBOARD_ADMIN_PASS;
This project's own admin/dashboard authentication follows exactly this pattern — the code reads process.env.DASHBOARD_ADMIN_USER / DASHBOARD_ADMIN_PASS, and if they're not set, the route explicitly refuses to serve rather than defaulting to some open or hardcoded fallback. That "fail closed, not open" behavior is a deliberate, important detail — a misconfigured deployment should be unreachable, never unprotected.
A subtler credential-hygiene issue: comparing a supplied password to the real one using a plain === string comparison leaks timing information (the comparison returns faster the earlier a mismatch occurs), which is a real, documented side-channel in security-sensitive code. The fix is a constant-time comparison function, and it's worth knowing this exists even if you're not implementing auth yourself day to day — it's the kind of detail that separates "looks secure" from "is secure."
git diff --cached before pushing.git filter-repo) if the exposure was severe and the repository's history genuinely needs cleaning — recognize this as a separate, more involved effort from simply "fixing it going forward," and treat rotating the credential as complete on its own regardless of whether you rewrite history.Credential hygiene is one part of a broader deployment safety mindset — see Writing Idempotent Deployment Scripts for how this project's own installer scripts are designed to never require a hardcoded secret to run safely.
Part of: System Administrators, DevOps Beginners, Cloud Beginners