Find the key in the history, then stop the next one at commit

short · 45 min · Objective 4.1

Task

Commit a fake cloud key to a repository, remove it in a later commit, and show that it is still recoverable from the history. Then write a pre-commit check that refuses the next one, and record the response order the lesson gives: revoke and rotate first, clean up afterwards.

Steps

  1. Create the repository with git init /tmp/secrets-lab, then give it a local identity: git -C /tmp/secrets-lab config user.email lab@example.com and git -C /tmp/secrets-lab config user.name lab.
  2. Add app.conf containing a fake key in the shape of a cloud access key ID -- AKIA followed by sixteen capital letters or digits, for example aws_key=AKIALABFAKE000000042 -- and commit it the way a developer in a hurry would.
  3. Remove the line in a second commit with the message remove key, and confirm the current files no longer contain it.
  4. Now search the history: git -C /tmp/secrets-lab log -p | grep -E 'AKIA[0-9A-Z]{16}'. The key is still there -- and in a shared repository it would also be in every clone made in between.
  5. Write a pre-commit hook at /tmp/secrets-lab/.git/hooks/pre-commit and make it executable. It inspects the staged changes (git diff --cached) and exits non-zero, printing which rule matched, if they contain a string matching AKIA[0-9A-Z]{16} or a line that begins -----BEGIN and contains PRIVATE KEY.
  6. Try to commit a second fake key and confirm the commit is refused.
  7. Write /tmp/secrets.md: the response order had the key been real (revoke and rotate it, check the provider's logs for any use, then clean the history), why removing it in a later commit was not enough, and where the value should live instead.

Verify

git -C /tmp/secrets-lab log -p | grep -cE "AKIA[0-9A-Z]{16}"
git -C /tmp/secrets-lab grep -cE "AKIA[0-9A-Z]{16}" HEAD || echo "0 in the current tree"
python3 - <<'PY'
import pathlib,subprocess
repo='/tmp/secrets-lab'
def git(*a):
    return subprocess.run(['git','-C',repo]+list(a),capture_output=True,text=True)
before=git('rev-parse','HEAD').stdout.strip()
probe=pathlib.Path(repo,'probe.conf')
probe.write_text('key=AKIA'+'PROBE00000000099'+'\n')
git('add','probe.conf')
c=git('commit','-m','probe')
after=git('rev-parse','HEAD').stdout.strip()
if after!=before:
    git('reset','--soft','HEAD~1')
git('reset','-q','probe.conf')
probe.unlink()
print('commit exit code with a key staged:',c.returncode)
assert c.returncode!=0 and after==before, 'the hook let a recognisable key be committed'
print('the pre-commit hook refused the key')
PY
grep -ciE "revoke|rotate" /tmp/secrets.md

The first must be non-zero: the key survives in the history even though the second commit removed it. The second must find nothing in the current tree. The assertion stages a fresh fake key, tries to commit it, and requires your hook to refuse -- then undoes its own change, so the repository is left exactly as you had it. If the commit went through, the hook is not executable or its pattern does not match.

Notes

Committing is only one of three places to scan. Repository platforms can refuse a push that contains a recognised secret, and a scan of the whole history finds what was committed before either check existed. None of the three makes a leaked key safe -- only revoking it does, which is why rotation comes before cleanup and why the lasting fix is a vault the application reads at runtime.

This is an independent study companion for CompTIA Security+ SY0-801 and is not produced by or endorsed by CompTIA.