如果它像 Package Manager 那样呱呱叫
Source: Hacker News
我花了很多时间研究包管理器,久而久之,你会对那些像包管理器一样叫的东西产生直觉。许多工具都有注册表、版本锁定、会代表你下载并执行的代码。但单纯的可安装列表并不太有趣。
真正吸引我注意的叫声是当某个东西形成了依赖图时:你的包依赖于一个包,而那个包又依赖于另一个包,这时你需要解析算法、锁文件、完整性校验,以及一种方式来回答“我到底在运行什么,它是怎么来到这里的?”
一些最初作为插件系统、CI 运行器和 chart‑模板工具的工具,悄悄地发展出了传递依赖树。现在它们走路像包管理器,叫声像包管理器,并且拥有 npm、Cargo 和 Bundler 多年学习管理的所有问题——尽管它们大多数还没有跟上相应的解决方案。
Source: https://nesbitt.io/2025/12/06/github-actions-package-manager
GitHub Actions
| 特性 | 状态 |
|---|---|
| Registry | GitHub 仓库 |
| Lockfile | 否 |
| Integrity hashes | 否 |
| Resolution algorithm | 递归下载,无约束求解 |
| Transitive pinning | 否 |
| Mutable versions | 是 — Git 标签可以被移动。不可变发布 在发布后锁定标签,但仍可删除。 |
我已经详细撰写了这篇文章(链接)。当你写下
uses: actions/checkout@v4
时,你实际上是在声明一个由 GitHub 解析、下载并执行的依赖。Runner 的 PrepareActionsRecursiveAsync(源码)通过以下方式遍历依赖树:
- 下载每个 Action 的 tar 包。
- 读取其
action.yml以发现进一步的依赖。 - 递归最多十层。
完全没有约束求解。2021 年加入的复合‑复合支持导致了传递依赖问题,随后有人请求加入 lockfile(2022 年被标记为“未计划”并关闭)。
你可以对顶层 Action 进行 SHA‑pin,但 Palo Alto 的 “Unpinnable Actions” 研究 表明,传递依赖始终无法固定。2025 年 3 月的 tj‑actions/changed‑files 事件 就是从 reviewdog/action-setup(一个依赖的依赖)开始,攻击者重新标记所有现有版本标签指向恶意代码,该代码会将 CI 密钥泄露到工作流日志,影响了 超过 23 000 个仓库。
GitHub 随后加入了 SHA‑pinning 强制策略(仅限顶层)——详见公告。
Ansible Galaxy
| 功能 | 状态 |
|---|---|
| 注册表 | galaxy.ansible.com |
| 锁文件 | 否 |
| 完整性哈希 | 选择加入 |
| 解析算法 | resolvelib |
| 传递固定 | 否 |
| 可变版本 | 是,未提供不可变性保证 |
Ansible 集合和角色通过 ansible-galaxy 从 galaxy.ansible.com 安装。依赖在 meta/requirements.yml 中声明。安装角色时,会自动安装其声明的依赖,这些依赖还能拥有自己的依赖,形成真正的传递树。
- 解析器是
resolvelib,与 pip 使用的回溯约束求解器相同——比 Terraform 或 Helm 使用的更为复杂。 - 锁文件最早在 2016 年(已归档)提出请求,2018 年重新打开;该议题仍未关闭。
- 已归档的 Mazer 工具在 2020 年被放弃前实现了
install --lockfile,因此该功能曾短暂出现后又消失。
ansible-galaxy collection verify 可以检查与服务器的校验和,且支持 GPG‑签名验证,但两者都是 可选且默认关闭。在 galaxy.ansible.com 上发布的版本可以被发布者覆盖,从 Git 仓库获取的角色同样会遭遇 GitHub Actions 中的可变标签问题。
角色以 Ansible 进程的全部权限执行(可选的 become 提升除外)。关于 无法排除或覆盖传递角色依赖 的长期未决问题仍在讨论中(见 #13215)。
Terraform Providers and Modules
| 功能 | 状态 |
|---|---|
| Registry | registry.terraform.io |
| Lockfile | .terraform.lock.hcl |
| Integrity hashes | 是 |
| Resolution algorithm | 贪婪,最新匹配 |
| Transitive pinning | 对提供者为是;对模块为否 |
| Mutable versions | 提供者不可变;模块使用可变的 git 标签 |
Terraform 从传统的包管理器中汲取了经验:
.terraform.lock.hcl记录 确切的提供者版本 和 加密哈希(多种格式)。terraform init会根据这些哈希验证下载内容,并且提供者是 GPG‑签名 的。- 支持版本约束语法(
~> 4.0、>= 3.1等)。
注意: 锁文件 仅跟踪提供者,不跟踪模块(GitHub issue #31301)。因此,嵌套模块的依赖需要通过层层升级版本来处理,且没有锁文件的保护。用于固定模块的 Git 标签是可变的,这意味着一个标签固定的模块可以 在不被察觉的情况下被替换为不同的内容(GitHub issue #29867)。
研究人员展示了注册表的拼写抢注(hashic0rp/aws,其中的零)【BoostSecurity 文章】以及 在 NDC Oslo 2025 上的实时供应链攻击演示【Class Central 视频】证明了该攻击在实际中的可行性。
提供者方面相对稳固,但跨越模块的传递树同样受到 GitHub Actions 那类可变引用问题的困扰。
Helm Charts
| 特性 | 详情 |
|---|---|
| Registry | Chart 仓库 / OCI 注册表 |
| Lockfile | Chart.lock |
| Integrity hashes | 可选 |
| Resolution algorithm | 贪婪式,根目录优先 |
| Transitive pinning | 是 |
| Mutable versions | 取决于注册表;OCI 摘要是不可变的,Chart 仓库标签则不是 |
Kubernetes Helm 的包管理器特性比这里的大多数工具都要多。
Chart.yaml声明带有版本约束的依赖。Chart.lock记录精确解析后的版本。- 子 Chart 可以拥有自己的依赖,构建真正的传递依赖树。
解析器(source)会挑选每个约束匹配的最新版本,且在冲突出现时,离根目录更近的版本声明拥有优先权。
- Chart 仓库会提供一个
index.yaml,其作用类似于包索引。 - OCI 注册表同样可用,但可变性不同:
- OCI 摘要 – 内容寻址且不可变。
- 传统 Chart 仓库 – 发布者可以通过重新上传到同一 URL 来覆盖版本;
Chart.lock记录的是版本号而非内容哈希,因此无法捕获此类更改。
Helm 支持 provenance 文件 用于 Chart 签名,尽管采纳率仍然很低(Helm provenance docs)。
已知限制与漏洞
helm dependency build仅解析一级依赖(GitHub issue #2247),不包括传递依赖。- 在未显式列出它们的情况下,无法为传递依赖设置 values(GitHub issue #8289)。
- 没有办法禁用传递子 Chart 的 condition(GitHub issue #12020)。
- 通过
Chart.lock的符号链接攻击 在运行helm dependency update时可能导致本地代码执行;已在 v3.18.4 中修复(GHSA‑557j‑xg8c‑q2mm)。 - 恶意 Helm Chart 曾被用于 攻击 Argo CD 并窃取部署中的机密(APIiro blog)。
如果它具有传递执行,它就是包管理器
一旦工具产生了传递依赖,它就会继承一套特定的问题——无论它是否承认这些问题:
| 问题 | 描述 |
|---|---|
| 可复现性 | 依赖树每次解析的结果可能不同;需要 lockfile 来记录确切的结果。 |
| 供应链放大 | 树中深处的单个受损包可以 向外级联,影响所有依赖它的项目(示例)。 |
| 覆盖与排除 | 用户需要机制来处理他们未选择且不想要的传递依赖。 |
| 可变引用 | 可以被移动、改写或强制推送的版本标签意味着同一标识符明天可能指向不同的代码。 |
| 全树固定 | 如果直接依赖的依赖使用了可变引用,仅固定直接依赖是无效的。 |
| 完整性验证 | 必须能够确认今天运行的内容与昨天运行的内容是相同的。 |
如果你的工具表现出这些问题,它 就是一个包管理器。再怎么称它为“插件系统”或“市场”,也阻止不了供应链攻击敲你的门。