Environment Variables: The Security Hole in Every Startup
Quick audit: where is your database password right now?
If you answered ".env file in the repo root" — you're in the majority. If you answered "also in a Slack message to the new hire, a screenshot in Confluence, and hardcoded in that one Lambda function that Dave wrote before he left" — you're being honest.
Environment variables are the most dangerous infrastructure in most startups because everyone treats them as an afterthought.
The Common Mistakes
Mistake 1: .env in Version Control
I've seen it in production repos at real companies. A `.env` file with the Stripe secret key, committed in 2022, still in git history even though it was "removed."
```bash
Check if you've ever committed secrets
git log --all --full-history -- .env git log --all --full-history -- "*.pem" git log --all -p | grep -E "STRIPE_SECRET|AWS_SECRET|DATABASE_URL" | head -5 ```
Git history is forever. Even if you delete the file, the secret is in every clone of the repo. You need to rotate the key AND clean the history (using git filter-branch or BFG Repo-Cleaner).
Mistake 2: Same Secrets Everywhere
Development database password = staging password = production password. I've seen this at a company processing $10M/month. One compromised developer laptop gives you production access.
Mistake 3: Sharing via Slack/Email
"Hey, what's the Stripe key?" "Check DM." That DM is now in Slack's database forever, searchable by anyone with workspace admin access.
What I Do Instead
For Development: .env.local + .env.example
```bash
.env.example (committed — template only)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb STRIPE_SECRET_KEY=sk_test_... NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
.env.local (gitignored — real values)
DATABASE_URL=postgresql://jason:real_password@localhost:5432/nexural STRIPE_SECRET_KEY=sk_test_actual_key_here ```
`.env.example` shows what variables exist. `.env.local` has real values and is gitignored. New developers copy the example and fill in their own values.
For Production: Platform-Native Secrets
- Vercel: Project settings → Environment Variables (encrypted at rest)
- AWS: Secrets Manager or SSM Parameter Store
- GitHub Actions: Repository secrets
Never put production secrets in a file that touches a developer's machine.
For CI/CD: GitHub OIDC (No Static Keys)
This is the pattern I'm most proud of. Instead of storing AWS access keys in GitHub secrets, I use OIDC federation:
```yaml
GitHub Actions assumes an AWS role via OIDC — no static keys anywhere
permissions: id-token: write contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::role/GitHubActions-Deploy aws-region: us-east-1 ```
The IAM role's trust policy limits access to your specific repo and branch. No long-lived keys to rotate. No keys to leak.
The Audit Script I Run Monthly
```bash #!/bin/bash
secrets-audit.sh
echo "=== Checking for committed secrets ===" grep -r "sk_live" . --include=".ts" --include=".js" --include=".py" | grep -v node_modules grep -r "AKIA" . --include=".ts" --include=".js" --include=".py" | grep -v node_modules grep -r "BEGIN PRIVATE KEY" . --include=".pem" --include=".key"
echo "=== Checking .gitignore ===" for pattern in ".env" ".env.local" ".pem" ".key"; do grep -q "$pattern" .gitignore && echo "OK: $pattern in .gitignore" || echo "MISSING: $pattern" done
echo "=== Checking for .env in git history ===" git log --all --full-history --diff-filter=A -- .env .env.local .env.production ```
Takes 30 seconds. Catches the most common mistakes. I run it before every major deploy.
The Bottom Line
Your .env file isn't a configuration file — it's a manifest of everything an attacker needs to own your infrastructure. Treat it accordingly.