Securing .env Files & Environment Variables: The Definitive Guide
The .env File Problem
Every week, GitHub detects millions of hardcoded secrets pushed to repositories. The leading source? .env files that were accidentally committed.
| Source | Secrets Leaked (2025) |
|---|---|
| .env files committed to Git | 42% |
| Hardcoded in source code | 28% |
| Exposed in CI/CD logs | 15% |
| Shared via Slack/email | 9% |
| Docker images | 6% |
Source: GitGuardian State of Secrets Sprawl 2025
Rule 1: Never Commit .env Files
# .gitignore — MUST include these
.env
.env.local
.env.*.local
.env.production
.env.staging
*.pem
*.key
What If You Already Committed a .env?
# Remove from tracking (keeps the file locally)
git rm --cached .env
echo '.env' >> .gitignore
git add .gitignore
git commit -m "Remove .env from tracking"
# ⚠️ The secret is STILL in Git history!
# You must rotate every credential that was exposed
# To fully remove from history (destructive):
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch .env' \
--prune-empty --tag-name-filter cat -- --all
git push origin --force --all
Critical: Even after removing the file from history, consider every secret in that file compromised. Rotate all credentials immediately.
Rule 2: Use .env.example (Not .env) for Documentation
# .env.example — Commit this (no real values!)
DATABASE_URL=postgres://user:password@localhost:5432/myapp
REDIS_URL=redis://localhost:6379
JWT_SECRET=generate-a-random-secret-here
STRIPE_SECRET_KEY=sk_test_your_test_key
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
# .env.local — Never commit this (actual secrets)
DATABASE_URL=postgres://admin:r3alP@ssw0rd@prod-db.internal:5432/myapp
REDIS_URL=redis://:auth-token@redis.internal:6379
JWT_SECRET=a1b2c3d4e5f6...64-char-random-string
STRIPE_SECRET_KEY=sk_live_51ABC123DEF456...
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Rule 3: Validate Environment Variables at Startup
// lib/env.ts — Validate ALL env vars on startup
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
JWT_SECRET: z.string().min(32, 'JWT secret must be at least 32 characters'),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
NODE_ENV: z.enum(['development', 'staging', 'production']),
PORT: z.coerce.number().default(3000),
});
// This throws on startup if ANY variable is missing or invalid
export const env = envSchema.parse(process.env);
// Usage — type-safe, validated access
import { env } from '@/lib/env';
const db = new PrismaClient({ datasources: { db: { url: env.DATABASE_URL } } });
// If DATABASE_URL is missing, the app fails at startup — not at runtime
Rule 4: Use a Secrets Vault for Production
HashiCorp Vault
# Store secrets
vault kv put secret/myapp \
database_url="postgres://..." \
jwt_secret="..."
# Read in application
vault kv get -format=json secret/myapp | jq -r '.data.data.database_url'
AWS Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: 'us-east-1' });
async function getSecret(name: string): Promise<Record<string, string>> {
const response = await client.send(
new GetSecretValueCommand({ SecretId: name })
);
return JSON.parse(response.SecretString!);
}
// Usage
const secrets = await getSecret('myapp/production');
const db = new PrismaClient({ datasources: { db: { url: secrets.DATABASE_URL } } });
Rule 5: Never Log Environment Variables
// ❌ DANGEROUS — secrets end up in log files
console.log('Config:', process.env);
console.log('DB URL:', process.env.DATABASE_URL);
console.log('Starting with JWT secret:', process.env.JWT_SECRET);
// ✅ Log only safe information
console.log('Starting server on port:', process.env.PORT);
console.log('Environment:', process.env.NODE_ENV);
console.log('Database connected:', !!process.env.DATABASE_URL);
Rule 6: Framework-Specific Pitfalls
Next.js
# ❌ NEXT_PUBLIC_ variables are embedded in CLIENT-SIDE JavaScript
NEXT_PUBLIC_API_KEY=sk_secret_key # EXPOSED TO BROWSER!
# ✅ Server-only variables (no NEXT_PUBLIC_ prefix)
DATABASE_URL=postgres://...
JWT_SECRET=...
# ✅ Only public-safe values get NEXT_PUBLIC_
NEXT_PUBLIC_APP_URL=https://myapp.com
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
Docker
# ❌ NEVER bake secrets into images
ENV DATABASE_URL=postgres://admin:password@db/myapp
# ✅ Use runtime injection
# docker run -e DATABASE_URL=postgres://... myapp
# ✅ Or Docker Secrets
# docker secret create db_url ./secret.txt
Secret Rotation Checklist
When rotating secrets, follow this order:
- Generate new secret
- Update in vault/secrets manager
- Deploy application with new secret
- Verify application works
- Revoke old secret
- Audit for any remaining usage of old secret
# Generate strong secrets
openssl rand -hex 32 # 64-char hex string
openssl rand -base64 32 # 44-char base64 string
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Environment Variable Security Checklist
-
.envin.gitignore— verified in every repo -
.env.examplecommitted with placeholder values - Environment variables validated at startup (Zod)
- Production secrets in vault (AWS Secrets Manager, Vault, etc.)
- No
NEXT_PUBLIC_prefix on server-only secrets - No secrets in Dockerfiles or docker-compose.yml
- No secrets logged via console.log or error handlers
- Secret rotation schedule (quarterly minimum)
- GitHub secret scanning alerts enabled
- Pre-commit hooks to detect secrets (gitleaks, trufflehog)
Advertisement
Free Security Tools
Try our tools now
Expert Services
Get professional help
OWASP Top 10
Learn the top risks
Related Articles
DevSecOps: The Complete Guide 2025-2026
Master DevSecOps with comprehensive practices, automation strategies, real-world examples, and the latest trends shaping secure development in 2025.
Shift-Left Security: How to Catch 85% of Vulnerabilities Before Production
Shift-left security moves security testing earlier in the SDLC — from production firefighting to design-time prevention. This guide shows how to implement security in requirements, design, coding, and CI/CD with measurable results.
IaC Security: Securing Terraform, Docker & Kubernetes Before Deployment
67% of IaC templates contain at least one misconfiguration. This guide covers Terraform security scanning, Docker hardening, Kubernetes RBAC, OPA policies, and automated IaC security in CI/CD pipelines.