.env Files Security Best Practices: Stop Leaking Secrets
The .env file is the most convenient and most dangerous file in any project. It holds database URLs, API keys, signing secrets, and authentication tokens in plaintext. One accidental git add . and every secret in that file is permanently embedded in your repository history, even if you delete it in the next commit.
GitHub's secret scanning service detects over 200 types of leaked credentials and reports that millions of secrets are exposed in public repositories every year. The majority originate from committed .env files. This guide covers every practical step to keep your dotenv files from becoming your biggest liability.
The Fundamentals: .gitignore Is Not Enough
Every developer knows to add .env to .gitignore. But this single line of defense fails in several predictable ways:
- Late addition. If you create the
.envfile before adding it to.gitignore, it may already be staged or committed. The.gitignorerule only prevents future additions, not retroactive ones. - Force adds. Running
git add -f .envoverrides the ignore rule. This happens during debugging sessions when developers are frustrated and adding files manually. - Renamed files. Your
.gitignoreblocks.envbut notenv.txt,.env.backup, orconfig.env. Developers create copies with different names and forget they are not ignored. - Nested directories. A
.envat the project root is ignored, butservices/api/.envmight not be if your.gitignorepattern does not use a recursive wildcard.
A comprehensive .gitignore pattern
# Block all .env variants
.env
.env.*
!.env.example
*.env
env.local
.secrets
credentials.json
The !.env.example exclusion is intentional. Your example file with placeholder values should be committed to document what variables the project requires.
Pre-Commit Hooks: The Safety Net
Relying on developers to never commit a .env file is relying on human perfection. Pre-commit hooks automate the check and block the commit before it happens.
Using git-secrets
AWS's git-secrets tool scans staged changes for patterns that look like secrets (AWS access keys, private keys, tokens). Install it globally and it runs automatically on every commit:
# Install
brew install git-secrets
# Register AWS patterns
git secrets --register-aws --global
# Add custom patterns for your keys
git secrets --add --global 'sk_live_[a-zA-Z0-9]{24}'
git secrets --add --global 'AKIA[0-9A-Z]{16}'
# Install the hook in your repo
git secrets --install
Using detect-secrets
Yelp's detect-secrets takes a different approach. It generates a baseline file listing known secrets (by hash, not value) and alerts you when new secrets appear in commits. This avoids false positives from existing, intentionally committed values:
# Create a baseline
detect-secrets scan > .secrets.baseline
# Add the pre-commit hook
detect-secrets-hook --baseline .secrets.baseline
Using Husky with lint-staged
For JavaScript projects, combine Husky and lint-staged to reject commits that include .env files:
// package.json
{
"lint-staged": {
".env*": ["echo 'ERROR: Attempting to commit .env file' && exit 1"]
}
}
What Goes in .env vs. What Does Not
Not everything belongs in a .env file, even if it is technically configuration:
Belongs in .env
- API keys and secret tokens
- Database connection strings
- Third-party service credentials
- Signing secrets and encryption keys
- OAuth client secrets
Does not belong in .env
- Non-secret configuration. Port numbers, feature flags, log levels, and API base URLs are not secrets. Put them in a committed config file.
- Large values. SSL certificates, private key files, and large JSON credentials should be stored as files referenced by a path in
.env, not pasted as multi-line values. - Derived values. If a value is computed from other environment variables, compute it in code rather than duplicating it in the
.envfile.
Encryption At Rest
Storing secrets in plaintext .env files on your local machine means anyone with file system access (malware, a stolen laptop, a compromised backup) has your keys. Encrypting your .env files adds a layer of protection.
SOPS (Secrets OPerationS)
Mozilla's SOPS encrypts files while preserving their structure. You can encrypt a .env file using a GPG key, an AWS KMS key, or age. The encrypted file is safe to commit because it cannot be decrypted without the key:
# Encrypt with age
sops --encrypt --age $(cat ~/.config/sops/age/keys.txt | grep "public" | cut -d: -f2 | tr -d ' ') .env > .env.enc
# Decrypt when needed
sops --decrypt .env.enc > .env
dotenvx
The dotenvx tool encrypts .env files natively and decrypts them at runtime. You commit the encrypted .env.vault file and store the decryption key as a single environment variable in your deployment platform:
# Encrypt your .env file
dotenvx encrypt
# Run your app with automatic decryption
dotenvx run -- node app.js
What to Do When You Accidentally Commit a .env File
If you committed a .env file, deleting it and committing the deletion is not sufficient. The secrets are still in your git history. Here is the complete remediation process:
- Rotate every secret immediately. This is the most important step. Do not spend time cleaning git history before rotating. Assume the secrets are compromised from the moment they were pushed.
- Remove from git history. Use
git filter-repo(preferred over the deprecatedgit filter-branch) or BFG Repo-Cleaner to rewrite history and remove the file from all commits. - Force push the cleaned history. This requires all collaborators to re-clone or reset their local copies.
- Invalidate GitHub caches. Contact GitHub support to clear cached views of the commit containing the secret. The commit may be visible in pull request diffs even after history rewriting.
- Audit for unauthorized usage. Check the access logs and usage dashboards for every service whose key was exposed. Look for unusual activity between the commit time and the rotation.
Prevention is always cheaper than remediation. A single committed
.envfile can take hours to fully remediate across git history, CI caches, deployment artifacts, and service audits.
Docker and .env Files
Docker Compose reads .env files natively, which creates additional exposure points:
- Build-time vs. run-time secrets. Never pass secrets as
ARGvalues in a Dockerfile. Build arguments are visible in the image layer history. Use Docker secrets or runtime environment injection instead. - Image layers. If you
COPY .env /app/.envin a Dockerfile, the.envfile is embedded in the image layer permanently, even if a laterRUN rm .envcommand deletes it. - Docker Compose environment files. Use the
env_filedirective to keep secrets separate from yourdocker-compose.yml, which is typically committed to version control.
# docker-compose.yml (safe to commit)
services:
api:
build: .
env_file:
- .env # NOT committed
Runtime Security
Even if your .env file is properly secured at rest, secrets can leak at runtime:
- Process listing. On shared machines,
ps auxcan show environment variables passed via command-line arguments. Use file-based injection instead ofENV_VAR=value command. - Error reporting. Exception tracking services (Sentry, Bugsnag) may capture environment variables in error context. Configure them to scrub sensitive fields.
- Server-side rendering. Frameworks like Next.js expose any environment variable prefixed with
NEXT_PUBLIC_to the client bundle. Never prefix a secret key withNEXT_PUBLIC_. - Logging. Structured logging that dumps request context may include environment variables. Use an allowlist approach for logged fields rather than logging everything and redacting.
The Modern Alternative: No .env Files at All
The most secure .env file is one that does not exist. Modern secret managers eliminate local files entirely:
- Doppler, Infisical, and 1Password CLI inject secrets directly into your process at runtime without writing them to disk.
- Vercel, Railway, and Fly.io manage environment variables through their dashboards with per-environment isolation.
- Browser extensions like keys.surf store and auto-fill API keys from an encrypted vault without ever writing them to a file.
The shift from file-based secrets to managed secrets is the single biggest improvement most teams can make to their secret hygiene. Files are leakable. Managed secrets with access controls and audit logs are not.
Checklist
- Comprehensive
.gitignorepatterns covering all.envvariants - Pre-commit hooks that scan for secrets before every commit
- Encrypted
.envfiles if you must use files at all - Separate
.envfiles per environment (dev, staging, production) - No secrets in Dockerfiles, build args, or image layers
- Runtime scrubbing in error reporters and loggers
- A tested remediation plan for accidental commits
- Migration path toward managed secret infrastructure
Your .env file is the single most sensitive file in your repository. Treat it that way, or accept the statistical certainty that its contents will eventually leak.
Recommended Security Tools
Go beyond dotenv best practices with these tools:
- YubiKey 5 NFC — Hardware Security Key — Protect the accounts that generate your API keys with phishing-proof hardware 2FA. Even if a .env file leaks, accounts behind a YubiKey stay locked.