The .env File Is Not a Security Strategy
Your secrets are probably more exposed than you think. Here's how to fix it.
The False Sense of Security
# You have a .env file with:
DATABASE_URL=postgres://user:password123@db:5432/myapp
JWT_SECRET=super_secret_key_abc123
STRIPE_SECRET=sk_live_51H3...
AWS_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
# And you added it to .gitignore ✅
# So you're safe, right?
# ❌ WRONG. Here's why:
How Secrets Leak (Even With .gitignore)
1. Committed Before .gitignore
# Timeline of a common mistake:
git init
echo "DB_PASSWORD=secret123" > .env
git add .
git commit -m "Initial commit" # Oops! .env is in git history!
echo ".env" >> .gitignore # Too late!
git add . && git commit -m "Add gitignore"
# The secret is STILL in git history forever
Fix: git filter-branch or BFG Repo Cleaner to remove from history.
2. Docker Images
# ❌ This copies .env into the image!
COPY . .
# Anyone who pulls your image can run: docker run --rm -it image env | grep SECRET
# Docker images are NOT private by default on many registries!
# ✅ Use environment variables or Docker secrets instead
ENV NODE_ENV=production
# Pass secrets at runtime only:
# docker run -e DB_PASSWORD=xxx myimage
3. Client-Side Bundling
// ❌ If you import/require .env in frontend code:
const API_KEY = process.env.API_KEY; // Gets BUNDLED into client JS!
// Anyone can View Source → see your key
// ✅ Only use env vars in server-side code
// For public keys that MUST be in frontend, accept the risk or use backend proxy
4. Logs and Error Trackers
// ❌ Logging sensitive data
app.use((err, req, res, next) => {
console.error(err); // Might include headers with auth tokens, DB URLs in stack traces
// These logs get sent to: Sentry, Datadog, CloudWatch, etc.
});
// ✅ Sanitize before logging
function sanitizeError(err) {
const sanitized = { ...err };
const sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth'];
for (const key of sensitiveKeys) {
if (sanitized.message?.toLowerCase().includes(key)) {
sanitized.message = '[REDACTED]';
}
}
return sanitized;
}
5. Debug Endpoints
// ❌ I've seen this in production code:
app.get('/debug/env', (req, res) => {
res.json(process.env); // DUMPS ALL ENV VARS!
});
// ❌ Or this:
app.get('/api/config', (req, res) => {
res.json({ dbUrl: process.env.DATABASE_URL }); // Leaks DB credentials
});
6. Backup Files and Swap Files
# Your editor creates temporary files:
.env.swp # Vim swap file
.env~ # Emacs backup
.env.bak # Your backup copy
.env.backup # Another backup
# If you don't exclude these, they might end up in:
# - Git (if you forget to add pattern)
# - Deployments (copying entire directory)
# - Public web server directories
The Proper Way to Handle Secrets
Layer 1: Prevention
# .gitignore — comprehensive
.env
.env.*
!.env.example
*.swp
*~
.bak
credentials*
secrets/
*.key
*.pem
# Pre-commit hook: Check for accidental secrets
npx detect-secrets > /dev/null
# Or use: git-secrets, gitleaks
npm install -g gitleaks
gitleaks protect # Install as pre-commit hook
Layer 2: Detection (If Prevention Fails)
# Scan git history for leaked secrets
gitleaks detect --source . -v
# Scan current directory
trufflehog file .
# GitHub: Enable secret scanning (Settings → Code security)
# GitLab: Push rules + Secret Detection
# AWS: GuardDuty + Macie
Layer 3: Rotation (Assume Compromise)
// Rotate keys regularly — automate this!
// Schedule: Every 90 days for most secrets
// When rotating:
// 1. Generate new key
// 2. Add new key alongside old one (graceful transition)
// 3. Update all services to use new key
// 4. Remove old key
// 5. Verify nothing broke
// For critical leaks:
// 1. Revoke IMMEDIATELY (Stripe dashboard, AWS console, etc.)
// 2. Rotate key
// 3. Audit access logs (was the key used maliciously?)
// 4. Document incident
Layer 4: Production Secret Management
// Option A: Environment variables at runtime (simplest)
// Set via: docker-compose.yml, Kubernetes secrets, CI/CD variables
// Never store in code or config files
// Option B: Secrets manager (for teams/production)
// HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault
// Example: Using environment-specific config
const config = {
development: {
dbUrl: 'postgres://localhost/dev',
jwtSecret: 'dev-secret-not-for-prod',
},
production: {
dbUrl: process.env.DATABASE_URL, // From vault/secrets manager
jwtSecret: process.env.JWT_SECRET,
},
}[process.env.NODE_ENV];
The .env File Checklist
□ .env in .gitignore (FIRST thing you do!)
□ .env.example committed (template without real values)
□ No .env in Docker images (use runtime vars)
□ No .env imported in client-side code
□ Editor backup files excluded (.swp, *~, .bak)
□ Pre-commit hooks scanning for secrets
⚠️ Git history scanned for accidentally committed secrets
□ Error logging sanitizes sensitive values
□ No debug endpoints exposing env vars
□ Keys rotated every 90 days
□ Incident response plan documented
Quick Tools
| Tool | Purpose |
|---|---|
gitleaks |
Scan git history for secrets |
detect-secrets |
Pre-commit secret detection |
trufflehog |
Scan repos for leaked credentials |
dotenv |
Load .env files safely |
zod |
Validate env vars at startup |
| Vault/SM | Production secret management |
Have you ever accidentally leaked a secret? How did you find out?
Follow @armorbreak for more security content.
Top comments (0)