Reported symptoms
platform-tools is an internal repository with about forty
contributors, three forks and a long tail of open pull requests. Six
weeks ago somebody added a deployment bootstrap file containing a cloud
provider access key, in a commit that also fixed a genuine bug and was
reviewed and approved on that basis.
Secret scanning raised the alert at 09:58 UTC this morning.
What happened next is the incident:
- 10:14. An engineer starts rewriting the history on their local clone. The ticket is updated: removing the secret from history.
- 11:20. The rewritten branch is force-pushed. The ticket is closed as resolved.
- 11:52. A colleague, who has not read the ticket, runs an ordinary pull followed by a push.
- 12:05. Somebody notices the file is back. Every commit that was removed is present again, merged in by a routine push from a clone that still had the old history.
- Throughout all of this, the key is live. Nobody has disabled it.
At 12:20 somebody finally opens the provider console. The key was last used at 03:41 this morning, by a service the organisation does not use, in a region it has never deployed to.
Evidence provided
$ gh secret-scanning alert view 118 --repo example-org/platform-tools$ aws iam get-access-key-last-used --access-key-id AKIAEXAMPLENOTREAL01$ git rev-list --objects --all --reflog | grep -F 4f2ab91$ git push --force origin refs/pull/941/head$ git filter-repo --version$ git log --format='%h %ad %an %s' --date=iso -5 origin/mainWork the evidence before reading on
Three separate things went wrong and only one of them is a Git problem.
- The alert was raised at 09:58 and the provider log was not read until 12:20. What question does the provider log answer that no amount of repository work can, and how would the answer have changed the first two hours?
- The rewrite was undone by somebody running two entirely ordinary commands. What did the rewrite change, what did it not change, and which of those did the colleague have a copy of?
- The commit remains reachable through a pull request reference and in at least one fork. Name what could remove those, and say who would have to agree.
- Assume for a moment that the rewrite had worked perfectly and every trace of the commit had vanished from every copy. Would the key have stopped working?
Before continuing: answer question four out loud, and then say what the first action in this incident should have been.
Root cause
A rewrite removes an object; only the issuer removes access
This is the whole scenario in one sentence. Git objects live in repositories; the credential lives at the provider. Deleting the object changes what your repository contains and changes nothing at all about what the key can do.
The guidance from the hosting platform puts revocation first for exactly this reason: once the secret is revoked or rotated, it can no longer be used for access. Every subsequent step becomes optional cleanup rather than remediation, and its urgency drops from minutes to days.
Here the key was live for six weeks and was used at 03:41 this morning from somewhere the organisation does not operate. That is not a hypothetical risk any more, and it was discoverable in one command at 09:59.
The rewrite was done in place, on a repository other people had cloned
Rewriting produces new commit identifiers for everything downstream of the change. The old identifiers do not disappear from other people’s clones; those clones simply now have two histories that Git is perfectly happy to merge.
The tool’s own documentation warns about this directly: people who cloned the original repository still have the old history, and if they run a pull followed by a push, the merge pushes back everything you carefully removed. That is exactly what the 11:52 push did, thirty two minutes after the ticket was closed.
A rewrite of a shared repository is a coordinated cut-over, not a command. It needs a write lock, a fresh clone, an announcement, and every contributor deleting their clone rather than pulling.
Some copies are not yours to delete
Even a perfect cut-over leaves reachable copies on the hosting platform. The commit survives in clones and forks. It is reachable by its identifier through cached views. And it lives in pull request references, which the server protects and will not accept a force push into.
Removing those requires the platform operator, through a support request, and the platform will decline where rotating the credential would mitigate the risk. That policy is not obstruction. It is the platform telling you that rotation is the control and history surgery is housekeeping.
Resolution
- Disable the key at the provider. First, before the rewrite, before the write-up, before working out how it got there. This is the only action that reaches every copy simultaneously, including the ones in forks you cannot enumerate, and it takes under a minute.
- Read the provider credential report immediately after revoking, and treat this as a confirmed compromise rather than a potential one, because it shows use from a service and region the organisation does not operate. Preserve that audit trail before it ages out of its retention window.
- Enumerate what the key was permitted to do and what it actually did. Those are two different investigations and the second one needs the provider log, not the repository.
- Issue the replacement through the secret manager and inject it at runtime. Do not paste it into a file, a ticket or a chat message, however temporarily; the original credential reached the repository the same way.
- Record the commit identifier, the blob identifier, the file path, the discovery time and the alert reference before any rewriting. A rewrite changes every identifier you will later want to cite in the report.
- Stop the in-flight rewrite and run it as a coordinated cut-over instead: lock the repository against writes, take a fresh clone, use a rewriting tool at version 2.47 or later with its sensitive-data removal mode, and push with mirror or prune so that deleted references actually disappear from the server rather than lingering.
- Instruct every contributor to delete their clone and clone again rather than pulling, and to delete local tags before refetching them. A single ordinary pull and push from one stale clone undoes the entire exercise, as it did at 11:52.
- Ask the platform operator to dereference the affected pull requests and remove cached views, and record their answer. Expect a refusal where rotation already mitigates the risk, and treat that refusal as confirmation that rotation was the real remediation all along.
Verification
- Confirm at the provider console that the key is disabled and that its last-used timestamp is frozen at or before the revocation time. A timestamp that keeps advancing means you disabled a different key, or a key in a different account.
- Attempt to authenticate with the old key from a controlled host and confirm it is refused. This distinguishes a genuinely disabled key from one that merely displays as inactive in a stale console view.
- Confirm the workload is running on the replacement by reading the provider access log rather than the application log. The party that grants access is a better witness than the party claiming it.
- Take a fresh mirror clone from the server and search every reachable object for the credential, rather than checking that one branch looks clean. Reachability is the property that matters and it is not visible from a working tree.
- Confirm the scanning alert is closed as revoked rather than dismissed, so the next reviewer can see which of the two actually happened.
- Confirm what is still true: the pull request reference resolves the old commit until the platform operator acts, and at least one fork still carries it. Record both as open items with named owners rather than letting the incident close over them.
- Watch the push event log for 24 hours after the cut-over and confirm nobody has reintroduced the history. This is the check that failed silently the first time, and it failed within half an hour.
Prevention
- Write the ordering down: revoke in 15 minutes, rewrite in 24 hours, never the other way round. Under pressure people reach for the action that looks like removal instead of the one that is removal, and a rule in a runbook is what interrupts that.
- Enable repository-level push protection. It is disabled by default and must be switched on by an administrator. The protection that is enabled by default is the user-level one, and it only covers pushes to public repositories, so an internal repository has none until somebody acts.
- Scan on the server, not only on the workstation. A pre-commit hook is a convenience that the person in a hurry can skip. Server-side scanning is the control, and it is what raised this alert.
- Remove the reason to commit a credential. Federated short-lived access makes a key pasted into a file worthless within the hour. A quarterly inventory targeting zero long-lived provider keys is what turns that from an aspiration into a number.
- Keep the rewriting tool at 2.47 or later everywhere. Its sensitive-data removal mode fetches all references and prints the cleanup instructions for other copies, at exactly the moment somebody is too rushed to look them up.
- Rehearse the cut-over annually on a throwaway repository, including the write lock, the re-clone instruction and the tag deletion. The failure at 11:52 was not a knowledge gap; it was a procedure nobody had ever run end to end.
This scenario deliberately stops at the credential. Git, CI/CD & GitOps for Infrastructure Engineers - Part XXXV (SecretsInGit) covers the repository-side mechanics in far more depth, and Part XCIV (IncSecretLeak) covers the incident process around them. Read both alongside this one if you own a repository estate rather than a credential estate.