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
The first question, and what it changesA credential that authenticates successfully is a live incident and rotation starts immediately. One that does not may still be worth tracking down, but it is a hygiene task rather than an incident, and the two deserve different amounts of urgency.Does the credential still authenticate?yesLive incidentrotate nownoHygienerecord and clean upcannot tellTreat as liverotate anywaywhen in doubt, rotate — the cost of an unnecessary rotation is minutes

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
The first hour of a leak responseValidity is established in the first minutes, rotation follows immediately, and the audit review runs while the replacement is being deployed. Cleaning the working tree and deciding about history come after the exposure has ended, not before.Test validitylive or not0-5 minRotateexposure ends5-20 minAudit reviewwas it used?20-40 minClean the treestop recurrence40-60 minHistory decisiondeliberate, not reflexivelateronly the first two rows are urgent; treating the rest as urgent is what delays them

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
What rotation and rewriting each achieveRotation ends the credential's validity everywhere, immediately, and cannot be undone by any copy of the repository. Rewriting removes the value from your history and does nothing about the clones, forks and caches that already exist.Rewrite historyRotateends the exposurenoyesaffects existing clonesnonot applicabletime to completehours, plus falloutminutesreversiblenoyes, issue anotherthe first row is why the order in this page is not negotiable

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.