How Greywall Prevents Every Stage of the LiteLLM PyPI Supply Chain Attack

On March 24, 2026, litellm versions 1.82.7 and 1.82.8 were published to PyPI with an embedded malware payload. The malware is a multi-stage attack that steals credentials, exfiltrates them to an attacker-controlled server, and attempts lateral movement across Kubernetes clusters.
If you're unfamiliar with the attack, FutureSearch's writeup has the full technical details. Turns out there's a bug in the malware, see be
As these types of attacks become more and more common[^study], there is more and more of a practical need to secure infrastructure.
While greywall was built with the use-case of wrapping AI agent processes in mind, there's nothing stopping someone from using it as a secure wrapper for LiteLLM. Turns out this is easy to do, and doing so stops the supply chain attack at every stage!
[^study]: Here's a study from last year examining the increased rate of attacks.
Mitigation
The payload is delivered via a .pth file (litellm_init.pth) that Python executes automatically on interpreter startup. It operates in three stages:
- Collection: Harvests SSH keys,
.envfiles, cloud credentials (AWS, GCP, Azure), Kubernetes configs, database passwords, shell history, crypto wallets, and environment variables. Queries cloud metadata endpoints (IMDS). - Exfiltration: Encrypts the haul with a hardcoded RSA public key (AES-256-CBC), bundles it into a tar archive, and POSTs it to
https://models.litellm.cloud/, a domain unaffiliated with legitimate litellm infrastructure. - Lateral movement: Reads Kubernetes secrets across all namespaces, attempts to spawn privileged
alpine:latestpods on every node inkube-systemwith host filesystem mounts, and installs a persistent backdoor via systemd at~/.config/sysmon/sysmon.py.
Here's how greywall stops the attack at every stage:
Stage 1: Collection
The malware's first move is to read sensitive files outside the project directory: ~/.ssh/, ~/.aws/, ~/.kube/config, ~/.env, .gitconfig, shell history, and anything matching common secret patterns.
Greywall enforces a default-deny filesystem model via Landlock (Linux 5.13+) and Seatbelt (macOS). When you run greywall -- your-command, the process starts with access to nothing. You explicitly grant read/write access to the project directory, and read-only access to dependency directories like node_modules/. This prevents binaries from attempting to update themselves, an increasingly common anti-pattern we've been seeing.
The collection stage is blocked from accessing secrets.
Stage 2: Exfiltration
Even if the malware somehow gathered data (it can't, see above), it needs to send it somewhere. The payload POSTs an encrypted archive to https://models.litellm.cloud/.
Greywall's network isolation operates at two levels:
Network namespace (Linux): Bubblewrap places the sandboxed process in its own network namespace via
bwrap --unshare-net. The process literally cannot see the host network. It has no access to the host's interfaces, routing table, or DNS resolver.TUN device + SOCKS5 proxy: All traffic from inside the sandbox is routed through a TUN device into GreyProxy. Every TCP and UDP packet hits the proxy, regardless of whether the process respects
HTTP_PROXYor not.
GreyProxy operates on a default-deny policy. Only domains you've explicitly allowed can be reached, and it has a dashboard from which endpoints can be dynamically allowed. This way, you can have a whitelist of users + endpoints (or pass all of your users through a local proxy), and adjust it on the fly.
And so, the models.litellm.cloud domain would never appear in an allowlist, so the connection is dropped before a single byte leaves the sandbox. Nor would the IMDS metadata endpoint (169.254.169.254), which the malware queries for cloud credentials.
Stage 3: Lateral movement
In the final stage, the malware attempts to infiltrate attempts to spread throughout a cluster. If a Kubernetes service account token is available, the malware reads all cluster secrets, then tries to create privileged pods on every node with the host filesystem mounted. On the local machine, it installs persistence at ~/.config/sysmon/sysmon.py with a systemd user service.
Greywall shuts this down at multiple layers:
The Kubernetes API server (typically at
https://kubernetes.default.svcor an IP on the cluster network) is only reachable if the network allowlist includes it. There is no reason why the API server should be reachable by LiteLLM, and so it would be blocked by default.Reading the token file at
/var/run/secrets/kubernetes.io/serviceaccount/tokenwould be blocked by the filesystem policy, since Greywall's default-deny model doesn't include that path.Setting aside the network block, the malware wants to create pods with
privileged: trueandhostPID: truethat mount/from the host. Inside a Greywall sandbox, the process runs in its own PID, mount, and network namespace (via Bubblewrap). It cannot see host processes, cannot mount host filesystems, and cannot escalate its namespace.The malware tries to write to
~/.config/sysmon/sysmon.pyand~/.config/systemd/user/sysmon.service. Both paths are outside the allowed write set in Greywall's filesystem policy.Seccomp BPF provides a final safety net. Greywall's seccomp profile blocks 27+ syscalls that are commonly used in privilege escalation and container escape:
ptrace,mount,umount2,kexec_load,init_module,finit_module,bpf, and others. Even if the malware found a creative way around the other layers, it cannot mount filesystems, trace other processes, or load kernel modules.
Fork Bomb
But, amusingly, none of this actually happens. We downloaded the malicious payload and ran it inside greywall, and all it did was fork-bomb itself. This bug is also mentioned in the futuresearch article.
The .pth fires on every Python subprocess startup, so subprocess.Popen spawns a child Python process which re-triggers the .pth which spawns another, etc. This continued until the process died with an OOM error.
So while the blast radius is theoretically contained, greywall didn't actually block any requests because the malicious payload fork-bombed itself before it could try anything.
How to run LiteLLM with Greywall
First, install greywall. You can find install instructions in the greywall website or on the project's github page.
Then, run:
greywall -- litellm
And use the default litellm profile, or modify it yourself.
The dashboard, by default, will be visible at localhost:43080. Note that you'll need to add that to your reverse proxy if you're running it in a cloud machine & want to access it locally.
That's it!