Git, CI/CD & GitOpsXCIV · Incident: Secret LeakIncidentResponse
Remove repository exposure — history cleanup with filter-repo
What you'll learn
- Execute a history rewrite with `git filter-repo --replace-text` to remove the leaked credential from every commit
- Run `git reflog expire --expire=now --all` and `git gc --prune=now --aggressive` to complete the cleanup in the local clone
- Force-push the rewrite to the central repository and coordinate the cleanup with forks and mirrors
- Identify the failure modes of partial rewrites and uncoordinated force-pushes
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
The history rewrite is the third step, after disable and rotation. It removes the leaked credential from the repository’s future: every commit, every tag, every ref. It does not remove the credential from the attacker’s memory, the cloud audit log, or any clone already taken.
The rewrite pipeline
A three-step pipeline:
flowchart LR
A["expressions.txt with old strings"] --> B["filter-repo --replace-text"]
B --> C["rewritten history"]
C --> D["reflog expire --expire=now --all"]
D --> E["gc --prune=now --aggressive"]
E --> F["clean local clone"]
F --> G["force-push to central repo"]
G --> H["rewrite complete"]
- Replace.
git filter-repo --replace-text expressions.txtrewrites every matching commit. - Expire.
git reflog expire --expire=now --allexpires every reflog entry. - Prune.
git gc --prune=now --aggressiveremoves unreachable objects.
The expressions file
Each line is a literal or regex, separated from its
replacement by ==>:
AKIAIOSFODNN7EXAMPLE==>REDACTED-AWS-KEY
sa-prod-[a-z0-9]+@project-id.iam.gserviceaccount.com==>REDACTED-SA-EMAIL
postgres://app:s3cr3t-prod-pw@db.internal:5432/app==>REDACTED-DB-URL
The filter-repo command
Run from a fresh clone (the rewrite is destructive):
# Fresh clone of the repository to be rewritten
git clone "$REPO_URL" rewrite-workdir
cd rewrite-workdir
# Rewrite every commit matching the patterns
git filter-repo --replace-text /tmp/leak-redact/expressions.txt
# Expire every reflog entry so the old objects are unreachable
git reflog expire --expire=now --all
# Prune the unreachable objects from the local object store
git gc --prune=now --aggressive
The force-push
The rewrite produces a new history. The central repository must be updated:
git push --force-with-lease origin --all
git push --force-with-lease origin --tags
The force-push is the moment the rewrite becomes visible to every consumer. Clones that pull after the force-push see the rewritten history; clones that do not pull continue to see the old history.
Coordinating the rewrite
- Active contributors. Every developer must re-clone after the force-push. A developer pushing to the old history re-introduces the leaked commit.
- CI runners. Every runner must clear its workspace and re-checkout.
- Mirrors and backups. Every mirror must be paused for the rewrite and re-synced after.
Failure modes
- Partial rewrite. The expressions file missed a variant. Extend and re-run from a fresh clone.
- Uncoordinated force-push. Central rewritten but clones are not. Run the coordination list.
- Forgotten forks. A public fork retains the old history. The secret is permanently exposed.
Production discipline
- Rotate before rewrite. The rewrite is detective hygiene; the rotation is the fix.
- Fresh clone for the rewrite. Local work is lost when the rewrite changes the SHAs.
- Coordinate before force-push. Consumers must be notified; otherwise the rewrite is undone.
Cross-course references
- Linux for Production Sysadmins - Part XXXIV (KeyMgmt) covers file-level scrubbing.
- Ansible for Production Sysadmins - Part XXXVII (RepoArch) covers backup and mirror patterns.
- Terraform for Production Sysadmins - Part XV (CredentialRotation) covers rotation patterns.
Quiz
Knowledge check · 4 questions
Q1. A history rewrite with `git filter-repo --replace-text expressions.txt` is followed by `git reflog expire --expire=now --all` and `git gc --prune=now --aggressive`. What does the gc step accomplish?
Q2. A force-push of the rewritten history to the central repository completes the history cleanup.
Q3. Name the three commands in the history cleanup pipeline and the role of each.
Q4. An expressions file for the rewrite contains the leaked AWS access key ID but not the secret access key. The rewrite runs and the central repository is force-pushed. Triage the failure mode.
The expressions file contains a single line: `AKIAIOSFODNN7EXAMPLE==>REDACTED-AWS-KEY`. The rewrite replaces the access key ID in every commit. The secret access key, which appears in the same file as the ID, is not in the expressions file and is not replaced. The credential is now partially redacted: the ID is gone, the secret is still in the history.
Passing score: 75%. Answers are checked in this browser.