Recover a secret that was committed and pushed

applied · 40 min · Objective 4.4

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

  1. Commit an application config, app.env, containing a line such as API_KEY=API_KEY_VALUE_1234, and push it to the "remote". Confirm it is there with git log -p.
  2. 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 -p and git show <first-hash> both still contain it.
  3. 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.
  4. Clean the history: git filter-repo --path app.env --invert-paths removes the file from every commit (the older filter-branch and 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 fresh git clone --mirror; in this disposable one, add --force. Confirm the key no longer appears anywhere in git log -p.
  5. Force-push the rewritten history. First run git remote -v: it is EMPTY. filter-repo deletes the origin remote 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), then git push --force --all origin. Explain, in one sentence, why every other clone is now inconsistent and must re-clone.
  6. Prevent a recurrence: add a pre-commit hook (or gitleaks) that refuses to commit staged content matching API_KEY_VALUE, and prove it blocks a new attempt. It is a COMMIT hook: git add will stage the file happily, and the refusal comes at git commit.
  7. Add *.env to .gitignore and confirm a fresh secret file is no longer even a candidate in git status. Then note what .gitignore is not: git add -f stages 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.