Git commit graph with a leaked .env file and a scan, rotate, revoke path on dark navy
secrets managementgitDevOpssecurity.envCI/CD

Stop Committing Secrets to Git

API keys in git history do not expire when you delete the file. A practical workflow for .env files, rotation, CI secrets, and local vaults - without pretending one tool fixes culture.

Swati ChaudharyFounder, Myrafy

A .env file in a commit is not a "whoops, we gitignored it later" story. Git history keeps the key. Forks, CI caches, laptop clones, and "that intern's zip from 2023" keep it too. Rotating the credential is the fix. Deleting the file is hygiene after the fact.

This is the working pattern we use on product and cloud / DevOps engagements: keep secrets out of the repo, inject them where the process actually runs, and assume leak-and-rotate will still happen.

JWT design is a different article (JWT authentication explained). This one is about the strings that unlock other systems.

What Counts as a Secret

Pre-commit hook scanning a commit so a leaked .env fails before it reaches origin
The first leak should be a failed commit, not a Slack apology

Treat as secret anything that grants access or signs trust:

  • API keys, webhook secrets, database URLs with passwords
  • Cloud access keys and session tokens
  • Private keys, .pem material, signing certs
  • Customer tokens copied into fixtures "just for local"

Not secrets: public site keys that are designed to ship to the browser (still restrict them by domain), OpenAPI URLs, non-sensitive feature flags.

If you are unsure, it is a secret until a threat model says otherwise.

The Failure Mode Is Cultural, Then Technical

Teams leak secrets because the happy path is paste.

  • Slack: "here's the staging Stripe key"
  • README: cp .env.example .env then someone fills production values
  • Docker Compose: POSTGRES_PASSWORD=postgres committed "for local" and reused in staging
  • GitHub Actions: a secret pasted into a echo step that prints in logs

Tools help. Defaults help more. If local setup requires hunting for a password in chat, people will commit a file that "just works."

A Baseline That Survives Real Teams

1. Never commit the live file

  • .env, .env.local, appsettings.*.json with real values, secrets.json, *.pem in .gitignore and a server-side ignore if you use one
  • Commit .env.example with empty or fake placeholders and comments for each key name
  • Add a pre-commit hook (gitleaks, trufflehog, or detect-secrets) so the first leak is a failed commit, not a Slack apology

Example files are documentation. They are not credentials.

2. Inject at runtime, per environment

Where the app runs Where the secret should live
Local laptop OS keychain, a local vault, or a gitignored file generated from a vault
CI The CI secret store (GitHub Actions / GitLab CI variables) - never in YAML
Staging / prod Cloud secret manager or platform env (AWS Secrets Manager, SSM, Azure Key Vault, container env from the orchestrator)

Application code should read process.env.DATABASE_URL, not fs.readFileSync('.env.production') from the repo.

For GitHub-hosted pipelines, prefer OIDC to cloud roles over long-lived access keys in repo secrets. That pattern is in our GitHub Actions CI/CD guide.

3. Different values per environment

Production credentials must not work in a developer's Docker Compose. Staging should not share the production Stripe account "to make QA easier." Blast radius is the point of environments - the same reason a boring AWS baseline separates accounts or at least IAM boundaries.

4. Rotate on a calendar, and on incident

Schedule rotation for high-value keys (cloud roots, payment, email). Rotate immediately if a key hit git, a ticket, or a screenshot. Removing the commit with git filter-repo is optional cleanup; invalidating the credential is mandatory.

Assume copies exist. Rewrite history only if you also rotate and notify anyone who cloned.

Local Developer Experience

Friction is why secrets land in git. Reduce it:

  • One command that writes a gitignored .env from an approved store
  • Document the key names, not the values, in the README
  • For .NET, keep launchSettings.json free of production connection strings; use user secrets or injected files
  • For Node, dotenv locally is fine; do not rely on it in production containers

Myrafy builds Sealed for this gap: a local-first encrypted vault that injects .env or launchSettings.json without storing plaintext in git. Use it, or 1Password/Bitwarden CLI, or direnv plus a team store - the product matters less than one approved path.

If you are debugging a JWT, hash, or Base64 blob, use browser-local tools so the string never goes to a random paste site. (Our tools run in the browser; they are not a vault.)

CI: The Second Most Common Leak

Typical mistakes:

  • echo $SECRET in a debug step
  • Printing env dumps on failure
  • Checking in serverless.yml or Helm values with base64 "encrypted" strings that are still in the repo
  • Using the same GitHub secret for fork PRs (untrusted code can steal pull_request_target secrets if you misconfigure)

Rules of thumb:

  • Mask secrets in the CI UI
  • Do not pass production secrets to jobs that run untrusted PR code
  • Prefer short-lived cloud credentials over static keys
  • Store Terraform state (and its secrets) in a backend with encryption - not in the repo next to your Terraform modules

If It Already Leaked

Incident checklist: rotate the key, revoke the old credential, purge CI logs, and notify clones
Invalidating the credential is mandatory; rewriting history is optional cleanup

  1. Revoke / rotate the credential at the provider. Do this first.
  2. Check access logs if the vendor offers them.
  3. Purge from chat, tickets, and wiki.
  4. Optional: history rewrite, then force every clone to re-fetch - only after rotation, and only if you control all remotes.
  5. Add the scanner to CI so the next leak fails the build.

Do not announce "we deleted the file" as the incident close.

What to Put in the SOW

If you hire a company to set this up, the SOW should name:

  • Secret store per environment
  • Who can mint credentials
  • Rotation owner
  • Scanner in CI
  • Explicit "no secrets in git" acceptance

This is cheaper than a production key in a public fork.

The Bottom Line

Stop committing secrets to git by making the default path a vault or a platform secret, not a file that "we'll gitignore later." Example env files, scanners, per-environment values, OIDC in CI, and a rotation habit beat any single product.

If a key hit history, rotate it. Then fix the path so the next engineer is not tempted to paste.


Need a secrets and CI baseline with your cloud setup? See Myrafy cloud & DevOps or contact us for a free estimate. For a local vault, look at Sealed.

Contact us