<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Passwords in my git history: how I removed them (and why that's not enough)

I was doing a general review of my project, looking for forgotten things. I found one I'd rather not have: a deployment configuration file containing the key that signs login tokens, the admin password and the password used to send emails. In plain text, in the git history, on three branches.

How it happened

The file was in .gitignore. The catch is that it got there after being committed once, and .gitignore only ignores files that aren't tracked yet: a file that is already tracked stays tracked, change after change, even if its name is in the list.

The repository was private, but private doesn't mean safe: collaborators, CI services, backups and every computer it has been cloned to can see it.

Removing it from the whole history

Deleting the file with a new commit doesn't help: it's still in every previous commit. You have to rewrite the history of every branch. I used git filter-repo on a mirror clone, then force-pushed and checked that no commit contained it any more:

# 1. Stop tracking the file (it stays on disk)
git rm --cached .env.deploy
git commit -m "Stop tracking .env.deploy"

# 2. Rewrite the history of EVERY branch on a mirror clone
git clone --mirror git@github.com:me/my-repo.git
cd my-repo.git
git filter-repo --path .env.deploy --invert-paths
git push --force --mirror

# 3. Verify: no commit on any branch may still contain it
git log --all --oneline -- .env.deploy

After a rewrite like this, anyone with a local copy must realign it with the new remote: the old commits must never be pushed again.

Why it's not enough

This is the most important lesson. Even after the rewrite, old commits may remain reachable: GitHub, for example, keeps internal references to pull requests that a force push cannot delete. And you can't know who downloaded the repository in the past.

Removing a secret from history stops things getting worse; the only thing that fixes it is changing the secret. In practice:

  • A new token signing key: it invalidates every existing session, so everyone has to log in again. That's the price.
  • A new admin password.
  • Revoking the mail app password and generating a new one.
  • Updating the variables in every environment where they run.

The second time, same pattern

A few weeks later I found the same problem in another form: a very handy server setup script that wrote the credentials straight into the configuration file. It's the recurring pattern: the convenience script that holds secrets so you don't have to configure anything. I rewrote it to generate keys on the spot and ask whoever runs it for the passwords:

# Before: secrets written into a script tracked by git
# JWT_SECRET=hardcoded-value...

# After: generated or asked at runtime, never committed
JWT_SECRET="$(openssl rand -base64 48)"
read -rsp "SMTP password: " SMTP_PASS; echo
umask 077   # the .env file is readable by its owner only
printf 'JWT_SECRET=%s\nSMTP_PASS=%s\n' "$JWT_SECRET" "$SMTP_PASS" > .env

Habits that stop it happening again

  1. A committed .env.example, with variable names and no values; the real .env never enters the repository.
  2. A secret scanner before every commit, such as gitleaks in a pre-commit hook: it blocks the commit before the damage is done.
  3. Push protection offered by GitHub and GitLab, which rejects pushes containing recognisable secrets.
  4. Never put secrets in scripts: generate them at runtime or read them from the environment.
  5. A periodic check of the history: a secret you find yourself is a small incident; one found by someone else isn't.

In short

A secret in a repository must be treated as compromised the moment you find it. Rewriting history is necessary but not sufficient: the real fix is changing every exposed credential. Then make the problem impossible to repeat, with scanners and clear rules. I've collected more security practices in this guide.

💬 Reader notes

0 notes

Write a note

Share your opinion, a suggestion or a compliment

Latest notes

No notes yet. Be the first to comment!