如果它像包管理器一样嘎嘎叫
If It Quacks Like a Package Manager

原始链接: https://nesbitt.io/2026/03/08/if-it-quacks-like-a-package-manager.html

## 意外包管理器的兴起 许多工具——例如 GitHub Actions、Ansible Galaxy、Terraform 和 Helm——尽管最初并非为此目的设计,却正在演变为包管理器。当它们发展出传递依赖树时就会发生这种情况:包依赖于其他包,从而产生复杂的关联。这引入了熟悉的包管理挑战:确保可重复性、保护供应链和管理覆盖。 虽然一些工具(如 Terraform)提供强大的功能,例如针对*某些*依赖项(提供程序)的锁定文件和完整性验证,但大多数工具仍然落后于成熟的包管理器,例如 npm 或 Cargo。常见问题包括缺乏锁定文件、可变版本标签(允许在相同标识符下替换代码)以及不完整的依赖项固定。 最近的事件,例如 tj-actions/changed-files 攻击,表明了这些漏洞的真实风险。即使通过 SHA 固定强制执行等改进,传递依赖项通常仍然没有得到保护。最终,任何具有传递执行的工具*都*是包管理器,必须解决这些固有的安全和可靠性问题——否则将面临供应链攻击的目标。

一个由“如果它像包管理器一样嘎嘎叫”帖子引发的黑客新闻讨论,探讨了什么*真正*定义了一个包管理器。最初的帖子认为传递性执行是关键,但评论者很快对此展开辩论。 争论范围从像制品下载和安装/卸载这样的定义性特征,到更抽象的概念。有人认为,即使是编程语言函数(如 Ruby 方法)也可能被认为是包管理器,因为它们保证了依赖关系。另一些人则指出像 Raku 这样的语言,它在代码*内部*管理版本依赖关系,而不是系统范围内的安装。 这场对话突出了依赖管理不断演变的状态,质疑传统定义是否仍然有效,因为工具和语言不断创新。一个关键的收获是,即使在函数级别,细粒度的依赖控制也可能对重构和升级有益。Go 和 CUE 中使用的最小版本选择 (MVS) 算法也被提及,作为一种有趣的依赖解析方法。
相关文章

原文

I spend a lot of time studying package managers, and after a while you develop an eye for things that quack like one. Plenty of tools have registries, version pinning, code that gets downloaded and executed on your behalf. But flat lists of installable things aren’t very interesting.

The quacking that catches my ear is when something develops a dependency graph: your package depends on a package that depends on a package, and now you need resolution algorithms, lockfiles, integrity verification, and some way to answer “what am I actually running and how did it get here?”

Several tools that started as plugin systems, CI runners, and chart templating tools have quietly grown transitive dependency trees. Now they walk like a package manager, quack like a package manager, and have all the problems that npm and Cargo and Bundler have spent years learning to manage, though most of them haven’t caught up on the solutions.

GitHub Actions

  • Registry: GitHub repos
  • Lockfile: No
  • Integrity hashes: No
  • Resolution algorithm: Recursive download, no constraint solving
  • Transitive pinning: No
  • Mutable versions: Yes, git tags can be moved. Immutable releases lock tags after publication but can still be deleted

I wrote about this at length already. When you write uses: actions/checkout@v4, you’re declaring a dependency that GitHub resolves, downloads, and executes, and the runner’s PrepareActionsRecursiveAsync walks the tree by downloading each action’s tarball, reading its action.yml to find further dependencies, and recursing up to ten levels deep. There’s no constraint solving at all. Composite-in-composite support was added in 2021, creating the transitive dependency problem, and a lockfile was requested and closed as “not planned” in 2022.

You can SHA-pin the top-level action, but Palo Alto’s “Unpinnable Actions” research documented how transitive dependencies remain unpinnable regardless. The tj-actions/changed-files incident in March 2025 started with reviewdog/action-setup, a dependency of a dependency, and cascaded outward when the attacker retagged all existing version tags to point at malicious code that dumped CI secrets to workflow logs, affecting over 23,000 repos. GitHub has since added SHA pinning enforcement policies, but only for top-level references.

Ansible Galaxy

  • Registry: galaxy.ansible.com
  • Lockfile: No
  • Integrity hashes: Opt-in
  • Resolution algorithm: resolvelib
  • Transitive pinning: No
  • Mutable versions: Yes, no immutability guarantees

Ansible collections and roles install via ansible-galaxy from galaxy.ansible.com, with dependencies declared in meta/requirements.yml. When you install a role, its declared dependencies automatically install too, and those dependencies can have their own dependencies, forming a real transitive tree with collections depending on other collections at specific version ranges. The resolver is resolvelib, the same library pip uses, which is a backtracking constraint solver and more sophisticated than what Terraform or Helm use.

A lockfile was first requested in 2016, that repo was archived, and the request was recreated in 2018 where it remains open. The now-archived Mazer tool actually implemented install --lockfile before being abandoned in 2020, so the feature existed briefly and then disappeared.

ansible-galaxy collection verify can check checksums against the server and GPG signature verification exists, but both are opt-in and off by default. Published versions on galaxy.ansible.com can be overwritten by the publisher, since there’s no immutability enforcement on the registry side, and roles sourced from git repos have the same mutable-tag problem as GitHub Actions.

Roles execute with the full privileges of the Ansible process with become directives escalating further, and there are open issues going back years about the inability to exclude or override transitive role dependencies.

Terraform providers and modules

  • Registry: registry.terraform.io
  • Lockfile: .terraform.lock.hcl
  • Integrity hashes: Yes
  • Resolution algorithm: Greedy, newest match
  • Transitive pinning: Yes, for providers; no, for modules
  • Mutable versions: Providers immutable; modules use mutable git tags

Terraform actually learned from package managers. .terraform.lock.hcl records exact provider versions and cryptographic hashes in multiple formats, terraform init verifies downloads against those hashes, and providers are GPG-signed. The version constraint syntax (~> 4.0, >= 3.1, < 4.0) looks like it was lifted straight from Bundler.

The resolver collects all version constraints from root and child modules, intersects them, and picks the newest version that fits, with no backtracking or SAT solving. Modules can call other modules which call other modules, creating transitive trees, and the lock file captures the resolved state.

The lock file only tracks providers, not modules though, so nested module dependencies require cascading version bumps with no lockfile protection. Git tags used to pin modules are mutable, meaning a tag-pinned module can be silently replaced with different content.

Researchers demonstrated registry typosquatting (hashic0rp/aws with a zero), and a live supply chain attack demo at NDC Oslo 2025 showed this working in practice. The provider side is solid, but the module side of the transitive tree has the same mutable-reference problems as GitHub Actions.

Helm charts

  • Registry: Chart repos / OCI registries
  • Lockfile: Chart.lock
  • Integrity hashes: Opt-in
  • Resolution algorithm: Greedy, root precedence
  • Transitive pinning: Yes
  • Mutable versions: Depends on registry; OCI digests are immutable, chart repo tags are not

Kubernetes Helm has more package manager DNA than most things here. Chart.yaml declares dependencies with version constraints, Chart.lock records the exact resolved versions, and subcharts can have their own dependencies, building out genuine transitive trees. The resolver picks the newest version matching each constraint, with versions specified closer to the root taking precedence when conflicts arise.

Chart repositories serve an index.yaml that works like a package index, and OCI registries work too. Mutability depends on which backend you use: OCI digests are content-addressed and immutable, but traditional chart repos let publishers overwrite a version by re-uploading to the same URL, and nothing in Chart.lock will catch the change since it records version numbers rather than content hashes. Helm supports provenance files for chart signing, though adoption is low.

helm dependency build only resolves first-level dependencies, not transitive ones, so subchart dependencies need manual handling. You can’t set values for transitive dependencies without explicitly listing them, and there’s no way to disable a transitive subchart’s condition.

A symlink attack via Chart.lock allowed local code execution when running helm dependency update, fixed in v3.18.4. Malicious Helm charts have been used to exploit Argo CD and steal secrets from deployments.

If it has transitive execution, it’s a package manager

Once a tool develops transitive dependencies, it inherits a specific set of problems whether it acknowledges them or not:

  • Reproducibility. The tree can resolve differently each time, so you need a lockfile to record what you got.
  • Supply chain amplification. A single compromised package deep in the tree can cascade outward through every project that depends on it.
  • Override and exclusion. Users need mechanisms to deal with transitive dependencies they didn’t choose and don’t want.
  • Mutable references. Version tags that can be moved, rewritten, or force-pushed mean the same identifier can point at different code tomorrow.
  • Full-tree pinning. Pinning your direct dependencies means nothing if their dependencies use mutable references.
  • Integrity verification. You need to know that what you’re running today is the same thing you ran yesterday.

If your tool has these problems, it’s a package manager, and no amount of calling it a “plugin system” or “marketplace” will stop the supply chain attacks from quacking at your door.

联系我们 contact @ memedata.com