DEV Community

Alex Chen
Alex Chen

Posted on

The .env File Is Not a Security Strategy

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

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
Enter fullscreen mode Exit fullscreen mode
# FIX: Add to .gitignore IMMEDIATELY
.env
.env.local
.env.*.local
!.env.example
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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
Enter fullscreen mode Exit fullscreen mode
# New team member just:
cp .env.example .env
# Fill in their own values
Enter fullscreen mode Exit fullscreen mode

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

The Complete Security Checklist

  • [ ] .env in .gitignore (ALWAYS, no exceptions)
  • [ ] .env.example committed (shows required vars without values)
  • [ ] No .env in Docker images
  • [ ] No secrets in client-side JavaScript
  • [ ] No secrets in error responses or logs
  • [ ] Git history scanned for accidentally committed secrets (git trufflehog or gitleaks)
  • [ ] 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 ==="
Enter fullscreen mode Exit fullscreen mode

What's the worst secret leak you've seen? Share (anonymously) below.

Follow @armorbreak for more security content.

Top comments (0)