DEV Community

kol kol
kol kol

Posted on

I Leaked API Keys Through My .env File — Here's What I Learned About Secret Management

I Leaked API Keys Through My .env File — Here's What I Learned About Secret Management


Last month, I pushed a commit that included a .env.production file.

Not a .env.example. Not a redacted template. The actual file with real API keys, database credentials, and webhook secrets.

It was in the repo for exactly 4 minutes before I realized what I'd done.

Those 4 minutes were the longest of my developer career.

The Myth of ".env is Safe"

We've all been told the same thing: "Just add .env to your .gitignore and you're fine."

This advice is technically correct and practically dangerous. It creates a false sense of security that leads to exactly the mistake I made.

Here's what most developers don't realize: .env is not a configuration management system. It's a leaky bucket waiting to happen.

The 5 .env Mistakes I See Every Day

1. Committing .env Files (Yes, Really)

The obvious one. But it happens more than you think. GitHub's secret scanning catches many, but not all. Private repos feel safe until they're not.

# .gitignore
.env
.env.*
!.env.example
Enter fullscreen mode Exit fullscreen mode

If you don't have these lines, add them now. Not later. Now.

2. Storing Non-Secrets in .env

Your .env file is for secrets only. Not feature flags. Not API endpoints. Not environment names.

I see this all the time:

# WRONG - .env is for secrets
NODE_ENV=production
API_URL=https://api.example.com
FEATURE_NEW_DASHBOARD=true
STRIPE_SECRET_KEY=sk_live_abc123
Enter fullscreen mode Exit fullscreen mode

The correct approach:

# config/production.ts - Non-secret config
export const config = {
  apiUrl: "https://api.example.com",
  featureFlags: { newDashboard: true },
  nodeEnv: "production"
};

// .env - Secrets only
STRIPE_SECRET_KEY=sk_live_abc123
DATABASE_URL=postgresql://...
Enter fullscreen mode Exit fullscreen mode

3. Same .env Structure Across All Environments

Development, staging, production — all sharing the same .env template. One typo in CI and you're connecting your test suite to production.

Instead, namespace your environment variables and validate them at startup:

// config/env.ts
const required = [
  "STRIPE_SECRET_KEY",
  "DATABASE_URL",
  "SUPABASE_SERVICE_ROLE_KEY"
];

for (const key of required) {
  if (!process.env[key]) {
    throw new Error(`Missing required env: ${key}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

If the app starts without a required key, it crashes immediately. Not at 3 AM when a user hits an untested code path.

4. No Rotation Policy

That Stripe key you put in .env two years ago? It's still there. The old Sentry DSN? Still active. The decommissioned API service? Its credentials are still in your repo.

Secrets should have expiration dates. Set calendar reminders. Rotate quarterly at minimum.

5. Sharing .env Over Slack or Email

"I'll just DM you the production keys."

No. Use a secrets manager. Even a simple one. Even the free tier. The fact that you're copy-pasting credentials over chat means your secret management is broken.

What I Do Now

After my 4-minute panic, I rebuilt my approach:

  1. .env for secrets only — Feature flags and config go in code
  2. .env.example in the repo — Every new developer can set up in 2 minutes
  3. Zod validation at startup — Missing keys = immediate crash, not mysterious runtime errors
  4. Secret rotation calendar — Quarterly reminders, automated where possible
  5. Never type secrets in chat — Use your platform's secret sharing (Pulumi, Doppler, even 1Password)

The Real Lesson

The .env file isn't the problem. The problem is treating a simple text file as a security boundary.

Your .gitignore is not a firewall. Your private repo is not a vault. Your memory is not a secrets manager.

Build your secret management like you build your code: explicit, validated, and reviewed.


What's your worst .env story? Drop it in the comments — misery loves company, and we can all learn from each other's near-misses.

Top comments (0)