Responding to a leaked credential Jump to heading
A credential in a repository is live from the moment it is committed until the moment it is revoked, and every intermediate action — deleting the file, force-pushing, making the repository private — changes nothing about that. The instinct to clean up first is strong and costs the hours during which the exposure continues. This recipe is the sequence that ends the exposure fastest, within secret scanning and remediation.
When to use this approach Jump to heading
- A scanner, a colleague or a provider alert has reported a credential in a repository.
- You have found a credential in history while working on something else.
- A repository containing credentials is about to become public.
- A laptop or a CI runner with repository access has been compromised.
- If the credential is an expired test value with no privileges, triage it and move on — the sequence below is for live ones.
Step 1 — Establish what the credential can reach, in two minutes Jump to heading
Scope decides urgency, and urgency decides how much process to skip.
# What is it, and what is it for?
git show "$COMMIT" -- "$FILE" | grep -iE 'key|token|secret|password' | head -3 # Test validity — this is the only question that matters first
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $VALUE" https://api.example.com/v1/me # Verification: if that returned 200, this is a live incident Step 2 — Rotate, before anything else Jump to heading
# 1. Issue a replacement
# 2. Deploy it wherever the old one is used
grep -rn "$(printf '%s' "$VALUE" | cut -c1-8)" --include='*.yml' --include='*.env' . 2>/dev/null | head # 3. Revoke the old one
# 4. Confirm it is dead
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $VALUE" https://api.example.com/v1/me # expect 401 # Verification: the replacement works everywhere the old one did
gh workflow run smoke-test.yml && gh run watch Revoking before deploying the replacement breaks production; deploying before revoking leaves a window. Which order you choose depends on whether an outage or an extra ten minutes of exposure is worse, and that is a decision to have made in advance rather than at the time.
Step 3 — Check for use you did not authorise Jump to heading
Rotation ends future access. It says nothing about what happened while the credential was live.
# Provider audit logs, for the period between commit and revocation
leaked_at=$(git log -1 --format='%aI' "$COMMIT")
echo "credential exposed from $leaked_at" # Requests from addresses or agents you do not recognise
# (provider-specific; the shape is always the same)
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.example.com/v1/audit?since=$leaked_at" | jq -r '.[] | "\(.ip) \(.action)"' | sort | uniq -c | sort -rn | head # Verification: account for every source of use during the window SAFETY WARNING — do not skip this step because the rotation is done. A credential exposed for eight months may have been used, and the rotation closes the door without telling you whether anyone came through it. If the audit log does not go back far enough to cover the exposure window, say so explicitly in the incident record rather than treating absence of evidence as evidence of absence.
Step 4 — Remove it from the working tree and stop the recurrence Jump to heading
# Remove from HEAD, so it cannot be re-committed
git rm --cached config/secrets.yml
printf 'config/secrets.yml\n' >> .gitignore
git commit -m 'chore: stop tracking the local secrets file' # The pattern that caused it: a file that should never have been tracked
git log --diff-filter=A --format='%h %ad %an' --date=short -- config/secrets.yml # Verification: it is ignored and untracked
git check-ignore -v config/secrets.yml
git ls-files config/secrets.yml | wc -l # expect 0 Step 5 — Decide about history without urgency Jump to heading
With the credential dead, rewriting history is a cleanup decision rather than a security one.
# How much would a rewrite touch?
git log --all --format='%H' -S"$(printf '%s' "$VALUE" | cut -c1-12)" | wc -l
git rev-list --count --all # The reasons that justify it, honestly:
# the repository is about to become public
# a policy or contract requires removal
# the credential could not be rotated # If proceeding: mirror first
git clone --mirror . ../backup-before-rewrite.git
git filter-repo --replace-text /tmp/replacements.txt # Verification: the value is gone and everyone has been told to reclone
git log --all -S"$(printf '%s' "$VALUE" | cut -c1-12)" --oneline | wc -l Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Should we make the repository private while we clean up? Jump to heading
It does not help and it delays the useful work. A credential that has been public is compromised, and making the repository private does not retrieve the copies that exist. Rotate, and change visibility only if there is a separate reason to.
What if rotation would cause an outage? Jump to heading
Then the decision is between an outage and continued exposure, and it should be made explicitly by someone who can weigh both. Frequently a middle path exists — issue a second credential, migrate consumers, then revoke the first — which costs an hour and avoids both. Knowing which of your credentials support that is worth establishing before an incident.
Do we have to tell anyone? Jump to heading
That depends on what the credential reached and on your obligations, and it is a question for whoever owns those rather than for the person who found it. What the engineering response owes is an accurate record: what was exposed, for how long, and what the audit logs showed — which is exactly what Steps 1 to 3 produce.
Related Jump to heading
- Secret Scanning & Remediation — the parent topic and the layers that prevent this.
- Rotating Credentials After a History Rewrite — why the rewrite is never the remediation.
- Removing a Leaked Secret From Git History — the rewrite itself, once it is justified.