Recover a secret that was committed and pushed
Task
Commit a secret to a repository, push it, and then carry out the correct remediation -- which is not "delete it in the next commit". This is the incident every team meets, and getting the order right matters: rotate first, clean history second, and understand why the deletion commit achieves nothing.
Steps
- Commit an application config,
app.env, containing a line such asAPI_KEY=API_KEY_VALUE_1234, and push it to the "remote". Confirm it is there withgit log -p. - Make the naive fix: remove the key in a new commit and push. Then prove it did nothing for the exposure -- the key is still in the earlier commit, and
git log -pandgit show <first-hash>both still contain it. - State the order of correct remediation before doing it: the key is disclosed the moment it is pushed, so ROTATE it first. Simulate rotation by generating a new key and updating the config.
- Clean the history:
git filter-repo --path app.env --invert-pathsremoves the file from every commit (the olderfilter-branchand BFG do the same job). Run it here and it REFUSES: filter-repo only rewrites a fresh clone, so a history rewrite cannot happen by accident in a working copy with unpushed work in it. On a real repository you would take a freshgit clone --mirror; in this disposable one, add--force. Confirm the key no longer appears anywhere ingit log -p. - Force-push the rewritten history. First run
git remote -v: it is EMPTY. filter-repo deletes theoriginremote on purpose, so a rewritten history is never pushed over a shared one without a deliberate step. Re-add it (git remote add origin ~/remote.git), thengit push --force --all origin. Explain, in one sentence, why every other clone is now inconsistent and must re-clone. - Prevent a recurrence: add a pre-commit hook (or
gitleaks) that refuses to commit staged content matchingAPI_KEY_VALUE, and prove it blocks a new attempt. It is a COMMIT hook:git addwill stage the file happily, and the refusal comes atgit commit. - Add
*.envto.gitignoreand confirm a fresh secret file is no longer even a candidate ingit status. Then note what.gitignoreis not:git add -fstages an ignored file anyway, which is why the hook in step 6 is the control and the ignore rule is only a convenience.
Verify
cd ~/lab-secret/work
git log --all -p | grep -c 'API_KEY_VALUE' # 0 after filter-repo (non-zero before)
git remote -v | grep -q origin && echo "origin re-added after filter-repo removed it"
touch fresh.env; git status --porcelain | grep -q fresh.env || echo "fresh.env is not a candidate"
echo 'API_KEY_VALUE=x' > new.env && git add -f new.env
git commit -m "try" 2>&1 | grep -qi 'secret\|blocked' && echo "hook blocks secrets"
git reset -q new.env; rm -f new.env fresh.env
The count going from non-zero to zero across filter-repo is the history clean. But the lab's real lesson is the order: by the time you are cleaning history the key has already been disclosed, so rotation in step 3 is the actual remediation and everything else is tidying up after it.
Notes
"Removing a secret from the latest commit" is the reflex to unlearn. The secret is in every earlier commit and in every clone anybody has taken; the deletion commit only stops it being in the tip. A pushed key is a disclosed key -- rotate it, then clean up.
This is an independent study companion for CompTIA Linux+ and is not produced by or endorsed by CompTIA.