The Key Was Deleted in 3 Hours. It Worked for 4 Months.

By Imad Orabi Alnajjar | 2026-11-09 | security

On 27 May 2026 at 18:53 UTC, a configuration file called opencode.json was committed to LiteLLM's public LiteLLM-Labs/lite-harness repository. It held an administrative key to an internal service. The mistake was spotted quickly: about three hours later a second commit deleted the file, and the key was noted as needing revocation. Nobody revoked it [1].

On 10 October, 136 days later, an attacker used that key. Eleven days after that, LiteLLM's own release pipeline built, signed and published a malicious litellm==1.107.0 to PyPI and to its container registries [1].

LiteLLM's canonical record of the incident, published on 23 October, lists the stolen credentials, the timestamps and the steps in order [1]. Read in order, it is less a story about a clever attacker than a chain of ordinary decisions, each reasonable on the day it was made. Two of them are specific to teams that build agents, and those are the two worth checking in your own setup this week.

Deleting the file did not delete the key

The follow-up commit removed opencode.json from the repository's current files. It did not remove the commit that added it. That commit "stayed in the repository's public history until October 22" [1], roughly 147 days in all.

This is how Git works, and it takes three commands to see it. We reproduced the pattern in a throwaway repository with a fake key: one commit adds a config file, the next deletes it, and the working tree looks clean.

# Which files were ever deleted, and in which commit?
git log --all --diff-filter=D --name-only --format='%h %s'

# Print a deleted file as it was just before the deleting commit
git show <deleting-commit>^:opencode.json

# Every commit that added or removed a given string
git log --all -S'ADMIN_KEY' --format='%h %s'

The second command printed our fake key back in full. Anyone who cloned the public repository during those 147 days could do the same.

LiteLLM names the cause plainly: "Because of human error, nobody followed through on revoking the administrative key" [1]. Its fix is as much process as tooling. Secret-scanning alerts will open tickets automatically, "and the ticket is only closed when revocation is confirmed" [2]. The point is the closing condition. A leak that is noticed, deleted and written down still counts as open until the key stops working.

An agent platform is a credential store with a list endpoint

The key opened the LiteLLM Agent Platform, which the record describes as "an experimental project for self-hosted cloud agents to file PRs to GitHub repositories" [1]. To open pull requests, the platform needed a GitHub token. The one it was given was not a personal access token scoped to the right repositories. It was the GitHub CLI OAuth token of one of the company's two GitHub organization owners [1].

At 21:27:10 UTC on 10 October, a single request, GET /api/v1/managed_agents/agents?limit=100, returned the configuration of every managed agent. That response contained the owner's token [1].

The record describes no exploit at this step, only authenticated requests made with the leaked key [1]. An agent's configuration includes the credentials the agent acts with, so an endpoint that lists configurations lists credentials. Any platform that runs agents on someone's behalf holds the same kind of bundle, and the read and list paths are where it leaks.

The kind of token made it worse. LiteLLM's record notes that GitHub CLI tokens "are not scoped, are post-2fa, and not subject to GitHub's 'super-user' 2fa reauth flows" for sensitive organization or repository actions [1]. From that request onward, the attacker could act as an organization owner.

If you read an early account, this part changed. LiteLLM's first post, written during the response, suspected the tokens came from the logs of an internal LiteLLM deployment [1][3], and Wiz's report carried that statement [4]. The canonical record corrects it: the request logs show the first token came from the Agent Platform [1]. LiteLLM left the early text in place and marked it [3].

Eleven days with the owner's keys

From the owner's token to the malicious release took ten and a half days. The record's timeline, trimmed to the steps that mattered [1]:

When (UTC) What happened Credential used
27 May, 18:53 opencode.json with the platform's admin key is committed to a public repository none
10 Oct, 21:27 The agent list endpoint returns the owner's GitHub CLI token platform admin key
10 Oct, 22:11 to 22:12 Two throwaway branches run workflows that post CI secrets to webhook.site, 24 seconds apart; two more maintainers' tokens are taken owner's token
10 Oct, 22:14 Both workflow runs are deleted owner's token
12 Oct A second round of exfiltration across private repositories owner's token
21 Oct, 10:08:29 A push changing the release workflow's source is rejected by branch protection second maintainer's token
21 Oct, 10:08:34 The same change goes in, overriding branch protection owner's token
21 Oct, 10:46 The staging build runs from the lookalike fork Berri-AI/litellm owner's token
21 Oct, 11:11 The production gate is approved second maintainer's token
21 Oct, 11:39 to 11:40 Seven litellm==1.107.0 wheels reach PyPI none
21 Oct, 11:53 to 11:54 The production and staging run records are deleted third maintainer's token
21 Oct, 12:06 An outside researcher opens the issue identifying the compromise none
21 Oct, by 13:23 PyPI quarantines the release none

Three things in that table deserve a second look.

The rejection and the override are five seconds apart. Branch protection did its job. It required a pull request, two passing checks and the codeowner's approval, and it rejected the push [1]. The owner's token was simply allowed to override it.

The release gate had been loosened that week. A release normally had to be started by an authorized engineer and approved by "a special account locked to a physical device in our office" [1]. Earlier that week the rule was relaxed for a push toward more frequent releases, so that any two engineers could trigger and approve a release [1]. The attacker held two engineers' tokens. LiteLLM adds, fairly, that an owner could have removed the restriction anyway, and believes the attacker used separate tokens to keep the audit log quiet [1].

The malicious release was validly signed. LiteLLM's later post describes version 1.107.0 as having a valid signature and valid Trusted Publishing provenance, and states that "no pipeline controls can defend against an attacker who is an organization owner, because the owner owns the pipeline" [2]. A signature proves which pipeline built an artifact. It says nothing about who controlled that pipeline at the time.

The damage window was short by supply chain standards. The PyPI wheels were live from 11:39 to 13:23 UTC, and the affected Docker tags from 11:46 to 14:20 UTC [3]. Wiz, citing ClickPy data, counts approximately 49,000 downloads of the malicious version [4]. The payload ran on every Python start, collected cloud, browser and AI provider credentials, and queried the LiteLLM proxy's own credentials table [4]. Wiz observed stolen AWS credentials being tested about four and a half hours after exfiltration, including Bedrock model listing calls it describes as consistent with LLMjacking [4].

Detection came from outside. Lidor Machluf of Upwind Security opened the public issue 27 minutes after the first wheel went up, and LiteLLM's incident response began at 13:02 [1].

Not a sequel to March

Readers of our earlier coverage will remember that LiteLLM was compromised in March as well. The two incidents are not connected. LiteLLM's 30 October post says they "look similar from the outside" but went through different surfaces [2]:

March 2026 October 2026
Way in A compromised dependency (Trivy) running in CI A leaked admin key for an internal experimental service
What the attacker held Static release credentials from CI environment variables An organization owner's GitHub token, then more tokens
How the package got out Published directly with stolen keys Built, signed and published by the normal pipeline

The controls LiteLLM built after March, including Trusted Publishing, signed images, immutable release tags and pinned GitHub Actions, worked as designed and limited the damage: existing release tags could not be overwritten [2]. Their goal was to keep untrusted code and long-lived credentials away from the release path [2]. October came in through an owner account instead.

We mapped the 25 commitments onto the chain

LiteLLM's 30 October post ends with 25 numbered commitments, each with a status and a target date, and a promise to update the table in place [2]. We counted the statuses as published on 8 November: 3 complete, 13 in progress, 9 planned. Then we matched each commitment to the step of the attack it would have broken. The matching is our reading; where LiteLLM ties a commitment to a step itself, the table says so.

Step in the attack Commitment that breaks it Status on 8 Nov
Key committed, never revoked #10 secret scanning and push protection on every repository; #11 ticketed revocation (LiteLLM ties it to the May key) In progress, 30 Nov; Planned, 30 Nov
Exposed platform hands over a token #12 identity-aware proxy in front of internal deployments; #24 automations never authenticate as a person In progress, 30 Nov; In progress, 31 Jan 2027
That token belongs to an owner #2 dedicated owner accounts that never use tokens, OAuth apps or the gh CLI Complete
CI secrets taken through throwaway branches #25 secrets only in environments limited to protected branches (LiteLLM: would have stopped the extra token theft, if the stolen token was not an owner's) Planned, 31 Jan 2027
Owner token overrides branch protection #18 rulesets on main and the release repository with no bypass actors; #4 alert on any owner-token activity Planned, 31 Dec; In progress, 15 Nov
Stolen token approves the release #1 release approval held only by the device-bound account Complete
Run records deleted, history force-pushed #3 audit logs streamed to Splunk with alerting; #13 alerts on every action the attacker took Complete; In progress, 30 Nov
Bad artifact already published #19 to #21 a verifier outside GitHub that compares releases with the tag and yanks on mismatch Planned, 31 Dec

Read from the bottom, the order makes sense. The three completed items sit at the end of the chain: who can be an owner, who can approve a release, and whether anyone sees what an owner does. Those are the steps that turned a leaked key into a poisoned package, and closing them first limits the worst outcome. Commitment #1 is not really a new control, though. It largely restores the device-bound approval rule that was relaxed the week of the incident [1].

Read from the top, the picture is less finished. The first link, a key that was found, deleted and never revoked, is closed by #10 and #11, and on 8 November neither was complete. A team cannot fix everything at once, and LiteLLM published this table with dates and a promise to say when items slip, which is more than most companies do after an incident. But the cheapest step in the whole chain to break was the first one.

What to check in your own agent setup

If you installed 1.107.0. You are affected if you installed an unpinned LiteLLM with pip between 11:39 and 13:23 UTC on 21 October, or ran the v1.107.0, latest, main-latest or main-stable images between 11:46 and 14:20 UTC [3]. LiteLLM's cleanup steps start with a warning not to run pip install first, because any Python start re-runs the payload; then reinstall a clean pinned version and rotate everything the host could reach [3]. If you verify LiteLLM images with cosign, note that the signing key was replaced on 23 October, and releases from 24 October use the new key [3].

The attack needed three things: a key nobody revoked, a token nobody scoped, and a release gate loosened for a busy week. None of them was exotic. The first is a ticket that was never closed, the second is a shortcut that is easy to take in an agent prototype, and the third is a decision a team makes when it is trying to ship faster. That is the uncomfortable part. Every one of them can happen on a team that is doing most things right.


References

[1] Oliver Jensen and Mateo Wang, LiteLLM: The attack chain behind the v1.107.0 LiteLLM release (2026-10-23). Blog

[2] Oliver Jensen, Krrish Dholakia and Ishaan Jaffer, LiteLLM: What we are changing after October 21 (2026-10-30). Blog

[3] LiteLLM: Contained: Oct 21, 2026 LiteLLM release v1.107.0 Compromised (2026-10-21). Blog

[4] Wiz Research: LiteLLM Compromised, Steals Credentials (2026-10-21). Blog