这周技术圈里讨论度蹿升最快的关键词,不是某个新模型,而是LiteLLM和供应链攻击放在一起的消息。作为AI应用层的开源网关,LiteLLM被大量团队用来统一转发不同模型服务的请求,可恰恰是这种“统一”属性,让它手里握着大量凭证信息:OpenAI的API Key、云厂商的AK/SK、内部环境的Master Key。一旦这类程序被恶意代码污染,攻击者拿到的不是一台机器的控制权,而是整套模型账密的访问权。这篇文章就给正在跑LiteLLM proxy的团队,梳理一次供应链攻击从投毒到窃取凭证的完整链路,以及中招后该怎么查、平时该怎么防。内容同时适合AI基础设施负责人、后端开发和安全运维人员参考。
1. LiteLLM代理在调用链里的位置,决定了它必然是攻击目标
1.1 一个网关替你的团队守住了多少密钥
用过LiteLLM的人都知道它的方便:团队里不需要每个人都去申请各家模型厂商的Key,只需要在LiteLLM proxy上配好上游模型,大家一起访问一个统一的OpenAI兼容端点就行。这个代理进程通常还会负责负载均衡、成本统计、并发控制,甚至带一个管理界面。
但你有没有算过,这个进程权限范围有多大?
它启动时会读取配置文件和环境变量,里面大概率躺着至少这些内容:
- OpenAI / Azure OpenAI 的API Key
- Anthropic、Google Gemini等多家模型厂商的Key
- LiteLLM自身的master_key,也就是管理界面和API的访问令牌
- 连数据库的字符串,比如用于记录用量和费用的PostgreSQL连接串
- 如果部署在云上,还可能有云厂商角色专用的临时凭证,或者通过环境变量暴露的AK/SK
这些值只要有一个被恶意代码读到,攻击者就能绕过LiteLLM的控制面直接调用你所有的模型额度。更麻烦的是,LiteLLM proxy常被部署成常驻服务,很多人图省事直接用root或用特权容器跑。攻击者一旦能在进程里执行代码,读取宿主机文件、嗅探内存、访问云元数据服务,基本没有任何阻力。
这个集合效应让LiteLLM成了一个“天然的密码保险柜”——而这个保险柜的钥匙,就挂在那些依赖包和启动脚本上。安全团队如果只盯住模型API本身,却忽略了网关这一层,等于把保险柜放在门口又没上报警器。
1.2 为什么供应链攻击者盯上了这类工具
供应链攻击的逻辑和“打单个目标”不太一样。攻击者最关心三件事:覆盖面够不够广、拿到的东西值不值钱、做了手脚之后能不能在短时间内大量复制扩散。
LiteLLM刚好三条全占。
从覆盖面看,它依赖PyPI上的Python包分发,安装路径千奇百怪:有人用pip install直接装,有人写进requirements.txt,有人把它打进Docker镜像,还有人通过运维脚本批量部署到多台机器。只要包源被污染,一次发布可能影响上千家公司的环境。攻击者不需要逐个攻系统,篡改一个上游包就够了。
从价值看,绝大多数AI应用团队用LiteLLM都是把密钥集中在代理层管理。它不像普通工具那样只泄露某一个环境变量,而是能一次性把“和模型服务相关的所有账密”都端走。这种高密度凭证集中点,在攻击者眼里比普通业务服务器有价值得多。
从隐蔽性看,LiteLLM代理对外发起HTTPS请求是正常行为。它每天都和各家模型API保持长连接,攻击者把窃取的凭证混在出网流量里带走,如果团队没有做出口流量审计,基本不会被怀疑。这就解释了为什么针对性供应链投毒会选这种工具而不是某个小众脚本库:收益高、暴露面大、又不容易被发现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一次典型的投毒过程:从恶意发布到成功运行
2.1 软件包分发链路中的薄弱点
LiteLLM的官方包托管在PyPI,但供应链攻击未必只发生在官方仓库。常见的有这么几条渗透路径。
最直接的是账户接管和身份仿冒:维护者账号密码泄露、邮箱失窃,或者通过钓鱼手段拿到PyPI发布权限,然后在一个“看起来完全正常”的版本里插入恶意代码。由于很多团队安装时用的是latest或某个版本范围,攻击者发布的新版本会在下一次容器重建、CI部署、甚至开发者的pip install --upgrade里被自动装上。
还有一种是依赖链污染。LiteLLM本身可能没被改,但它依赖的某个第三方库先被投毒。Python的依赖解析会一路往下安装,你看到pip install litellm只装了一个包,实际上背后可能拉了十几个传递依赖。如果这些依赖里有任何一个被劫持,恶意代码依然能在LiteLLM启动前或启动瞬间被执行。
更隐蔽的是本地镜像和私有源的污染。不少团队为了加速安装,会在内网搭建PyPI镜像。镜像同步机制如果不校验源包签名,攻击者或内网渗透者就可以往缓存里塞一个被篡改的whl文件。这种情况最难发现,因为表面上你用的仍然是正式版本号,但文件内容已经变了。
把这几条路径摆在一起,你会发现问题的核心并不是“LiteLLM有没有漏洞”,而是“你安装LiteLLM的过程是否可信”。只要安装链路上任何一个环节失去完整性,恶意代码就有了站稳脚跟的机会。
2.2 恶意代码的运行时机和持久化方式
恶意代码一旦进入环境,它的启动时机决定了隐蔽程度。Python包的恶意代码主要选在这几个时机执行。
第一种是安装时执行。攻击者可以把代码塞进setup.py或者项目里的自定义构建脚本,pip install真正跑起来的时候,系统就执行了一遍脚本。这个阶段最大的好处是“还没运行业务代码就已经得手了”,而且安装过程通常是自动化执行,不会有人盯着输出日志看。
第二种是导入时执行。Python的模块只要被import,模块顶层代码就会跑。恶意代码可以藏在LiteLLM某个子模块的__init__.py里,只要服务启动时import litellm,它就被激活。对能阅读源码的人而言,这算常规操作;但对大多数只用不读源码的团队来说,几乎没人会注意到。
第三种是运行时挂钩。攻击者可以monkey-patch掉LiteLLM内部的配置加载函数,或者替换httpx、requests的请求函数。Gateway启动时看着一切正常,但当请求到达、代码去读取某个API Key时,恶意逻辑才在幕后把密钥抓走。这种手法更狡诈,因为它依赖的是正常业务路径,检测起来难度最大。
持久化方式也不太一样。在容器环境里,攻击者可能只做“一次性窃取”:进程启动时把密钥发出去,之后什么都不做,容器销毁了痕迹也就没了。在虚拟机或物理机上,攻击者常常会写一个开机自启脚本、往crontab里塞一条定时任务,或者把恶意代码挂到Python的sitecustomize里,保证服务重启后还能再执行一次。
我见过不少团队排查时只盯“有没有多出来的进程”,却忽略了启动持久化文件被改过。这相当于只把冲进门的人赶走,却没堵上他留在门锁上的钥匙复制件。
3. 凭证被窃取的完整链路拆解
3.1 恶意代码如何读取LiteLLM的配置与密钥
要算清楚被盗的影响面,就得先知道攻击者能从哪些位置把凭证弄出来。这里把最常见的几种列个表:
| 凭证类型 | 常见存放位置 | 恶意代码的提取方式 |
|---|---|---|
| 模型厂商API Key | .env、环境变量 | 读取进程环境变量、解析.env文件 |
| LiteLLM master_key | config.yaml、环境变量 | 解析YAML、读取环境变量 |
| 云厂商AK/SK | 环境变量、云元数据 | 读取环境变量或请求169.254.169.254 |
| 数据库连接串 | config.yaml、Secret文件 | 解析配置、直接读文件 |
| 第三方工具Token | 服务启动脚本 | 读取启动参数、shell profile |
LiteLLM读取配置时通常支持两种方式:直接把Key写在config.yaml里,或通过os.environ从环境变量引用。直接写在YAML里的密钥最容易被解析,恶意代码只要用PyYAML把文件读一遍,再遍历常见字段名就能拿到。走环境变量的场景,攻击者也有办法:只要能拿到容器的shell或读取/proc/<pid>/environ,就能看到运行中进程启动时被注入的所有环境变量。
如果LiteLLM跑在k8s里,Secret通常以环境变量或挂载文件的形式进Pod。攻击者拿到Pod内代码执行权限后,读挂载Secret文件和读普通文件没有区别。也就是说,容器逃逸不是必要条件,只要包被投毒,容器内部的一次代码执行就已经足够让凭证出圈了。
更值得警惕的是云元数据服务。很多云厂商的实例在默认网络环境下,可以通过169.254.169.254这个内部地址拿到临时凭证。LiteLLM的容器如果没做网络隔离,恶意代码连AK/SK都不用找,直接请求这个地址就能劫持整个实例的云角色权限。这个细节经常被忽略,但它往往比偷一个OpenAI Key严重得多,因为云凭证可能牵涉对象存储、数据库、生产环境等多套资源。
3.2 数据外传的可疑通道与掩盖手法
拿到凭证只是第一步,攻击者还得把数据送出去。这里有个现实困境:LiteLLM代理本来就要频繁访问外部的模型服务,因此你不能简单地封掉所有出网。
攻击者常用下面几种外传通道来掩盖偷窃行为:
- 直接向攻击者域名发起HTTPS POST请求,数据包很小,不像大文件传输那么扎眼。
- 把数据编码后塞进DNS查询的域名里,很多企业的安全设备对DNS解析内容不敏感。
- 利用现有正常接口“带私货”,比如改成向一个攻击者控制的模型服务端点推数据,流量形态和真实调用几乎没有区别。
- 通过企业协作软件Webhook、网盘上传等方式外带,绕过只针对特定端口的监控。
为了让流量更难被识别,恶意代码通常会把数据做一层编码或加密。常见的是base64、XOR、或者一个简单AES加密,然后拼进一个看似合规的POST body里。你要是只做“抽样看几眼日志”的排查,很难发现异常。
从防御角度看,真正有效的办法是先给LiteLLM代理“画一个正常的出网画像”:它到底应该和哪些域名通信。通常就那几家模型厂商的API域名、你的内部日志系统、以及DNS服务器。这个清单越短越好,任何不在清单里的出网目标都应当触发告警。
4. 怀疑中招后的排查动作和验证方法
4.1 从安装记录和文件哈希回溯攻击入口
如果你已经收到风声,或者发现模型用量暴增、API Key异常被调用,先别急着卸载重装。隐患排查的第一原则是保留现场。
先确认当前LiteLLM的实际版本和安装路径:
bash复制pip show litellm
python - <<'PY'
import litellm, inspect
print(litellm.__version__)
print(inspect.getfile(litellm))
PY
拿到安装路径后,看里面的文件有没有最近被修改过的痕迹:
bash复制find /usr/local/lib/python3.*/site-packages/litellm -type f -name "*.py" -mtime -30
如果发现一堆近期出现的新文件,比如utils.py、telemetry.py、version_check.py之类,要重点看它们内容里有没有可疑的网络请求和加密逻辑。可以用简单的grep先扫一遍:
bash复制grep -rn "requests\|base64\|socket\|subprocess\|eval\|exec" \
/usr/local/lib/python3.*/site-packages/litellm --include="*.py"
这种搜索命中率不低,但也要注意误报,因为LiteLLM本身会用requests调用模型API。真正需要警惕的是那些看起来和业务功能无关、却主动访问外部地址的代码。
更严谨的做法是下载官方源包逐文件比对:
bash复制pip download litellm==<你的版本> --no-deps -d /tmp/verified
然后解压官方whl文件,和你环境里的同名文件用sha256sum做对比。文件数量多,可以用脚本遍历目录,找出所有哈希不一致的文件。这样改了什么一眼就能看出来。
4.2 检测进程行为与出网请求
文件层面没问题,不代表运行层面安全。接下来要把聚焦点放到进程和网络行为上。
先看代理进程有没有异常子进程、有没有开额外的端口:
bash复制ps aux | grep -i litellm
lsof -p <PID> -i
如果怀疑它对外发数据,可以在测试环境里先临时下线业务流量,再用tcpdump抓一段时间的包:
bash复制sudo tcpdump -ni any tcp port 443 -c 2000 -w /tmp/litellm.pcap
或者用strace跟踪进程的网络系统调用,但要注意strace在压力较大的生产环境会拖慢进程,最好先确认清楚再执行:
bash复制sudo strace -p <PID> -f -e trace=network -o /tmp/strace.log
容器化部署的情况,优先查容器内部和外部的差异:
bash复制docker inspect <container>
docker top <container>
docker logs --tail 200 <container>
重点看容器启动时挂载了哪些宿主机目录、进程启动命令里有没有奇怪的参数、日志里有没有向陌生域名发起调用的记录。现在的容器编排平台都会记录镜像层信息,把镜像历史翻出来也能看出依赖是哪一层被引入的。
注意:如果发现异常连接,先不要贸然把进程杀掉。先记录目标IP、域名、证书信息、用户代理字符串,这些都可能成为后续溯源的关键证据。
4.3 确认受影响后的应急处理顺序
万一确认中招,处理顺序决定了损失能不能被拦住。我建议按下面这个顺序来:
- 快照取证。容器、磁盘、进程内存能留就留,至少要dump一份日志和文件哈希清单。
- 立刻轮换所有可能暴露的凭证。不要只换OpenAI Key,所有在LiteLLM环境里出现过的AK/SK、数据库密码、第三方Token全部要换。
- 网络隔离。把受害机器从生产内网摘除,或者直接对出口做限制,让恶意代码无法继续外传。
- 清理持久化。检查crontab、systemd服务、shell profile、sitecustomize.py,把所有启动项里的恶意残留删干净。
- 从干净镜像重建。用锁版本、锁哈希的方式重新搭建服务,而不是“把包重新装一遍”。
- 复盘加固。把这次暴露的入口映射到对应的权限和监控缺口上,再去改流程。
最容易犯的错是“我先卸载重装再看看”。这类处理方式会把已经被篡改的文件覆盖掉,线索全都消失,后面想确认感染范围就只能靠猜了。
5. 这次事件给AI应用团队的三条长期防线
5.1 依赖与镜像的可信管理
经过这样一次折腾,最该改变的其实是安装习惯。别再用pip install litellm或者pip install --upgrade litellm这种方式了。
第一步是把版本固定死,并且生成带哈希的锁文件。用pip-tools、poetry或uv都能生成包含依赖哈希的锁定结果。Dockerfile里安装时,可以配合--require-hashes参数让pip在安装前校验所有依赖的哈希值。这样哪怕某个上游发布了被篡改的版本,只要哈希对不上,pip就会直接拒绝安装。
bash复制pip install --require-hashes -r requirements.txt
Docker镜像方面,FROM不再只写python:3.11-slim这种tag,要尽量用带digest的形式锁死基础镜像。构建时也尽量只装运行时必要的依赖,不要用pip install把所有开发包都带进去。条件允许的话,在CI里加pip-audit或osv-scanner,扫描已知漏洞和异常依赖。
很多团队觉得这些操作“影响效率”,但真出事之后的排查成本和停机成本,比建设这几道门槛高一个数量级。
5.2 凭据的最小化和动态化
依赖链防御只能降低恶意代码进入的概率,真正限制损失的是凭据本身的设计。
首先做到“一套密钥只服务一个环境”。开发环境、测试环境、生产环境的模型API Key必须分开。即使攻击者拿到开发环境里的Key,也不能直接刷爆生产额度。同理,LiteLLM的master_key不要全员共用,按团队成员或调用方角色分别签发,用完可以单独吊销。
其次,能不用长期静态密钥就别用。在云环境里,优先给LiteLLM所在的Pod绑定实例角色,通过云平台的元数据服务获取临时凭证,而不是把AK/SK写进.env。对外部模型服务,如果厂商支持短期Token,就设置较短的有效期;不支持的话,至少要把轮换周期压缩到每周或每月,并保留自动轮换脚本。
还有一个容易被忽略的点:不要让代理容器默认能访问云元数据地址。很多云平台提供metadata service的禁用手册,或可以通过安全组对169.254.169.254做访问控制。把这条掐掉之后,即使容器被投毒,攻击者也拿不到云角色凭证,只能去翻静态Key,影响面会小很多。
5.3 代理层的审计与告警
最后一道防线是把“看不见的问题”变成“可观测的问题”。LiteLLM的日志本来就有很多信息:谁调用了哪个模型、消耗了多少token、用了哪个Key。把日志接入统一日志平台并不难,难的是设置针对性的告警规则。
我建议至少盯这几类指标:
- 同一个API Key在短时间内被多个IP调用,这是被盗Key被分发的典型信号。
- 模型用量或费用突然飙升,尤其是空闲时段出现大量请求。
- 代理进程主动连接了不在白名单里的目标域名。
- 管理接口的master_key出现登录失败或瞬时大量访问。
在出网方向,给代理进程加一层白名单。LiteLLM的服务器实际需要通信的外部域名数量有限,把模型厂商的API域名、内部日志端点、DNS服务器列出来,其余全部默认拒绝。这条规则看起来简单,却能直接掐断恶意数据的走私通道。
再把LiteLLM自身放在一个独立的网络环境里,和办公网、其他开发环境做隔离。它只负责转发模型请求,不该有权限访问公司的数据库、文件服务器等内部资源。网络隔离做不到绝对安全,但能把一次供应链攻击的横向移动范围压到最小。
我自己在内部网关上的习惯是:每季度至少做一次依赖审计和密钥轮换,每次升级LiteLLM之前先用隔离环境跑一天流量,再正式上线。这个流程看着繁琐,累计下来能挡掉的潜在风险却不少。AI基础设施的信任链路,从来不是靠某一个环节的强安全能力撑住的,而是靠每一层都留一点约束,让攻击者在任何一个节点上动手都显得代价过大。
