Cloud Security Office Hours Banner

LiteLLM / PyPI 2026

Step-by-step kill chain mapped to MITRE ATT&CK Cloud, sourced from official post-mortems and primary technical analyses.

March 2026 Critical PyPI / AI Infra

LiteLLM / TeamPCP - Poisoned Security Scanner → Stolen PyPI Token → .pth Autorun → Cloud Credential Sweep → Kubernetes Lateral Movement

On March 24, 2026 two malicious releases of litellm, versions 1.82.7 and 1.82.8, appeared on PyPI. LiteLLM is the open-source LLM gateway that sits in front of model providers for a very large number of AI applications, and it draws on the order of 95 million downloads a month, so the blast pattern was wide by construction. The releases were live for roughly forty minutes before PyPI quarantined the project, and they still collected tens of thousands of downloads. The interesting part is upstream of the package: the maintainers traced the compromise to Trivy, the vulnerability scanner running inside LiteLLM's own CI/CD pipeline. A threat actor group tracked as TeamPCP had poisoned that scanner, and the scanner step read the pipeline's secrets, which handed over the project's PyPI publishing token. Neither malicious version has a matching tag or release in the LiteLLM GitHub repository: the attacker uploaded straight to PyPI and skipped the release workflow entirely. Installed, the package planted a .pth file that runs on every Python interpreter start, harvested cloud and AI provider credentials, probed Kubernetes for lateral movement, and installed a systemd unit that beaconed for further payloads.

~40 minutes live on PyPI before quarantine
Tens of thousands of downloads in that window
Two versions: 1.82.7 and 1.82.8, neither with a matching git tag
Root cause: the security scanner in CI, not the maintainers' laptops
📄 LiteLLM - Security Update: Suspected Supply Chain Incident ↗ 📄 Cycode - LiteLLM Supply Chain Attack: What Happened and How to Respond ↗
🧰 Resource Development - Poisoning the Scanner That Everyone Trusts
01
TeamPCP compromised Trivy, the vulnerability scanner that LiteLLM and thousands of other projects run as a build step
T1195.001 - Compromise Software Dependencies and Development Tools

The entry point was not a typosquatted package or a phished maintainer. It was a security tool. Trivy is a scanner teams add to CI precisely to reduce supply chain risk, which means it is installed early in the job, runs with the pipeline's permissions, and is rarely pinned or reviewed with the suspicion applied to application dependencies. Researchers place the scanner compromise in the days immediately before the malicious publish, roughly March 19 to 23, 2026. LiteLLM's own advisory states that the incident appears linked to the broader Trivy compromise. Nothing about the LiteLLM codebase had to be touched for this to work: the attacker only had to be present in a step that the pipeline ran on the maintainers' behalf.

Compromised component: Trivy, a vulnerability scanner invoked as a CI step
Approximate window: March 19 to 23, 2026, ahead of the March 24 publish
Why this target: Security tooling runs early, with pipeline credentials, and is trusted by definition
Structural problem: The tools that scan the supply chain are themselves part of the supply chain
TrivyTeamPCPScanner CompromiseT1195.001
🔑 Credential Access - The Pipeline Handed Over Its Publishing Token
02
Running inside LiteLLM's CI, the poisoned scanner read the job environment and exfiltrated the project's PyPI publishing token
T1552.007 - Unsecured Credentials: Container API T1555 - Credentials from Password Stores

A CI job is a shared credential space. Every step in it can read the environment, the mounted secret files, and in most configurations the tokens injected for later publish steps. One LiteLLM maintainer stated plainly that the breach originated from Trivy used in their CI/CD pipelines. The scanner step did not need to escalate anything: it simply read what the runner already held and shipped the PyPI publishing token out. This is the same lesson that tj-actions/changed-files taught a year earlier and that Mini Shai-Hulud taught two months later, arriving each time by a different door: a build step is a principal, and its blast radius is every secret in the job.

Stolen credential: The LiteLLM project's PyPI publishing token
Mechanism: A trusted CI step reading the job's own environment and secret mounts
No escalation needed: The runner already held the token for the release job
Defense gap: Long-lived publishing tokens present in jobs that also run third-party tooling
CI SecretsPyPI TokenToken TheftT1552.007
Initial Access - Two Releases That Never Existed in Git
03
Versions 1.82.7 and 1.82.8 were uploaded directly to PyPI thirteen minutes apart, bypassing the release workflow, with no corresponding tag on GitHub
T1195.002 - Compromise Software Supply Chain

At 10:39 UTC on March 24, 2026 version 1.82.7 went up carrying an obfuscated payload inside proxy_server.py. At 10:52 UTC version 1.82.8 followed with a cleaner delivery mechanism, the .pth file described in the next step. PyPI administrators quarantined the project the same day. Because the upload went straight to the index, the repository never showed a matching tag or release, which is the single cheapest detection available here and the one most consumers never check: the artifact and the source of truth disagreed for the entire window. The official LiteLLM Proxy Docker image was not affected, because its dependencies were pinned. Everyone who ran an unpinned pip install or built an image with a floating requirement during those minutes was.

10:39 UTC: 1.82.7 published, obfuscated payload inside proxy_server.py
10:52 UTC: 1.82.8 published, adding the litellm_init.pth autorun mechanism
Same day: PyPI administrators quarantine the project
Tell: No matching git tag or GitHub release for either version
Not affected: The official LiteLLM Proxy Docker image, which pins its dependencies
Direct UploadNo Git TagUnpinned InstallT1195.002
💻 Execution - A .pth File Runs Before Your Code Does
04
litellm_init.pth in site-packages executed on every Python interpreter start, whether or not the application ever imported litellm
T1546 - Event Triggered Execution T1027 - Obfuscated Files or Information

This is the detail worth carrying away from the whole incident. A .pth file in site-packages is processed by Python's site module at interpreter startup, and lines beginning with import are executed. So the payload ran before any application code, before any import guard, and in every Python process on the machine, including ones that had nothing to do with LLM traffic. A developer who installed the package once and never called it was still executing the payload on every python invocation. One accidental side effect became a useful behavioral indicator: some hosts saw a fork bomb pattern producing extreme lag or memory exhaustion, which is how a number of teams noticed anything at all.

Artifact: litellm_init.pth in the environment's site-packages
Trigger: Python's site module at interpreter startup, before application code
Scope: Every Python process in that environment, imported or not
Accidental tell: A fork bomb pattern causing severe system lag or memory exhaustion
Hunt command: find / -name 'litellm_init.pth' 2>/dev/null
.pth Autorunsite-packagesPre-Import ExecutionT1546
🔑 Credential Access - Sweeping Cloud, AI and Cluster Secrets
05
Stage one scanned environment variables and local files for AWS, GCP and Azure credentials, model provider API keys, SSH keys, kubeconfigs and database passwords
T1552.001 - Credentials In Files T1552.004 - Private Keys

An LLM gateway is an unusually rich place to land. By design it holds provider API keys for OpenAI, Anthropic, Azure OpenAI and others, and it usually runs somewhere that also carries cloud credentials so it can reach logging, caching and storage. The payload collected exactly that: environment variables, cloud provider credentials across all three major clouds, model provider keys, SSH private keys, Kubernetes configuration files, and database passwords. Harvested material was encrypted and posted to models.litellm.cloud, a domain chosen to read as project infrastructure in any egress log skimmed by a human.

Targets: Environment variables, AWS/GCP/Azure credentials, LLM provider API keys, SSH private keys, kubeconfigs, database passwords
Exfiltration: Encrypted POST to models.litellm.cloud
Second C2 domain: checkmarx.zone, also named after a security vendor
Why the gateway hurts: One host holds provider keys and cloud credentials together
Credential SweepLLM API KeysLookalike C2 DomainT1552.001
🔀 Lateral Movement - Service Account Tokens Inside the Cluster
06
Stage two used mounted Kubernetes service account tokens to probe the internal API and look for privilege escalation paths
T1078.004 - Valid Accounts: Cloud Accounts T1613 - Container and Resource Discovery

Where the package landed in a container, the payload reached for the service account token that Kubernetes mounts into pods by default and used it to query the API server for what that identity could reach. That default mount is the point: a pod running an LLM proxy has no business enumerating cluster resources, but unless automountServiceAccountToken is disabled the token is sitting on the filesystem waiting to be used. Responders were told to look for pods named node-setup-* in the kube-system namespace, a name chosen to blend into cluster housekeeping.

Credential used: The pod's mounted Kubernetes service account token
Activity: Internal API probing to map reachable resources and escalation paths
Cluster IoC: Pods named node-setup-* in the kube-system namespace
Cheap control: automountServiceAccountToken: false on workloads that never call the API
KubernetesService Account Tokenkube-systemT1613
🔐 Persistence - A systemd Unit Named After a Monitoring Tool
07
Stage three installed a systemd service masquerading as sysmon and beaconed to command and control for follow-on payloads
T1543.002 - Create or Modify System Process: Systemd Service T1036.005 - Match Legitimate Name or Location

The last stage made removal of the package insufficient. A systemd unit was written alongside a payload at ~/.config/sysmon/sysmon.py, named to resemble a system monitoring agent, and it polled attacker infrastructure for additional code. Anyone who responded by downgrading litellm and moving on left a live C2 channel running on the host. Full recovery meant hunting the unit and the config directory, rotating everything the host could reach, and rebuilding self-hosted runner images and CI caches, because a poisoned cache reintroduces the artifact on the next build.

Persistence artifacts: ~/.config/sysmon/sysmon.py and a matching sysmon.service systemd unit
Behavior: Beacon to C2 and poll for additional payloads
C2 domains: models.litellm.cloud, checkmarx.zone
Why downgrading is not enough: The unit survives package removal, and CI caches re-serve the artifact
systemdMasqueradingC2 BeaconT1543.002T1036.005

🛡 How to Defend Against This Chain

Pin dependencies with hashes, and treat an unpinned pip install in a build as a control failure. The single reason the official LiteLLM Proxy image escaped is that it pinned its dependencies. Use a lockfile with hashes (pip install --require-hashes, uv.lock, or Poetry's lock) so a version published in a forty-minute window cannot enter your builds at all. Floating requirements turn every minute of every registry's uptime into your exposure window.
Compare the artifact against the source of truth before you trust it. Neither malicious version had a matching tag or release on GitHub. A release-integrity check that fails the build when a package version has no corresponding signed tag, or that requires provenance attestation, catches this class of direct-to-index upload without needing to understand the payload. Prefer SLSA provenance and PyPI Trusted Publishing over long-lived API tokens on the publishing side.
Stop keeping publishing tokens in jobs that run third-party tooling. The scanner read the token because the token was there. Split scanning and publishing into separate jobs with separate permissions, mint publishing credentials through OIDC-based Trusted Publishing rather than storing a static token, and scope every remaining CI secret to the single job that needs it. Purge build caches and rebuild self-hosted runner images after any pipeline compromise, or you will restore the payload yourself.
Scan for the artifacts, not just the version number. Search hosts and images for litellm_init.pth in site-packages, for ~/.config/sysmon/sysmon.py and its systemd unit, and for pods named node-setup-* in kube-system. Downgrading to 1.82.6 removes the package; it does not remove the persistence the package installed.
Give the AI gateway its own blast radius. A component holding model provider keys should not also hold cloud administrator credentials, and its pods should not automount a Kubernetes service account token they never use. Set automountServiceAccountToken: false, scope the workload identity to least privilege, and put provider keys in a secrets manager with short-lived retrieval rather than in the process environment.
Filter egress and hunt the lookalike domains. Both C2 domains were named to pass a skim: models.litellm.cloud reads as project infrastructure, checkmarx.zone as a security vendor. Default-deny egress from build runners and AI workloads, allow-list the provider endpoints you actually call, and sweep EDR and DNS logs for both domains across the March 2026 window.

Related defense topics