
Environment Variable Security: 9 Mistakes Leaking Your Secrets
Environment variable security best practices to stop API keys and secrets from leaking in logs, repos, and client bundles.
Environment variables feel safe. They're not in your codebase, not in version control (supposedly), and they live quietly in your hosting dashboard. That false sense of security is exactly why they're one of the most common sources of leaked credentials — database passwords, API keys, JWT secrets, and third-party tokens end up in GitHub commits, server logs, and client-side JavaScript bundles far more often than teams realize.
Here are the mistakes that actually cause breaches, and the fixes that work in production.
1. Committing .env Files to Git
This is the classic. A developer runs git add ., doesn't notice .env wasn't ignored, and pushes a database password straight to a public (or soon-to-be-public) repository.
- Add
.env,.env.local, and.env.*.localto.gitignorebefore you create the file, not after. - Commit a
.env.examplewith placeholder keys and no real values, so teammates know what's expected without exposing secrets. - Run
git log -p | grep -i "API_KEY"or use a tool liketrufflehogorgitleaksto scan your entire history — not just the latest commit. A secret removed in a later commit is still sitting in git history.
If You've Already Leaked a Secret
- Rotate the credential immediately — assume it's compromised the moment it hits a public repo, even briefly.
- Don't just delete the file and commit again; the secret still exists in history. Use
git filter-repoor BFG Repo-Cleaner, then force-push. - Check provider logs (AWS CloudTrail, Stripe dashboard, etc.) for unauthorized usage during the exposure window.
2. Mixing Client-Side and Server-Side Variables
Frameworks like Next.js, Vite, and Create React App bake certain environment variables directly into the client-side bundle at build time — anything prefixed with NEXT_PUBLIC_, VITE_, or REACT_APP_. Developers sometimes assume "environment variable" automatically means "hidden," then prefix a secret key that way out of convenience.
- Never prefix a secret (API key, database URL, signing secret) with your framework's public-exposure prefix.
- Open your browser DevTools, go to the Sources tab, and search your built JS bundle for suspicious strings like
sk_live_,AKIA, or your internal API hostnames. If you find them, they're public — anyone can view-source your site and grab them. - Remember: anything shipped to the browser is public, full stop, regardless of how it's obfuscated or minified.
3. Logging Environment Variables (Even by Accident)
A common debugging habit — console.log(process.env) — is harmless locally and catastrophic in production if that log ends up in a centralized logging service, error tracker, or CI/CD output that other team members (or outside contractors) can access.
- Audit error-tracking integrations (Sentry, LogRocket, Datadog) to confirm they scrub environment variables and request headers by default — most do, but not all configurations enable it out of the box.
- Avoid printing full request objects or
process.envin any log statement, even temporarily. "Temporary" debug code has a habit of surviving into production. - Check your CI/CD pipeline logs (GitHub Actions, GitLab CI, CircleCI) — build logs frequently print full environment dumps when a script fails verbosely, and those logs can sit accessible for months.
4. Using the Same Secrets Across Environments
If your staging, development, and production environments all share the same API keys and database credentials, a breach in your least-secure environment (usually staging, which gets far less scrutiny) compromises production too.
- Generate distinct credentials per environment — separate database users, separate API keys, separate signing secrets.
- Apply the principle of least privilege: your staging database user shouldn't have the same permissions as production.
- Rotate secrets on a schedule (quarterly at minimum for anything sensitive) and immediately after any team member with access leaves.
5. Storing Secrets in Plaintext Config Files on the Server
Environment variables loaded from a .env file on a shared server are only as secure as the file permissions protecting them.
- Set restrictive file permissions:
chmod 600 .envso only the owning user can read or write it. - On shared hosting or multi-tenant servers, confirm other users/processes genuinely can't read your app's working directory.
- For anything beyond a small project, move to a dedicated secrets manager — AWS Secrets Manager, HashiCorp Vault, Doppler, or your cloud provider's native equivalent — rather than flat files.
6. No Secret Rotation Strategy
Many teams set an API key once and never touch it again, sometimes for years. If that key is ever exposed — through a log, a compromised laptop, a departing contractor — it remains valid indefinitely.
- Maintain an inventory of every secret, what it accesses, and who provisioned it.
- Set calendar reminders or automate rotation for high-privilege credentials (database admin, payment processor keys, cloud provider root keys).
- Use short-lived tokens where your provider supports them (OAuth refresh tokens, STS temporary credentials) instead of long-lived static keys.
7. Overly Broad Access Scopes
It's common to generate an API key with full account access because it's faster than configuring granular permissions. That convenience becomes a liability the moment the key leaks — a read-only analytics key leaking is an inconvenience; a full-admin key leaking is a breach.
- Scope every API key and database credential to only the permissions it actually needs.
- Use read-only database replicas for reporting or analytics tools that never need write access.
- Review third-party integrations (CI/CD, monitoring, analytics) annually — many accumulate more access than they currently use.
8. Ignoring Secrets in Docker Images and Build Artifacts
Baking environment variables into a Docker image with ENV or ARG at build time means the secret is permanently embedded in every layer of that image — including layers pushed to a registry.
- Pass secrets at runtime via orchestration tools (Kubernetes Secrets, Docker secrets, ECS task definitions) rather than baking them into the image.
- Run
docker history --no-truncto check whether secrets are visible in your image's build layers. - Use multi-stage builds so build-time secrets (like a private npm token) never make it into the final production image.
9. No Visibility Into What's Actually Exposed
Even disciplined teams can miss a leaked key in an old CORS misconfiguration, an exposed debug endpoint, or a response header that reveals more than it should. This is where external scanning closes the gap that manual code review misses.
A tool like WebSentry checks your live site's headers, CORS policy, cookies, and SSL configuration — the surface-level signals that often correlate with deeper environment and secrets-handling problems. If your CORS policy is wide open or your headers are leaking server details, it's worth asking what else might be improperly exposed.
Building a Habit, Not a One-Time Fix
Environment variable security isn't a checkbox you tick once during setup — it's an ongoing discipline that degrades the moment a new developer joins, a new integration gets added, or a deadline pressures someone to skip the review step.
- Add secret-scanning (gitleaks, trufflehog, GitHub's built-in secret scanning) as a mandatory pre-commit or CI step, not an optional one.
- Document your environment variable conventions so new team members know exactly what gets prefixed, scoped, and rotated.
- Periodically scan your live, deployed site — not just your codebase — since misconfigurations often show up in production headers and policies long after the code itself looks clean.
Run a free scan at websentry.dev to see how your site's security grade stacks up across SSL, headers, CSP, cookies, DNS, and CORS — and catch the configuration issues that often go hand-in-hand with poor secrets hygiene.
Check your own site
Run a free security scan and see if your site has the issues covered in this article. Results in under 30 seconds.