When a key leaks, the reflex is to rotate it. That's the right first move and it is in every runbook. But this week showed again that rotation answers the wrong question. It deals with what the attacker took. It does nothing about what the attacker added while they had it, and that is usually what lets them back in.
What this week's CI stealer did
From 10 November, a campaign that Aikido named wfx-loot used thousands of hijacked GitHub accounts to run a secret stealer in at least 1,806 repositories. Stealing CI secrets is not new. What stood out was the stealer's second job. With the access it had just taken, it:
- added federated identity credentials, named
wfx-ci-…, to existing Entra ID applications, so that workflows in the compromised repository could sign in to Azure without any stored secret; - created new long-lived IAM access keys in AWS;
- rewrote AWS role trust policies so that any AWS account could assume them;
- posted forged "success" status checks so that required branch protections were satisfied.
An organisation that rotated every leaked secret on 11 November, and did nothing else, would still have had three open doors on 12 November. None of them shows up in a secret inventory, because none of them is a leaked secret. They are new entries in configuration that nobody reviews often. Our threat research write-up covers how to find the Entra side of it.
This isn't specific to CI
The same pattern shows up in almost every incident we work. Attackers who get privileged access for even a few minutes rarely leave without setting up a way back:
- a new client secret or certificate on an existing Entra application;
- a consent grant to an OAuth app with mailbox access;
- a mailbox forwarding rule or a new inbox rule;
- an extra MFA method registered to a real user;
- a new local administrator, scheduled task or remote-access tool on an endpoint;
- a role assignment on a subscription, or a federation trust added to the tenant.
Resetting the password the attacker used doesn't touch any of these. Neither does rotating the API key they stole. Each one needs to be found and removed separately.
Containment means inventorying change
Our view is that leak response needs a second step alongside "rotate what leaked": list everything that changed while the attacker had access, and decide whether each change should stay. In practice that means:
- Bound the window. Work out the earliest time the credential could have been used and the time it was revoked. Everything in between is in scope.
- Ask what the credential could change, not only what it could read. A pipeline identity with permission to update applications, assign roles or edit trust policies could have done all of those. Its permissions define where you look.
- Pull the change logs for that window. Entra audit logs, Azure Activity, the GitHub organisation audit log, and the equivalent in any other cloud. Filter on the identity, and also on any other identity that changed something in the same period.
- Remove persistence before you rotate. If you rotate first, a planted federated credential or a new access key lets the attacker straight back in to watch you clean up. Close the doors, then change the locks.
- Write down what "clean" means. Agree in advance what evidence is needed to declare an identity clean, so the decision doesn't depend on whoever happens to be on call.
Why it gets skipped
Rotation is quick, visible and easy to report: forty secrets rotated, ticket closed. Inventorying change is slower and crosses teams. The developer who owns the repository doesn't own the Entra application. The cloud team owns the subscription but not the GitHub organisation. Nobody owns the gap between them, and that gap is where persistence sits.
The fix is partly organisational. Someone has to own the incident across those boundaries, with the access to read every log involved. It is also partly preparation. If your Entra audit logs, Azure Activity logs and developer-platform audit logs aren't already being collected somewhere you can query them together, the inventory step turns into a week of exports instead of an afternoon of queries.
How we help
This is the work our incident response team does on every engagement: bound the window, list what changed, remove what shouldn't be there, then rotate. Where we run the SOC, our analysts already have those logs side by side, because on the managed service connectors, parsers and analytical rules for the rest of the estate are built at onboarding. If you would like an independent check on whether a past leak left anything behind, a compromise assessment answers that question.
If your runbook for a leaked secret stops at "rotate", talk to our team.