Skip to content
Secrets7 min read

A .env file leaked into Git: cleanup in the right order

Accidentally committed a .env file? Reissue the values first, then rewrite history, then stop it happening again. What scanners catch and what to record.

Crunu teamPublished 7 min read

Example. Kiteline is a fictional company.
On this page

If you pushed a .env file with real values, treat every value in it as exposed from the moment of the push, and fix it in this order: reissue each value at its provider, then remove the file from Git history, then put guards in place so it doesn't happen again. Rewriting history first is the common mistake. It takes longer, it can't reach clones and forks, and the leaked keys keep working the whole time. GitHub's own guidance starts with the same step: revoke or rotate the secret first.

This post goes through each step, what push protection and secret scanning catch, and what to write down afterwards. If you're still deciding which env files belong in a repository at all, start with should you commit .env files.

First: did it leave your machine?

If you committed the file but haven't pushed, the values never left your laptop through Git. Remove the file from the commit, add it to .gitignore, and commit again:

Shell
git rm --cached .env
echo ".env" >> .gitignore
git commit --amend --no-edit

If the bad commit is further back but still unpushed, an interactive rebase or a reset does the same job. Check with git log --stat origin/main..HEAD that nothing with secrets is left before you push.

Everything below is for the case where the commit reached a remote.

Assume it's already copied

Once a commit is pushed, its contents can spread faster than you can react.

Example. Kiteline is a fictional company.

Each collaborator who fetched has it. CI may have cloned it, cached it or printed it into a log. Forks have it. On a public repository, automated scrapers look for credentials in new pushes. Whether or not anyone actually used the values, you can't prove they didn't, so the safe assumption is that they were seen.

That assumption sets the order. The only step that makes a leaked value harmless is replacing it at the provider. Everything else limits further spread.

Step 1: reissue every value, starting with the dangerous ones

Make a list of every variable in the leaked file, then sort it by what an attacker could do with it: payment keys and cloud credentials with broad permissions first, then production databases, then everything else.

For each one, use the order that avoids an outage:

  1. Issue a new value at the provider while the old one still works.
  2. Deploy it everywhere the old one was used.
  3. Confirm the services run on the new value.
  4. Revoke or delete the old value.

Some providers allow two valid credentials at once for this reason; the post on rotating API keys without downtime explains the overlap window. Steps for specific providers are in the key rotation guides, for example AWS and Stripe.

While you're at each provider, look at its access logs for the period between the push and the revocation. Unexpected calls, new resources or charges are the difference between "exposed" and "used", and they decide who else needs to know.

Don't skip values that look harmless. A webhook signing secret lets someone forge events to your app. A session signing key lets someone mint sessions. A database URL often contains the password.

Step 2: remove the file from history

Once nothing in the file works any more, clean up the repository so the old values stop circulating. GitHub notes that after revoking, rewriting history "may not be warranted" at all; you may still do it to keep the old values from being found and tried later.

GitHub recommends git-filter-repo, at least version 2.47, for this. To remove a file from every commit:

Shell
git-filter-repo --sensitive-data-removal --invert-paths --path .env
git push --force --mirror origin

Use the full path in the repository, and add another --path for each place the file lived if it was moved or renamed (GitHub: removing sensitive data).

Know what the rewrite does and doesn't reach:

  • It changes commit hashes of the commit that added the file and of every commit after it. Open pull requests are best merged or closed first.
  • Old clones can bring it back. A collaborator who pulls and pushes from an old clone can re-push the data. Everyone needs to re-clone or rebase onto the rewritten history.
  • Copies elsewhere stay. GitHub warns that the commits "may still be accessible elsewhere": in clones and forks, in cached views reached by commit hash, and through pull requests that reference them. Only GitHub Support can remove cached views and pull request references, and forks keep the data until their owners remove it.

This is why history rewriting is cleanup, not remediation.

Step 3: make it hard to do again

Three layers, each catching what the one before misses.

Ignore rules. Ignore all env files and allow only the template:

Text
.env
.env.*
!.env.example

Push protection. GitHub's push protection "blocks pushes that contain secrets before they reach your repository." It recognizes provider patterns from its partner program, plus generic patterns such as private keys and database connection strings when those are turned on. Two limits matter. At the repository level it "is disabled by default" and requires GitHub Secret Protection; the per-user setting, on by default, covers pushes to public repositories. And "anyone with write access to the repository can bypass push protection by specifying a bypass reason", with the bypass recorded in the audit log (GitHub: about push protection).

Secret scanning. Scanning looks backwards: it "scans your entire Git history on all branches" and raises alerts. It runs automatically for free on public repositories; private ones need GitHub Secret Protection. For partner secrets found by scanning, GitHub notifies the provider "so they can take action, such as revoking the credential" (GitHub: about secret scanning). That is useful, but don't count on a provider revocation as your fix: you still need the new value deployed.

What scanners miss is anything without a recognizable shape: an internal service password, a random string your app uses as a signing key, a value split across lines. Pattern-based tools are built around known key formats, so a secret without one is harder for them to match. The glossary entry on secret scanning covers how these tools match.

Step 4: tell the right people and write it down

Who to tell depends on what the values could reach. Your security contact or team lead in every case. The owner of each affected service. If the values could reach customer data, whoever handles incidents and legal obligations in your company, because some regulations have notification deadlines that start when you become aware.

Keep a short record while it's fresh:

  • the repository, branch and commit, and when it was pushed;
  • the list of values in the file;
  • for each value, when it was reissued and when the old one was revoked;
  • what the provider logs showed between push and revocation;
  • whether history was rewritten, and whether forks or support requests were involved.

The record is what you'll want if one of the old values turns up months later. The glossary entry on leaked secrets has the short version of this checklist.

The longer fix: values that never sit in the repository

A .env file leaks into Git because it sits in the working directory, next to the code, one git add . away from a commit. The durable fix is to keep only the template there.

With Crunu, the values live in an environment such as payments-api / development, encrypted on the devices of the people who have access; the server holds ciphertext and metadata such as names, never the values. A developer starts the app with crunu run --env payments-api/development -- npm run dev, and the variables arrive in the process environment instead of a file. The repository keeps .env.example, and there is no .env with real values to commit by accident.

Example. Kiteline is a fictional company.

When you reissue values after a leak, you save the new ones into the environment as a new version. History keeps the last 100 versions with who changed what, by variable name, never by value, so the record from step 4 has a matching trail. If a new key turns out to be wrong, Restore… brings back an earlier version as a new one, and with crunu run --restart on-change the service restarts on the corrected values. For well-known services, each file shows a Where to issue a new key link.

Two things Crunu does not do. It doesn't scan repositories for secrets; keep push protection or a scanner for that. And it doesn't reissue anything at providers; step 1 is still yours. The guide on sharing .env files with a team covers the setup in full.

Sources