All posts
5 min read

Why .env Files Leak and How to Stop It

securitybest-practicesenv-vars

Every developer has a .env file. Most developers have accidentally committed one. GitHub's secret scanning program reports that millions of secrets are pushed to public repositories every year. The majority of those come from dotenv files.

The .env file was never designed for security. It was designed for convenience. And that gap between convenience and safety is where leaks happen.

How .env files end up in the wrong places

1. Committed to git

The most common leak. You create .env in your project root, forget to add it to .gitignore, and push it to GitHub. Even if the repo is private today, it might not be tomorrow. And git history is forever -- removing the file in a later commit doesn't delete it from the history.

# This happens more than you'd think
git add .
git commit -m "initial commit"
git push origin main
# .env is now in your remote history permanently

Even if you immediately force-push a cleaned history, anyone who pulled in the meantime has your secrets.

2. Copied into Docker images

If your .env file is in the build context, it can end up baked into a Docker image layer:

COPY . /app

That single line copies everything in the build context, including .env. Even if you delete it in a later layer, the file still exists in the intermediate layer. Anyone with access to the image can extract it.

3. Shared through insecure channels

"Can you send me the env file?" is a message that shows up in every team's Slack history. The file gets shared as a snippet, a DM, an email attachment, or a link to a Google Doc. Each copy is untracked, unversioned, and sitting in a system with its own breach surface.

4. Included in error logs and stack traces

Some frameworks and logging libraries dump environment variables when an unhandled exception occurs. If your error reporting tool captures process.env, your secrets are sitting in a third-party dashboard with its own access controls.

5. Left on developer machines

When someone leaves the team, their laptop still has .env.production with live database credentials. Unless you rotate every secret on offboarding (most teams don't), those credentials remain valid.

The .gitignore is not enough

Most advice starts and ends with "add .env to your .gitignore." That helps with the git commit problem, but it doesn't help with any of the other vectors. It also relies on every developer remembering to check .gitignore before their first commit in a new project.

And .gitignore doesn't protect you from:

  • git add -f .env (force-adding an ignored file)
  • Copying the project directory to a new location where .gitignore doesn't apply
  • CI/CD pipelines that create .env files from pipeline variables and don't clean them up
  • Backup tools that sync your project directory to cloud storage

A layered approach

There's no single fix. Reducing the risk of .env leaks requires multiple layers.

Keep secrets out of files when possible

Platform-native secret stores (Vercel environment variables, AWS Secrets Manager, etc.) avoid the file-on-disk problem entirely. The secret is injected at runtime, never written to the filesystem.

The tricky part is that you still need secrets locally for development. You can't avoid files entirely -- but you can reduce what's in them and how long they stay valid.

Use short-lived credentials

If your database supports connection tokens with a TTL, use them instead of permanent passwords. If your API provider offers scoped keys, create keys that only have the permissions you need. A leaked key that expires in an hour is significantly less dangerous than one that lives forever.

Separate test and production values

Never put production credentials in a file called .env. Use separate files with clear names:

.env.test      # Development/test values -- safe to leak (test API keys, local DB)
.env.live      # Production values -- treat as highly sensitive

Your .env.test file should contain values that are genuinely harmless if exposed: test-mode API keys, localhost database URLs, placeholder webhook secrets. If your test secrets would cause damage when leaked, they're not really test secrets.

Sync instead of share

Instead of sending .env files through Slack, use a sync tool that pushes values directly to each platform's API:

dotenvy set STRIPE_SECRET_KEY=sk_test_xxx
dotenvy sync test

This workflow means the secret value lives in your local .env.test file and on the target platform. It never transits through Slack, email, or a shared document. When a new developer joins, they run dotenvy pull instead of asking someone to send them a file.

Audit what's actually deployed

Most teams don't know what secrets are currently set on each platform. They set values months ago and never checked again. Running a comparison between your local source of truth and what's actually deployed catches both drift and unexpected values:

dotenvy sync test --dry-run

If a secret shows up on the remote that isn't in your config, something was set manually and needs to be tracked or removed.

Rotate on exposure

If a .env file does leak, rotate every secret in it immediately. Not tomorrow. Not after you assess the blast radius. Rotate first, investigate second. The cost of rotating a secret that wasn't actually compromised is near zero. The cost of not rotating a compromised secret can be catastrophic.

The real problem is the workflow

The .env file isn't inherently broken. The problem is the workflow around it: creating secrets in dashboards, copying them to files, sharing files between people, and hoping nothing falls through the cracks.

A better workflow treats the local file as a single source of truth that syncs directly to platforms, with clear separation between test and production values and no manual copying between systems.

curl -fsSL https://dotenvy.dev/install.sh | sh