security9 min readpublished 2026-04-19

The .env file security checklist (15 items, 2026)

Everything you should check before pushing, sharing, or deploying a .env — from .gitignore entries to rotation policy. Printable 15-item checklist.

Print this, tape it to your monitor, and go through it every time you touch a .env. Most .env leaks don't come from sophisticated attacks — they come from checklist items nobody ran.

Fifteen items, grouped by phase.

Phase 1 — At rest (the file on disk)

1. .env, .env.* are git-ignored

Your .gitignore should contain at minimum:

.env
.env.local
.env.*.local
.env.development
.env.production
.env.test
!.env.example

The ! explicitly allows .env.example even though .env* excludes it. Without that line, your example file doesn't get committed — and nobody knows what keys to set.

2. .env.example is committed and up to date

Every required key exists in the example, with no values. Generate it fresh from your live .env whenever you add or remove keys — the browser generator does it in two clicks.

3. Nothing in git history leaks a value

# Catch common patterns across history
git log -p --all | grep -E "sk_live_|AKIA|ghp_|xox[bpa]"

If anything comes back, rotate the affected secret first, then scrub history with git filter-repo or BFG.

4. File permissions are reasonable (Unix)

chmod 600 .env     # read/write owner only
ls -la .env        # should show -rw-------

Stops casual reads by other users on shared dev machines. Overkill for a single-user laptop; real on shared VMs.

Phase 2 — At build time (what gets into your bundle)

5. No server secrets are client-exposed

In Next.js, anything prefixed with NEXT_PUBLIC_ is inlined into the browser bundle. In Vite, VITE_. Run this before every release:

# Anything that looks like a secret behind a public prefix = bug
grep -E "^(NEXT_PUBLIC_|VITE_).*(sk_|secret|password|key)" .env || echo "ok"

6. Leak check with known patterns

Run the scanner on your .env — it matches 17+ known secret shapes: AWS, Stripe, GitHub, OpenAI, Anthropic, Slack, private key blocks, Firebase, SendGrid, Mailgun, Twilio, JWTs. Two seconds; catches the obvious stuff gitleaks would catch in CI.

7. .env is not read from code that ships to the browser

Next.js handles this automatically. Custom bundler setups can accidentally resolve process.env in client code — grep your build output for suspicious strings:

grep -E "sk_live_|AKIA|DATABASE_URL" .next/static/chunks/*.js | head

Phase 3 — In transit (sharing + deployment)

8. Never share a plaintext .env over Slack, Discord, email

These are persistent chat logs with broad access. If you must, encrypt the file first (AES-GCM via WebCrypto) and send the passphrase through a different channel.

9. CI/CD doesn't log env values

Many CI systems redact "known" secret names automatically — but custom keys slip through. Verify by running:

# In your CI step, inspect what's being echoed
set -x
env | grep -v "^_"
set +x

If you see values in plaintext, mark those variables as "secret" in your CI config.

10. Host dashboard env vars have the right scope

Vercel, Railway, Fly, Render all let you scope env vars per environment (production / preview / development). Check that nothing prod-only is tagged for preview — preview URLs are sometimes publicly accessible.

Phase 4 — At runtime (the live app)

11. Validate env on boot, crash loud if anything's missing

Required keys should fail fast, not fail five requests later with a confusing stack trace.

import { z } from 'zod';
export const env = z.object({
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
}).parse(process.env);

12. Secrets don't appear in error messages or logs

Custom error classes should never serialize config. Audit every console.log(...config) / logger.info(...env)call. Use a log-redaction library (pino's redact, winston's format layer) to mask known keys automatically.

13. Error reporters (Sentry, Datadog) don't get env vars

Configure the Sentry beforeSend hook (or equivalent) to strip any *_SECRET, *_TOKEN, *_KEY values from the breadcrumbs and extras. Otherwise your secrets end up in the error dashboard.

Phase 5 — Ongoing (team hygiene)

14. Rotation plan exists + is tested

For every production secret, know:

  • Where it's stored (host dashboard / secrets manager)
  • How to rotate it (provider dashboard URL)
  • Which services depend on it
  • How long it takes to propagate

If you can't answer all four for every secret, you're one leaked key away from a 2 AM scramble. Test the rotation in staging quarterly.

15. Access is minimal — fewer people = fewer leak paths

Not every developer needs the production .env. Read access to secrets managers should be limited to ops + senior engineers. Use scoped personal tokens (GitHub PATs with only the needed scopes, AWS IAM per-person) instead of shared company credentials.

Automation

Treat this as CI, not manual steps. Add as pre-commit hooks:

The five-second version

If you only remember one thing:

  1. Git-ignore .env*
  2. Commit .env.example
  3. Generate secrets, never invent them
  4. Scan before you share
  5. Validate on boot, crash loud

Bookmark this page and check it every time you ship env-related changes. For the long-form rationale behind each rule, read the .env best practices guide.

Try these tools

Related guides