从 LiteLLM 供应链投毒事件聊聊密钥安全

Posted on 六 28 3月 2026 in Tech

Abstract 从 LiteLLM 供应链投毒事件聊聊密钥安全
Authors Walter Fan
Category Security
Status v1.0
Updated 2026-03-28
License CC-BY-NC-ND 4.0

你的密钥,可能正在裸奔

咱们做后端开发的,手里多多少少都攥着几把密钥——OpenAI 的 API Key、AWS 的 Access Key、数据库密码……平时往环境变量里一塞,.env 文件一放,感觉挺安全。可你有没有想过,有一天你 pip install 的那个包,把这些密钥全偷走了?

2026 年 3 月,LiteLLM 就出了这么一档子事。LiteLLM 是个很流行的大模型代理网关,帮你统一调用 OpenAI、Claude、Gemini 等各家 API。结果它的两个 PyPI 版本被人植入了恶意代码,你的密钥、SSH 私钥、云凭证,一股脑被打包加密发到攻击者的服务器上。

这事儿细想挺后怕的,咱们来扒一扒。

事件还原:两个版本,一场精准偷袭

根据 LiteLLM 官方披露,事件经过大致是这样的:

攻击者疑似通过窃取维护者的 PyPI 账号凭证,绕过了项目正常的 CI/CD 发布流程,直接往 PyPI 上传了两个含有恶意代码的版本:

  • v1.82.7:在 proxy_server.py 中注入了恶意 payload
  • v1.82.8:除了 proxy_server.py 中的 payload,还多带了一个 litellm_init.pth 文件

这两个版本干的事情简单粗暴——收集宿主机上能找到的一切敏感信息:

  • 环境变量(API Key、数据库密码、各种 Token 都在这里)
  • SSH 密钥
  • AWS、GCP、Azure 的云凭证
  • Kubernetes 的 ServiceAccount Token
  • 数据库连接串和密码

收集完毕,加密后通过 POST 请求发到 models.litellm.cloud——这个域名看着像官方的,其实是攻击者注册的。

LiteLLM 团队发现后紧急下架了这两个版本,轮换了维护者凭证,还请了 Google Mandiant 做取证分析。受影响的用户被建议立即轮换所有密钥。

.pth 文件:Python 启动时的"隐秘后门"

这次事件里,v1.82.8 带的那个 litellm_init.pth 文件值得单独说说,因为它利用了 Python 一个很多人不知道的机制。

.pth 文件是什么?

Python 的 site 模块在启动时会自动扫描 site-packages 目录下的 .pth 文件。这个文件的本意是让第三方包声明额外的模块搜索路径,每行一个路径,加到 sys.path 里。

可是,Python 官方文档里有这么一条规则:

Lines starting with import (followed by space or tab) are executed.

也就是说,.pth 文件中以 import 开头的行,会在每次 Python 启动时自动执行。不需要你显式 import 任何东西,只要 Python 进程一起来,这行代码就跑了。

攻击者怎么利用的?

想象一下这个场景:你 pip install litellm==1.82.8,一个叫 litellm_init.pth 的文件安静地躺进了你的 site-packages。从此以后,你机器上每一次 Python 进程启动——不管是跑 Django、Flask、还是一个简单的脚本——这个 .pth 文件里的恶意代码都会执行。

这比在 proxy_server.py 里塞代码更狡猾:

  1. 持久化:即使你卸载了 litellm,.pth 文件可能残留在 site-packages
  2. 隐蔽性:大多数人不会去翻 site-packages 下面有哪些 .pth 文件
  3. 广撒网:不限于 litellm 自己的进程,所有 Python 进程都中招

官方文档说这个特性本意是"keep impact to a minimum",可一旦被恶意利用,杀伤力可不小。

怎么排查?

如果你曾经安装过 LiteLLM,赶紧检查一下:

# 搜索 site-packages 中的可疑 .pth 文件
find $(python -c "import site; print(site.getsitepackages()[0])") \
  -name "*.pth" -exec grep -l "import" {} \;

# 直接搜 litellm 相关的 .pth
find / -name "litellm_init.pth" 2>/dev/null

漏洞根因:三层防线全部失守

这次事件不是某一个环节出了问题,而是三层防线依次崩塌:

第一层:维护者凭证泄露

攻击者拿到了 PyPI 维护者的账号权限。怎么拿到的?目前调查指向 Trivy(一个安全扫描工具)相关的凭证泄露链路。

这层失守暴露的问题是:长期有效的静态凭证是定时炸弹。一个 PyPI API Token 可能几年不换,一旦泄露,攻击者就能随时发包。

第二层:CI/CD 流水线被绕过

正常情况下,LiteLLM 的发布应该走 CI/CD 流水线——代码审查、测试、自动构建、发布。可攻击者直接用窃取的凭证往 PyPI 上传,完全跳过了这些环节。

问题在于 PyPI 不知道这个包是不是从合法的 CI/CD 流水线里出来的。它只认 Token,不认来源。

第三层:运行时密钥明文暴露

即使前两层被突破,如果用户的密钥不是明文存在环境变量或磁盘上,攻击者拿到的也不过是一堆密文或者短期 Token。可现实是,绝大多数人的 API Key 就这么明晃晃地放在 .env 文件或者环境变量里,一偷一个准。

三层全破,就好比城门没关、城墙塌了、金库大门还敞着。

前车之鉴:密钥泄露从来不是新鲜事

LiteLLM 这事儿并非孤例,密钥泄露引发的安全事故在业界屡见不鲜,聊两个经典的。

Uber 2016:一把 AWS Key 卖了 5700 万用户的数据

2016 年,Uber 的两名开发者把 AWS 的 Access Key 提交到了一个私有 GitHub 仓库。攻击者扫描 GitHub 发现了这把钥匙,顺藤摸瓜拿下了 Uber 存放在 S3 上的用户数据——5700 万乘客和司机的姓名、邮箱、手机号,还有 60 万司机的驾照号码。

更离谱的是,Uber 发现后没有公开披露,而是付了 10 万美金给攻击者让他们删数据、签保密协议,试图把这事儿捂住。后来东窗事发,Uber 的首席安全官因此被刑事起诉,公司也付出了上亿美元的和解金。

一把写死在代码里的 AWS Key,代价是几个亿。

Codecov 2021:和 LiteLLM 如出一辙的供应链投毒

2021 年的 Codecov 事件和这次 LiteLLM 几乎是同一个剧本。Codecov 是一个很流行的代码覆盖率工具,攻击者篡改了它的 Bash Uploader 脚本,在里面加了一行:把 CI 环境中的所有环境变量(包括各种密钥、Token)发送到攻击者的服务器。

受影响的公司包括 Twitch、HashiCorp、Confluent 等一大批科技公司——它们的 CI/CD 流水线每次跑测试都会调用这个脚本,密钥就这样源源不断地"上贡"了两个多月才被发现。HashiCorp 的 GPG 签名私钥都因此泄露,不得不紧急轮换。

这两个案例的教训很清楚:密钥一旦以明文形式存在于代码、配置文件或环境变量中,就只差一个泄露渠道——可能是 GitHub 的一次误提交,可能是供应链里一个被篡改的脚本,也可能是一个被攻破的 .pth 文件。

密钥不落盘:不是洁癖,是生存策略

"密钥不落盘"这个理念说起来简单——密钥不以明文形式持久化存储在磁盘上。可为什么这么重要?

环境变量也不安全

很多人觉得密钥放环境变量比放配置文件安全。其实不然:

  • /proc/<pid>/environ 可以读到进程的环境变量(Linux)
  • 任何能执行 os.environ 的恶意代码都能拿到
  • Docker 镜像的 inspect 信息里可能包含构建时的环境变量
  • 日志和错误追踪系统经常不小心把环境变量打出来

这次 LiteLLM 事件里,恶意代码第一件事就是 dump 环境变量,简单高效。

"不落盘"的核心思路

密钥的理想生命周期应该是这样的:

应用启动 → 从密钥管理服务动态获取 → 存在进程内存中 → 用完/过期即销毁 → 需要时重新获取

几个关键原则:

  1. 密钥只在内存中短暂存在,不写入文件、不写入环境变量、不写入日志
  2. 密钥有过期时间,即使泄露,窗口期有限
  3. 获取密钥需要身份认证,而这个身份认证本身基于短期凭证或硬件绑定
  4. 审计一切访问,谁在什么时候拿了什么密钥,全有记录

业界通用方案一览

密钥安全和软件供应链安全是两个相关但不同的战场,业界在两个方向上都有成熟方案。

密钥管理:让密钥"活"起来

方案 核心思路 适用场景
HashiCorp Vault 集中式密钥管理,动态密钥生成,自动轮换,细粒度访问控制 自建基础设施,混合云
AWS Secrets Manager 与 IAM 集成,支持自动轮换,按次计费 AWS 生态
GCP Secret Manager 与 IAM 和 Workload Identity 集成 GCP 生态
Azure Key Vault HSM 支持,与 Managed Identity 集成 Azure 生态
CyberArk / Conjur 企业级特权访问管理,适合合规要求高的场景 金融、医疗等强监管行业

以 Vault 为例,一个典型的使用方式:

import hvac

client = hvac.Client(url='https://vault.internal:8200')
# 用 Kubernetes ServiceAccount 自动认证,无需存储长期凭证
client.auth.kubernetes.login(role='my-app', jwt=sa_token)

# 动态获取数据库凭证(Vault 自动创建临时账号,到期自动回收)
creds = client.secrets.database.generate_credentials(name='my-db-role')
db_user = creds['data']['username']
db_pass = creds['data']['password']
# 这组凭证 1 小时后自动过期

这样一来,即使恶意代码 dump 了你的内存,拿到的也只是一组即将过期的临时凭证,而不是一把"万能钥匙"。

供应链安全:让发布过程可验证

方案 解决什么问题
PyPI Trusted Publishers (OIDC) 用 GitHub/GitLab 的 OIDC Token 替代长期 API Token 发布包,Token 短期有效且绑定特定仓库和工作流
SLSA 框架 定义软件供应链完整性的等级标准(L1-L4),从"有构建记录"到"完全可重现构建"
Sigstore / cosign 对构建产物做数字签名,消费者可以验证包确实来自官方 CI/CD
依赖锁定 + Hash 校验 pip freeze + --require-hashes,确保安装的就是你审计过的那个版本
Software Bill of Materials (SBOM) 列出所有依赖的清单,方便在漏洞披露时快速排查影响范围

如果 LiteLLM 用了 Trusted Publishers,攻击者即使拿到维护者的 PyPI 密码,也无法从自己的机器上发包——因为 OIDC Token 只能从指定的 GitHub Actions 工作流中签发。

个人电脑:开发者日常怎么管密钥

很多人觉得密钥管理是运维和服务器的事,自己笔记本上随便放放没关系。可 Uber 那个案例就是开发者本地的 Key 不小心推到了 GitHub。个人电脑上有三招实用的,一招比一招省心。

第一招:用系统钥匙串存密钥(零成本)

macOS 自带 Keychain,Linux 有 GNOME Keyring,Windows 有 Credential Manager。这些系统级钥匙串把密钥加密存在操作系统里,应用通过 API 按需读取,不需要你自己管加解密。

以 macOS 为例,把一个 API Key 存进 Keychain:

# 存入密钥(会弹出 Keychain 密码确认)
security add-generic-password \
  -a "my-project" \
  -s "OPENAI_API_KEY" \
  -w "sk-xxxxxxxxxxxxxxxxxxxxxxxx"

# 读取密钥(脚本里用这个)
security find-generic-password \
  -a "my-project" \
  -s "OPENAI_API_KEY" \
  -w

在你的 Python 脚本里这么调用:

import subprocess

def get_secret(service: str, account: str = "my-project") -> str:
    """从 macOS Keychain 读取密钥,不落盘"""
    result = subprocess.run(
        ["security", "find-generic-password", "-a", account, "-s", service, "-w"],
        capture_output=True, text=True, check=True
    )
    return result.stdout.strip()

# 用的时候
openai_key = get_secret("OPENAI_API_KEY")

Linux 上用 secret-tool(GNOME Keyring 的命令行工具),思路一样:

# 存入
echo -n "sk-xxxxxxxx" | secret-tool store --label="OpenAI Key" project my-project key OPENAI_API_KEY

# 读取
secret-tool lookup project my-project key OPENAI_API_KEY

优点是零额外安装,操作系统帮你管加密和访问控制。缺点是不能跨机器同步,换台电脑得重新存一遍。

第二招:用 1Password / Bitwarden CLI 管密钥(推荐)

如果你已经在用密码管理器(很多开发者都在用),那它的命令行工具是管理 API Key 最顺手的方式。以 1Password 为例:

# 1. 安装 CLI
brew install 1password-cli

# 2. 登录(只需一次,之后用 Touch ID 解锁)
op signin

# 3. 把 API Key 存进 1Password
op item create --category=api_credential \
  --title="OpenAI API Key" \
  --vault="Development" \
  "credential=sk-xxxxxxxxxxxxxxxxxxxxxxxx"

# 4. 在脚本里读取(不落盘)
export OPENAI_API_KEY=$(op read "op://Development/OpenAI API Key/credential")

1Password 有个杀手级功能叫 op run,它能自动把 .env 文件里的引用替换成真实密钥,注入到子进程的环境变量里,进程退出后密钥就消失了

# .env 文件里这么写(注意:存的是引用,不是真实密钥)
OPENAI_API_KEY="op://Development/OpenAI API Key/credential"
AWS_ACCESS_KEY_ID="op://Development/AWS/access key id"
AWS_SECRET_ACCESS_KEY="op://Development/AWS/secret access key"

# 用 op run 启动你的应用
op run --env-file=.env -- python my_app.py

这个 .env 文件可以放心提交到 Git——里面只有引用路径,没有真实密钥。op run 在启动子进程时实时从 1Password 解密注入,进程结束后环境变量自动清除。

Bitwarden 用户可以用 bw CLI,思路类似:

# 安装
brew install bitwarden-cli

# 登录并解锁
bw login && export BW_SESSION=$(bw unlock --raw)

# 读取密钥
export OPENAI_API_KEY=$(bw get password "OpenAI API Key")

第三招:Git 提交前拦截密钥泄露(兜底防线)

前两招解决的是"怎么存",这一招解决的是"万一手滑了怎么办"——在 git commit 之前自动扫描,发现密钥立刻拦截。

# 1. 安装 gitleaks
brew install gitleaks

# 2. 手动扫一下当前仓库(先看看有没有历史泄露)
gitleaks detect --source . -v

# 3. 配置 pre-commit hook,以后每次提交自动扫
pip install pre-commit

在项目根目录创建 .pre-commit-config.yaml

repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

然后激活:

pre-commit install

从此每次 git commit,gitleaks 会自动扫描你要提交的文件。如果发现疑似密钥(API Key、私钥、Token 等),直接拦截提交并告诉你在哪一行:

Finding:     OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
Secret:      sk-xxxxxxxxxxxxxxxxxxxxxxxx
RuleID:      generic-api-key
File:        config/settings.py:23

另外,在 .gitignore 里把常见的敏感文件类型都加上,多一层保险:

# 密钥和证书
.env
.env.*
*.pem
*.key
*.p12
*_rsa
*.gpg
credentials.json
service-account*.json

三招配合使用:钥匙串/密码管理器负责"安全存",op run 负责"安全用",gitleaks 负责"兜底拦"——个人电脑上的密钥管理就齐活了。

个人开发服务器:SOPS + age 三件套

个人电脑上的方案解决了"本地开发不泄露"的问题,可如果你有一台个人开发服务器或者 side project 的 VPS 呢?上 Vault 那套明显杀鸡用牛刀,维护成本比项目本身还高。这种场景,推荐一个简单实用的组合:SOPS + age + direnv

核心思路:密钥加密存储在 Git 仓库里,用的时候解密到内存,不留明文文件。

  • SOPS(Mozilla 出品):加密/解密配置文件,支持 YAML、JSON、.env 格式。它只加密 value 不加密 key,git diff 的时候能清楚看到改了哪个字段,对版本管理很友好
  • age:现代加密工具,比 GPG 简单太多——一条命令生成密钥对,没有 GPG 那套让人头大的信任链

基本用法:

# 1. 安装
brew install sops age

# 2. 生成 age 密钥(只需一次)
age-keygen -o ~/.config/sops/age/keys.txt

# 3. 加密你的 .env 文件
sops --encrypt --age $(age-keygen -y ~/.config/sops/age/keys.txt) \
  .env.plaintext > .env.encrypted

# 4. 运行时解密(不落盘,直接注入环境变量)
export $(sops --decrypt .env.encrypted | xargs)

日常使用的几个习惯:

做法 说明
.env.encrypted 提交到 Git 加密的,安全
.env / .env.plaintext 加入 .gitignore 明文永远不入库
age 私钥备份到密码管理器(1Password / Bitwarden) 丢了就解密不了了
用 direnv + sops 自动加载 进目录自动解密到环境变量,离开自动清除

再进一步,在项目目录放一个 .envrc,配合 direnv 实现全自动:

# .envrc
export $(sops --decrypt .env.encrypted | xargs)

进入目录,direnv 自动执行解密,密钥注入当前 shell 的环境变量;离开目录,direnv 自动清除——密钥只活在当前 shell 的内存里,不留痕迹。

跟其他方案比一比:

方案 评价
直接放 .env 裸奔,不推荐
git-crypt 只能整文件加密,diff 不友好
Vault 个人服务器杀鸡用牛刀,维护成本高
云厂商 Secrets Manager 要花钱,个人项目没必要
SOPS + age 免费、轻量、Git 友好,够用

一句话总结:SOPS 管加密,age 管密钥,direnv 管自动加载——三件套配齐,个人服务器的密钥管理就够体面了。

服务器 / 生产环境:密钥的"正规军"打法

生产环境的密钥管理要正规得多,核心思路是身份驱动、动态签发、最小权限

1. 用 Workload Identity 替代静态密钥

在 Kubernetes 上,Pod 通过 ServiceAccount 绑定云平台的 IAM 角色,直接获取临时凭证,全程不需要 Secret 对象或环境变量:

# Kubernetes ServiceAccount 绑定 AWS IAM Role
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/my-app-role

应用代码里直接用 AWS SDK,它会自动通过 IRSA(IAM Roles for Service Accounts)拿到临时凭证,不需要配任何 Access Key。GCP 的 Workload Identity 和 Azure 的 Managed Identity 也是同样的思路。

2. Sidecar 模式注入密钥

Vault Agent 作为 sidecar 容器运行,自动认证、获取密钥、写入共享内存卷(tmpfs),主容器从内存卷读取。密钥永远不碰磁盘:

# Vault Agent sidecar 注解
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-inject-secret-db: "database/creds/my-role"
vault.hashicorp.com/secret-volume-path-db: "/vault/secrets"
# 挂载到 tmpfs,重启即消失

3. Sealed Secrets / External Secrets Operator

如果团队习惯把配置都放在 Git 里(GitOps),可以用 Bitnami Sealed Secrets 把密钥加密后提交,只有集群内的控制器能解密;或者用 External Secrets Operator 从 Vault / 云 Secrets Manager 同步,Git 里只存引用,不存值。

4. 短期证书替代长期密钥

对于服务间通信(mTLS),用 cert-manager 或 Vault PKI 自动签发短期证书(比如 24 小时有效),到期自动轮换。比起一张用三年的自签证书,攻击窗口缩小了几个数量级。

把个人电脑和服务器的方案放在一起看,核心逻辑是一样的:密钥的生命周期越短、获取路径越动态、存储形式越远离明文,安全性就越高。

给开发者的 CheckList

结合这次事件,以下是一份可以直接落地的防护清单:

密钥管理

  • [ ] 生产环境的密钥全部迁移到密钥管理服务(Vault / 云厂商 Secrets Manager),不在环境变量和配置文件中存储明文密钥
  • [ ] 为密钥设置最短可用的过期时间,能用动态凭证的地方不用静态凭证
  • [ ] CI/CD 中的密钥用 OIDC 或 Workload Identity 获取,不用长期 Token

供应链防护

  • [ ] 锁定依赖版本,使用 --require-hashes 校验包的完整性
  • [ ] 定期审计依赖,用 pip-auditsafetytrivy(对,就是那个 Trivy)扫描已知漏洞
  • [ ] 对关键依赖订阅安全通告,发现问题版本第一时间降级或替换
  • [ ] 发布自己的包时启用 Trusted Publishers,告别长期 API Token

运行时防护

  • [ ] 定期扫描 site-packages 中的 .pth 文件,发现异常立即排查
  • [ ] 容器镜像用最小化基础镜像,减少攻击面
  • [ ] 网络层面限制出站流量,恶意外发到未知域名可以被防火墙拦截

写在最后

这次 LiteLLM 事件给咱们提了个醒:AI 时代,大家忙着接 API、调模型、搭 Agent,密钥管理这事儿往往被丢到优先级最低的角落。可偏偏是这些"不紧急"的事,出了问题就是大事。

古人说,"千里之堤,毁于蚁穴"。一个 .pth 文件,几行代码,就能把你积攒的所有密钥一锅端。安全无小事,在 AI 基础设施日益复杂的今天,密钥安全是咱们每个开发者都绕不开的必修课。

与其亡羊补牢,不如现在就动手——打开你的项目,grep 一下 .env 文件里有多少明文密钥,然后认真考虑把它们搬进 Vault 或者 Secrets Manager。

共勉。

@startmindmap
* LiteLLM 供应链投毒事件
** 攻击链
*** 维护者凭证泄露
*** 绕过 CI/CD 直接发包
*** 恶意代码窃取密钥
*** .pth 文件持久化
** 根因
*** 静态长期凭证
*** 发布流程无签名校验
*** 密钥明文落盘
** 防护方案
*** 密钥管理
**** Vault / Secrets Manager
**** 动态凭证 + 自动轮换
**** OIDC / Workload Identity
*** 供应链安全
**** Trusted Publishers
**** SLSA 框架
**** Sigstore 签名
**** 依赖锁定 + Hash 校验
*** 运行时防护
**** .pth 文件扫描
**** 最小化容器镜像
**** 出站流量管控
@endmindmap

LiteLLM 供应链投毒事件思维导图