Secrets management has one non-negotiable rule: secrets never go into git. Everything else is tradeoffs between convenience, cost, and security posture appropriate for your team's size and risk profile. This guide covers what not to do, what to do instead at each stage (local development, CI/CD, and production), and how to handle the situation when a secret has already leaked.
What Not to Do
Never commit .env files with real values. This is the most common mistake. An .env file committed to a git repository — even a private one — creates permanent risk. Git history persists. Former employees retain access to cloned repositories. Repository access controls change.
Never hardcode secrets in source code. const API_KEY = "sk-live-..." in a TypeScript file is the same as committing it to git. Even if you "remember to remove it before pushing," you won't always remember.
Never share secrets over Slack, email, or chat. Chat logs persist. They're searchable. They often sync to cloud services with broader access than your secrets store.
Never use the same secret across environments. Production database credentials should be different from staging credentials. Development API keys should be different from production API keys. When a secret leaks, you want to rotate only the affected environment, not all environments.
Local Development: .env.local in .gitignore
The standard pattern for local development is a .env.local file that is explicitly ignored by git.
Create .gitignore (or add to it):
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
Create .env.example (committed to git, contains no real values):
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your-jwt-secret-here
ANTHROPIC_API_KEY=your-anthropic-key-here
STRIPE_SECRET_KEY=sk-test-...
Create .env.local (not committed, contains real development values):
DATABASE_URL=mongodb://localhost:27017/myapp_dev
JWT_SECRET=a-real-random-secret-for-local-dev-only
ANTHROPIC_API_KEY=sk-ant-...
STRIPE_SECRET_KEY=sk-test-...
New team members clone the repo, copy .env.example to .env.local, and fill in the values from your team's password manager or secrets store.
CI/CD: Environment Variables in GitHub Actions
In GitHub Actions, store secrets in the repository or organization secrets store (Settings > Secrets and variables > Actions) and reference them in workflows:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
run: pnpm deploy
GitHub Actions secrets are:
- Encrypted at rest with libsodium
- Never exposed in logs (GitHub redacts them if they appear)
- Scoped to the repository or organization
- Not accessible to pull requests from forks (protecting against supply chain attacks)
For organization-wide secrets shared across multiple repositories, use organization-level secrets in GitHub.
Production: A Secrets Manager
For production deployments, environment variables set in a hosting platform (Vercel, Railway, Coolify, Fly.io) are the minimum viable solution. They store secrets encrypted and inject them at runtime.
For teams that want a dedicated secrets manager:
Doppler is the practical choice for small teams. It provides a UI for managing secrets across environments (dev, staging, production), syncs secrets to your hosting platform, has a CLI for local development (doppler run -- pnpm dev), and audits who accessed what and when. Pricing: free for early-stage teams (up to 5 users and 5 projects), $6/user/month after that.
AWS Secrets Manager is the choice for teams already on AWS infrastructure. It stores secrets encrypted, rotates them automatically, and integrates with IAM for fine-grained access control. Cost: $0.40/secret/month plus API call costs.
1Password Secrets Automation integrates with 1Password, which many teams already use as their password manager. Secrets are available via CLI, SDKs, or direct injection into CI/CD environments. Cost: $19.95/month for teams.
HashiCorp Vault is the self-hosted option for teams that cannot use cloud secrets managers for compliance reasons. Complex to operate, powerful, free to self-host.
The Accidental Commit Problem
If a secret is committed to git, treat the secret as compromised immediately, even if the repository is private.
Step 1: Rotate the secret immediately. Generate a new API key, change the database password, regenerate the JWT secret. Do this before anything else. The old secret is no longer safe.
Step 2: Remove the secret from git history. git rm removes the file from the next commit but not from history. To purge from history, use git filter-repo (the modern replacement for BFG):
pip install git-filter-repo
git filter-repo --path .env --invert-paths
git push --force-with-lease
All collaborators will need to re-clone the repository after a force push. The history rewrite propagates to the remote but any existing clones retain the old history.
Step 3: Audit access. Check your secret provider's audit logs for any activity with the compromised key between when it was committed and when you rotated it.
Detecting Secrets Before They're Committed
gitleaks scans git history and staged changes for secrets:
brew install gitleaks
gitleaks detect --source .
Integrate as a pre-commit hook to catch secrets before they're committed:
# .git/hooks/pre-commit
gitleaks protect --staged
GitHub secret scanning automatically scans repositories for known secret patterns (AWS keys, Stripe keys, GitHub tokens) and alerts you if it finds them. This is enabled by default on public repositories and available for private repositories on GitHub Advanced Security.
Secret Rotation Policy
Secrets should be rotated regularly, not just when they leak. A practical rotation schedule:
- Production database passwords: every 90 days
- JWT secrets: every 90 days (requires all existing sessions to re-authenticate)
- Third-party API keys: every 180 days, or immediately after any team member leaves
- CI/CD secrets: when the team member who set them up leaves
Doppler and AWS Secrets Manager support automated rotation for some secret types (database passwords via RDS, for example).
Keep Reading
- Docker for Developers Guide — How to handle
.envfiles in Docker correctly - CI/CD for Small Engineering Teams — Secrets in GitHub Actions in depth
- API Rate Limiting Implementation Guide — Protecting your API endpoints beyond secrets
Pristren builds AI-powered software for teams. Zlyqor is our all-in-one workspace — chat, projects, time tracking, AI meeting summaries, and invoicing — in one tool. Try it free.