Securing .env Files & Environment Variables: The Definitive Guide
On this page
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)
Published by SecureCodeReviews
This article is part of our original AI security and cybersecurity content library. We show publish and update dates, keep company and policy pages public, and update important guidance when material changes affect readers.
Want an expert review before this issue reaches production?
We combine manual code review with AppSec tooling to find vulnerabilities, logic flaws, and insecure patterns before release or audit deadlines.
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.