Full lesson
Explore the full explanation, examples, and visuals at your own pace.
Contain the exposure
Treat a committed secret as compromised. Deleting the file leaves the credential in older commits. Before cleaning history, revoke it through its issuer, or rotate it so the old value stops working. Update legitimate consumers, then review access logs and permissions for signs of misuse.
- Revoke or rotate the credential
- Review access logs and permissions
- Deletion leaves older commits intact
Deleting the file does not erase its ancestors
Master points to commit C, where the file is deleted. But C still points back to B, which contains the secret, and B points back to the clean commit A. The feature branch and release tag also point directly to B. Deleting today's file leaves those historical snapshots intact, so cleanup must cover history across branches and tags.
Freeze pushes, then make a fresh mirror
Coordinate a push freeze so nobody changes history during cleanup, and securely isolate any backups that retain the secret. Replace the placeholder URL, then create this fresh mirror in a private directory. It includes advertised refs, not hidden ones. Inventory historical paths, including config/prod.env and its earlier name, .env. Use git-filter-repo version two point forty-seven or newer.
# Replace this placeholder with your repository URL.
REPO_URL='https://example.com/team/app.git'
git clone --mirror "$REPO_URL" repo-clean.git
cd repo-clean.gitRemove historical paths and remaining literal occurrences
This removes both historical paths, then replaces the revoked secret in ordinary text files and commit or tag messages. Keep literal replacement rules private, outside the repository. Encoded variants need additional matching rules. By default, rewriting covers all locally available refs, not just your current branch. Changed commits and their descendants get new IDs because each commit references its parent.
# Privately edit ../replacements.txt outside the repo.
# Rule format: literal:OLD_VALUE==>REMOVED
# Replace OLD_VALUE with the actual revoked secret.
chmod 600 ../replacements.txt
git filter-repo --sensitive-data-removal \
--invert-paths \
--path .env --path config/prod.env \
--replace-text ../replacements.txt \
--replace-message ../replacements.txtVerify before publishing
Before publishing, check history reachable from every branch and tag, including file contents and messages. Match the known secret privately, without printing it, and run broader scans for missed variants. Check both old paths, review rewrite reports, and test application behavior. A clean scanner result isn't proof.
- Check all branches and tags
- Scan historical content and messages
- Review rewrite reports and application behavior
Verification finds the revoked secret in an old release tag. What must happen before publishing?
Verification finds the revoked secret in an old release tag. What must happen before publishing?
- Rewrite or remove the affected tag
- Delete the file only on master
- Rotate again without changing the tag
Verification finds the revoked secret in an old release tag. What must happen before publishing?
Rewrite or remove the affected tag. The tag still makes contaminated history reachable. Revocation blocks use of the credential, but it does not remove the stored value from Git.
- Rewrite or remove the affected tag
- Delete the file only on master
- Rotate again without changing the tag
Publish only after approval and verification
Publish only during the agreed freeze, after approval and verification. Inspect remotes first, restoring origin only if it's absent, with the verified repository URL. The forced mirror push replaces remote refs and deletes those missing locally, so it's destructive. Arrange approved branch-protection changes beforehand, and inspect any rejected refs rather than assuming publication completed.
# Full rewrites normally remove origin; inspect first.
git remote -v
# If origin is absent, restore the same repository URL.
git remote add origin "$REPO_URL"
# Destructive: replaces refs and deletes absent remote refs.
git push --force --mirror originA force-push is not universal erasure
A force-push only replaces the remote refs you're allowed to update. Provider-controlled pull-request refs, cached views, and unreachable objects may still retain the secret. Follow your host's cleanup procedures; local garbage collection can't clean remote storage. Contact fork and artifact owners separately about their copies.
- Request hidden-ref and cached-view cleanup
- Address unreachable objects with the host
- Contact fork and artifact owners
Keep old history from returning
Merging an old branch can bring the contaminated ancestry back. Have collaborators replace their clones, then transfer only reviewed, clean changes from unpushed work, not old branch history. Also purge retained CI clones, caches, and artifacts, and securely remove private cleanup files containing the secret.
- Replace collaborator clones with fresh clones
- Review unpushed work before transferring changes
- Purge CI clones, caches, and artifacts
Check the published result
Create a fresh mirror from the remote, then scan for the known secret. Confirm expected branches and tags, and check hosting-provider cleanup. Restore protections before reopening pushes. These checks cover reachable history, not every external copy, so a clean scan doesn't prove universal erasure.
- Scan a fresh remote mirror
- Confirm expected branches, tags, and host cleanup
- Restore protections before reopening pushes
Contain, rewrite, verify
Credential revocation contains access risk; history rewriting removes data from the refs you control. Revoke first, coordinate rewriting across every affected branch and tag, then verify the published history. Other copies need separate cleanup. You can confirm what you've cleaned without promising erasure everywhere.
- Revoke the credential first
- Rewrite every affected branch and tag
- Clean other copies; acknowledge erasure limits
