DEV Community

Alex Chen
Alex Chen

Posted on

The .env File Is Not a Security Strategy

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:
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The Proper Way to Handle Secrets

Layer 1: Prevention

# .gitignore — comprehensive
.env
.env.*
!.env.example
*.swp
*~
.bak
credentials*
secrets/
*.key
*.pem
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)