跳到主内容
快讯直播
AI智模界
教程

LiteLLM 投毒应急:排查、凭据轮换与版本回滚

自建 AI 网关的好处是所有模型调用都集中在一个进程里,坏处也一样——这个进程能看见你所有的上游密钥。所以当 PyPI 上某个刚发布的 LiteLLM 版本被曝出植入凭据窃取后门时,它就不是“某个依赖出了点问题”,而是一次全量密钥泄露事件。

下面这套流程以 LiteLLM 1.107.0 被植入后门这一情形为例,但里面的命令和思路对任何一次 Python 供应链投毒都通用。

报错现象

往往先出现的不是报错,而是告警。常见的几种现场:

1. 依赖侧告警。 安全平台或 PyPI 侧通知某个版本被标记为恶意、已 yank;CI 里的依赖扫描直接失败。如果你已经加了哈希锁定,安装时会看到:

```

ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE.

```

这条的意思是:你下载到的文件和锁文件里记录的 sha256 对不上。要么源被换了,要么包内容变了。

2. 上游厂商的用量告警。 OpenAI / Anthropic / Azure OpenAI 控制台里出现你没有发起过的调用,某个 key 的消费曲线突然抬头,或者收到额度异常邮件。

3. 网关容器出站异常。 docker logs 里多出无关的 traceback,主机连接表里出现指向陌生域名的 443 长连接,且不是任何一家模型厂商的域名。

4. 启动行为变化。 升级到该版本后启动变慢,pip check 报依赖冲突,或者容器启动时有几秒钟的空白期——那可能是在做网络请求。

影响范围要判断清楚:只要这个进程能读到的环境变量、能读到的配置文件、能连到的数据库,都算潜在泄露面。典型自建网关里同时存在:多个上游模型 key、master key、数据库连接串、Redis 密码、可观测平台的 key。清单比你想的长。

可能原因

按出现概率从高到低:

1. 直接装了被投毒的发布版本。 出现频率较高。pip install litellm 或 pip install "litellm[proxy]" 没锁版本,Dockerfile 里写了 RUN pip install -U litellm,构建那一刻拉到了有问题的版本。

2. 间接引入。 你的依赖树里某个包依赖了 LiteLLM,或者基础镜像里预装了它。你锁了自己的直接依赖,却没锁传递依赖。

3. 镜像 tag 被覆盖。 用的是 latest 或浮动 tag,CI 重新构建时底层内容换了,docker history 看不出来。

4. 并不是投毒,是网关被打穿。 master key 泄露、admin UI 暴露公网、/v1/* 没鉴权,攻击者直接拿你的网关刷模型。这种情况的账单异常和后门非常像。

5. 密钥以别的方式泄露。 .env 被 COPY . . 打进镜像层、日志里打印了 key、Git 历史里有明文。这类问题常被误判成“后门干的”。

6. 误报。 正常的版本检查、telemetry、健康探针被判成恶意外连。概率不高,但不能先自我安慰——先按真事处理。

逐条排查与解决

排查一:确认实际跑的到底是哪个版本

```bash

容器内

docker exec -it <container> pip show litellm

docker exec -it <container> python -c "import litellm, os; print(getattr(litellm,'__version__','?'), os.path.dirname(litellm.__file__))"

不启动进程,直接查镜像

docker run --rm --entrypoint pip <image> show litellm

docker run --rm --entrypoint pip <image> freeze | grep -i litellm

```

判断依据:pip show 的版本和 lock 文件里的版本是否一致。不一致,就是原因 1 或 2;一致但正好是问题版本,那就是命中了。记下 Location 字段,后面取证要用。

排查二:看包内容有没有被动过

```bash

SITE=$(docker exec <container> python -c "import litellm,os;print(os.path.dirname(litellm.__file__))")

docker exec <container> ls -la "$SITE"

docker exec <container> sh -c "grep -rnE 'subprocess|os\.system|eval\(|exec\(|b64decode|requests\.post' $SITE | head -40"

```

正常的库也会有 subprocess 之类的命中(比如调用本地 CLI),所以看上下文:如果入口文件里出现向硬编码域名 POST、启动时读 ~/.aws/credentials、或者把整个 os.environ 打包发出去,基本可以定性。

再看网络:

```bash

docker exec <container> cat /proc/net/tcp | head

宿主机侧更直观

ss -tunp | grep -i python

lsof -i -P -n -p $(pgrep -f litellm | head -1)

```

持续重连的外部 443 就值得追:

```bash

tcpdump -i any -n 'tcp port 53 or tcp port 443' -c 200

```

排查三:确认密钥有没有被用过

到每个上游厂商控制台看账单、看 API key 的最近调用时间与来源 IP。这一步比在服务器上翻文件有用——后门不一定留痕,但调用一定留记录。

同时在网关侧列出“有哪些密钥进过这个环境”,这就是轮换范围:

```bash

docker exec <container> env | grep -iE 'key|token|secret|password' | sed 's/=.*/=<redacted>/'

```

注意别把带值的输出贴进工单。

处置一:先隔离,再动手

顺序很重要:取证 → 摘流量 → 重建。发现问题后直接 docker compose down,现场就没了。

```bash

1. 摘流量

kubectl scale deploy/litellm --replicas=0

或

docker compose stop litellm

2. 留证

docker commit <container> litellm-forensic:$(date +%Y%m%d)

docker logs <container> > litellm.log 2>&1

docker exec <container> tar czf - "$SITE" > litellm-site-packages.tgz

```

跑在 K8s 上的,kubectl get pod -o yaml 存一份,外发前先把 env 里的值抹掉。

处置二:回滚到已知安全版本

不要在原有环境里 pip install 降级——被投毒的进程已经跑过,容器文件系统不可信。用干净的构建重来:

```dockerfile

版本以官方 release 页面当前版本为准,这里钉一个内部验证过的版本

RUN pip install --no-cache-dir --require-hashes -r /tmp/requirements.lock

```

生成带哈希的锁文件:

```bash

pip-compile --generate-hashes -o requirements.lock requirements.in

pip install --require-hashes -r requirements.lock

```

用 uv 的话:

```bash

uv lock

uv sync --frozen

```

关键在 --require-hashes:它保证装到的文件和锁文件记录的 sha256 完全一致,源被替换、包被重传都会直接失败。

镜像侧同时把基础镜像钉到 digest:

```dockerfile

FROM python:<version>-slim@sha256:<digest>

```

重建加 --pull --no-cache,避免复用本地旧层。

处置三:凭据轮换清单

划范围的标准是“这个秘密在网关环境里出现过吗”,宁可多换。顺序是:先建新、再切流、后吊销。

类别具体项备注
上游模型OpenAI / Anthropic / Azure OpenAI / 自建推理服务的 token逐个建新 key,顺手设预算上限
网关自身LITELLM_MASTER_KEY、UI 账号密码master key 泄露等于网关全开
加密盐LITELLM_SALT_KEY用于加密数据库里存储的上游 key,更换后旧密文无法解密,需重新录入,操作前看官方文档当前版本说明
数据层Postgres 密码、Redis 密码连接串里出现的都算
云与编排云厂商 AK/SK、K8s ServiceAccount token有 Workload Identity 就轮换角色信任关系
可观测Langfuse / OTel 后端 / webhook secret常被忽略,但往往带写权限

生成随机值:

```bash

openssl rand -hex 32

```

轮换后确认一遍:

```bash

kubectl rollout restart deploy/litellm

kubectl exec deploy/litellm -- env | grep -c 'sk-'

kubectl exec deploy/litellm -- sh -c 'grep -rl "sk-" /app 2>/dev/null' # 期望输出为空

```

旧 key 不要立刻删,先观察 24 小时调用量是否归零,再吊销。归零之前别宣布处置结束。

都不管用时的兜底方案

如果重建之后仍有异常外连,或者你没法确认泄露面,按保守假设处理:

1. 换环境重建。 新命名空间、新主机、全新密钥,从 IaC 重新拉起。旧环境整体保留做取证,不再接生产流量。

2. 出站默认拒绝。 网关只需要访问少数几个上游域名,其余全部拒掉。示例思路(具体写法按你的 CNI 或安全组来):

```bash

iptables -A OUTPUT -p tcp --dport 443 -d <上游地址段> -j ACCEPT

iptables -A OUTPUT -p tcp --dport 443 -j LOG --log-prefix "EGRESS-DENY: "

iptables -A OUTPUT -p tcp --dport 443 -j REJECT

```

3. 业务侧降级。 临时摘掉网关,业务代码直连各家 SDK,先保可用性,再慢慢修网关。

4. 请安全团队做主机取证。 保留镜像、内存、日志、抓包,别自己清理文件。

5. 通知相关方。 上游厂商可以协助查看 key 的滥用情况、冻结额度;如果涉及的密钥关联第三方数据,按公司合规流程上报。

如何预防再次发生

  • 锁死依赖。 提交 requirements.lock / uv.lock / poetry.lock,CI 里只允许 --require-hashes 或 --frozen 安装,绕过 lock 的安装命令直接让流水线失败。
  • 走内部源。 用 devpi / Nexus / Artifactory 做 PyPI 代理,只允许从内部源装包,外部包进内部源前过一遍扫描。上游被投毒时,这是一层缓冲。
  • 镜像不用浮动 tag。 latest、1.x 这类 tag 不进生产,用 digest 固定;构建加 --pull --no-cache。
  • 依赖升级走 PR。 版本变更产生 diff,人工看一眼 changelog 再合。自动升级很好用,但它也会自动帮你把后门装上。
  • CI 里跑审计。 pip-audit、safety、SBOM 生成加漏洞库比对,失败即阻断。同时订阅 PyPI 的 yank 通知和上游项目的安全公告。
  • 密钥按需授权。 每个上游 key 只开需要的模型、设预算上限、设到期时间。这样即使泄露,损失也有上限。
  • 网关不裸露。 admin UI 和 /v1/* 走内网或 VPN,master key 只发给确实需要它的服务;虚拟 key 按团队分发,能单独吊销。
  • 出站白名单。 生产网关只放行上游域名,DNS 查询异常也纳入告警。
  • 留好基线和日志。 记录“正常情况下的出站域名清单”和“正常用量曲线”,异常才有对照物。

供应链投毒的特点是,它不需要攻破你的任何一台机器,只要你在合适的时间执行了一次 pip install。所以防线的重心不在防火墙,而在“你装的东西是不是你以为什么”——版本、哈希、来源,这三件事守住,大部分事故就挡在门外了。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。