Security
Leaked credentials: what to do now
Keys, tokens or passwords were sent to an example domain. Revoke and replace them, with direct links to the revocation pages of common providers.
Guide · Security
A key, token or password ended up in a commit. Deleting the file in a new commit does not help: the old commit still contains it, in every clone and fork. This guide shows the right order: revoke first, then rewrite the history, clean up the hosting platform, and make sure it does not happen again.
Treat a committed secret as exposed, even in a private repository: clones, forks, CI logs and backups may already hold a copy, and automated scanners pick up new commits in public repositories within minutes.
Where to revoke keys at common providers: Leaked credentials: what to do now. Rewriting history comes after this, never instead of it.
Once a secret is revoked it no longer grants access, and GitHub notes that this may be enough. A rewrite is still worth it when:
A rewrite changes the ID of every later commit. Collaborators have to rebase their work, open pull requests may lose their review comments, and commit signatures are removed. Agree on a moment with everyone involved, and merge or close open pull requests first.
# Which commits added or removed the string?
git log --all --oneline -S 'the-secret-value'
# Which files did it appear in?
git grep 'the-secret-value' $(git rev-list --all)
# Scan the whole history for known secret formats
gitleaks git -v
Note every file path the secret appeared under, including earlier names if the file was moved or renamed.
git-filter-repo is the tool GitHub recommends. Use version 2.47 or later, which has the
--sensitive-data-removal option, and work in a fresh clone.
# Install (or use your package manager)
brew install git-filter-repo # macOS
pip install git-filter-repo # anywhere with Python
git clone https://github.com/YOUR-ORG/YOUR-REPO
cd YOUR-REPO
# Option 1: remove a whole file from all history
git-filter-repo --sensitive-data-removal --invert-paths --path config/secrets.yml
# Option 2: replace the secret everywhere it occurs
git-filter-repo --sensitive-data-removal --replace-text ../replacements.txt
The replacements file lists one value per line. By default each match becomes ***REMOVED***; with
==> you choose the replacement, and regex: matches a pattern:
# ../replacements.txt (outside the repository)
sk_live_51Hx0000000000000000000000
AKIA0000000000000000==>AWS_ACCESS_KEY_ID_REMOVED
regex:password\s*=\s*"[^"]+"==>password = "REMOVED"
Check the result with git log --all -S 'the-secret-value': it should print nothing. Then overwrite the
remote:
git push --force --mirror origin
Branch protection that blocks force pushes has to be switched off for this moment. After the push, the rewrite cannot be undone.
After the force push, old commits can still be reached through pull requests, cached views and forks.
grep -c '^refs/pull/.*/head$' .git/filter-repo/changed-refs) and the “First Changed Commit(s)” that
git-filter-repo printed. GitHub only helps where rotating the credential cannot remove the risk.On GitLab, Bitbucket or a self-hosted server the steps are similar; check that platform’s documentation for how it removes old objects and cached views.
BFG Repo-Cleaner is an older, Java-based tool that is still widely used. It works on a mirror clone and, by default, leaves the latest commit untouched, so remove the secret from the current version in a normal commit first.
git clone --mirror https://github.com/YOUR-ORG/YOUR-REPO.git
java -jar bfg.jar --replace-text replacements.txt YOUR-REPO.git
cd YOUR-REPO.git
git reflog expire --expire=now --all && git gc --prune=now --aggressive
git push
.env and similar files to .gitignore before the first commit.git diff --cached, instead of
git add . or git commit -a.Also sent that key to a placeholder address such as api.example-petstore.com? Then it reached a server outside your control as well: Configuring API clients and SDKs.