The .env File Is Not a Security Strategy
Every developer uses .env files. Almost everyone does it wrong.
What .env Files Are For
# .env — environment variables for local development
DATABASE_URL=postgresql://localhost:5432/myapp
SECRET_KEY=dev-secret-key-change-in-prod
API_KEY=sk-test-1234567890
NODE_ENV=development
PORT=3000
Purpose: Keep secrets out of code and git. Simple, effective.
The problem: Most people treat .env as a security solution. It's not.
The 7 Ways Your .env File Gets Exposed
1. Committed to Git (The #1 Mistake)
# This happens ALL THE TIME:
$ git add .
$ git commit -m "initial setup"
$ git push
# Oops! .env is now in git history FOREVER
# Even if you delete it next commit, it's in the history
# FIX: Add to .gitignore IMMEDIATELY
.env
.env.local
.env.*.local
!.env.example
2. In Docker Images
# ❌ BAD: COPY .env into image
COPY .env . # Secret baked into image layers!
RUN npm install
# ✅ GOOD: Use build args or runtime env
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL # Set at docker run time
# Or use Docker secrets (swarm mode)
# Or use Kubernetes ConfigMaps + Secrets
3. In Client-Side Code
// ❌ NEVER do this:
const API_KEY = process.env.REACT_APP_STRIPE_KEY;
// This ends up in the browser bundle! Anyone can View Source → find it.
// ✅ Instead: Use a backend proxy
// Frontend calls your API → API calls Stripe with real key
app.post('/api/create-checkout', async (req, res) => {
const session = await stripe.checkout.sessions.create({
// Server-side only — key never leaves server
});
});
4. In Error Messages
// ❌ Leaking config in errors:
app.use((err, req, res, _next) => {
res.status(500).json({
error: err.message,
// Oops! err.message might contain "Connection to DB at postgres://user:pass@..."
stack: err.stack,
env: process.env // EVERYTHING exposed!
});
});
// ✅ Clean error responses:
app.use((err, req, res, _next) => {
const requestId = req.id || crypto.randomUUID();
console.error(`[${requestId}] ${err.message}`, { stack: err.stack });
res.status(err.statusCode || 500).json({
error: {
message: 'Internal server error',
requestId // Support can look up the actual error
}
});
});
5. In Logs
// ❌ Logging sensitive data:
app.use((req, _res, next) => {
console.log(`${req.method} ${req.url} ${JSON.stringify(req.body)}`);
// If req.body contains password/token → it's in logs forever
next();
});
// ✅ Sanitize log output:
function sanitize(obj) {
const sensitive = ['password', 'token', 'secret', 'key', 'credit_card', 'ssn'];
const result = { ...obj };
for (const key of Object.keys(result)) {
if (sensitive.some(s => key.toLowerCase().includes(s))) {
result[key] = '[REDACTED]';
}
}
return result;
}
6. Shared Via Screenshots/Chat
# Slack message from yesterday:
"Hey, my .env looks like this, what's wrong?"
[screenshot showing DATABASE_URL=postgres://admin:password123@...]
Screenshots of terminals/code are the #1 cause of accidental secret leaks in teams.
7. Public Repos With History
# You removed .env from current code...
git rm .env
git commit -m "remove .env"
# ...but it's still in git history!
git show HEAD~1:.env # Still accessible!
# Anyone who cloned before your fix has it.
# GitHub's "Secret scanning" might catch it, but don't rely on it.
Fix: git filter-repo or BFG Repo Cleaner to purge history. Then rotate ALL compromised keys immediately.
The Right Way to Handle Secrets
Development (.env is fine here)
# .env (gitignored)
DATABASE_URL=postgresql://localhost:5432/myapp_dev
SECRET_KEY=insecure-dev-key-not-for-production
# .env.example (committed to git — shows what variables are needed)
DATABASE_URL=
SECRET_KEY=
API_KEY=
PORT=3000
# New team member just:
cp .env.example .env
# Fill in their own values
Staging & Production (NEVER use .env files)
Option A: Platform environment variables (recommended)
─────────────────────────────────────────────────
Vercel/Railway/Render/GitHub Actions UI:
Set DATABASE_URL = postgres://...
Set SECRET_KEY = xxx
Code reads: process.env.DATABASE_URL
No .env file needed. No file to leak.
Option B: Secrets manager (for serious apps)
──────────────────────────────────────
AWS Parameter Store / HashiCorp Vault:
aws ssm get-parameter --name /prod/DATABASE_URL --with-decryption
Secrets encrypted at rest. Audit trail. Rotation support.
Option C: CI/CD with encrypted secrets
──────────────────────────────────────
GitHub Actions:
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Encrypted in repo settings. Never visible in logs.
The Complete Security Checklist
- [ ]
.envin.gitignore(ALWAYS, no exceptions) - [ ]
.env.examplecommitted (shows required vars without values) - [ ] No
.envin Docker images - [ ] No secrets in client-side JavaScript
- [ ] No secrets in error responses or logs
- [ ] Git history scanned for accidentally committed secrets (
git trufflehogorgitleaks) - [ ] Production secrets in platform env vars or secrets manager (not .env files)
- [ ] Different keys per environment (dev/staging/prod)
- [ ] Key rotation plan documented
- [ ] Team trained on secret handling (especially screenshots!)
Quick Audit Script
#!/bin/bash
# audit-secrets.sh — Run this in any project directory
echo "=== Secret Leak Audit ==="
# Check if .env is gitignored
if grep -q "^\.env$" .gitignore 2>/dev/null; then
echo "✅ .env in .gitignore"
else
echo "❌ .env NOT in .gitignore!"
fi
# Check git history for .env commits
if git log --all --full-history -- ".env" | head -1 | grep -q "commit"; then
echo "❌ .env found in git history!"
else
echo "✅ .env not in git history"
fi
# Check for hardcoded secrets in source code
echo "--- Checking source code for secrets ---"
grep -rn "password\|secret\|api_key\|apikey\|token" --include="*.js" --include="*.ts" \
--exclude-dir=node_modules --exclude-dir=.git 2>/dev/null | \
grep -v "process\.env\|console\.\|// \|require(" | head -10
# Check for .env in Dockerfile
if grep -q "\.env" Dockerfile 2>/dev/null; then
echo "⚠️ .env referenced in Dockerfile"
else
echo "✅ No .env reference in Dockerfile"
fi
echo "=== Done ==="
What's the worst secret leak you've seen? Share (anonymously) below.
Follow @armorbreak for more security content.
Top comments (0)