跳转到内容
GitHub

供应链安全

了解 Depsilo v0.11.0 的最小发布年龄、恶意包封锁、篡改检测、冻结快照与审计完整性。

Depsilo 把供应链策略放在包管理器与上游之间。v0.11.0 提供这些相互独立的控制:

控制 新安装默认值 结果 主要边界
最低发布年龄 关闭 年轻版本返回 451 QUARANTINED 时间戳来源按生态绑定;Go 与 APT 没有可用来源
已知恶意包封锁 开启,每 6 小时同步 匹配版本返回 451 MALICIOUS_BLOCKED 覆盖 8 个生态的 OSV MAL-* 记录
篡改检测 开启 保留首见哈希并发出严重告警;不返回 451 被动检测,首见信任
冻结 / 黄金快照 关闭 快照模式下未批准的版本返回 451 SNAPSHOT_BLOCKED 只覆盖缓存中已经存在的制品
审计完整性与 SIEM 转发 审计链开启;转发需配置 审计行哈希链接、可离线校验,并可转发到收集器 整库重写只有配合外部锚点才能发现

这些控制彼此独立,也不会改变缓存、上游选择或代理行为。

最小发布年龄在请求路径上拒绝“太新”的版本。Depsilo 为每个生态绑定“制品身份 + 时间戳 + 将要提供该制品的上游”,只有能证明时间戳来源的生态才接受正阈值:

生态 时间戳来源 启用条件
npm 包文档的 time[version],随认证过的 tarball 令牌传递 直接可用
PyPI 同一上游 PEP 691 JSON simple index 的 upload-time 上游需提供 JSON;仅 HTML 的上游会 fail closed
Composer p2 元数据自身的 time 字段 直接可用;客户端可能在 451 之后回退原始 dist URL
NuGet v3 registration index 的 published 直接可用
Cargo sparse index 的 pubtime 与 cksum 直接可用
RubyGems compact index 的 created_at 与校验和 直接可用
Conda、CRAN、Maven、Alpine、Helm 制品响应头的 Last-Modified(近似) 需在 supply_chain.approximate_sources 中确认
Docker 本实例第一次看到该 digest 的时间(首次观测,不是发布时间) 需在 supply_chain.observation_sources 中确认
Go、APT 没有可用的发布时间来源 阈值保持 0
[supply_chain]
min_release_age_enabled = true
mode = "block" # 或 "warn":只记录并放行,用于上线前的观察期
fail_closed = true
approximate_sources = ["conda", "cran", "maven", "alpine", "helm"]
observation_sources = ["docker"]
# allow = ["npm:internal-*"] # 按生态前缀绕过隔离,绕过同样写审计
[supply_chain.min_release_age]
default = "0"
pypi = "3d"
npm = "7d"
cargo = "3d"

行为与边界:

  • 决定发生在缓存查找之前:已经缓存过的版本也不能绕过新策略。
  • 时间戳缺失时 fail closed;给未绑定或未确认的生态配置正阈值会被启动拒绝,而不是 静默降级成“不检查”。
  • 管理员可以在 Admin 中批准单个 (生态, 包, 版本);批准、撤销与 allow 绕过都会写审计。
  • Docker 的年龄从本实例第一次拉取该 digest 起算,发布者无法伪造;未见过的 digest 在阈值内保持隔离,registry 不返回 Docker-Content-Digest 时拒绝提供。
  • 近似生态信任服务上游返回的 Last-Modified;Composer 客户端可能绕过 451 回退到 原始 dist URL,因此这一条只能尽力而为。

恶意包封锁默认开启:

[supply_chain.blocklist]
enabled = true
sync_interval = "6h"
# mirror_url = "https://osv-vulnerabilities.storage.googleapis.com"
# proxy = "http://127.0.0.1:7890"

v0.11.0 从 OSV malicious-packages 数据集中同步 npm、PyPI、Cargo、RubyGems、Composer、 NuGet、Go 和 Maven 的 MAL-* 记录。它导入明确版本列表和“所有版本”通告;没有明确 版本列表的有界范围会被跳过,因为 Depsilo 不对这种范围作推导。

当数据集覆盖某个生态时,身份无法证明的制品会被拒绝而不是放行:PyPI 的旧式归档 (.zip、.egg、.tar.bz2 等)通过声明它的 simple index 解析身份,RubyGems 通过 compact index 解析精确的 gem 身份,平台 gem 继承基础版本的记录。

匹配发生在发布年龄和 allow 规则之前。已在缓存中的匹配制品也不会通过该门继续提供, 但字节会留在存储中,直到 LRU 或管理员清理。确认误报时,管理员可创建最长 24 小时的 审计覆盖;覆盖不能原地延期,继续使用必须重新作出有记录的决定。

同步失败采用降级策略:

  • 本地已有数据时,继续使用最后一次成功的数据集。
  • 本地没有任何数据时,不会阻断任何版本。
  • 同步错误本身不会让整个代理停止提供包。
[supply_chain.tamper_detection]
enabled = true

对于被视为不可变的制品,Depsilo 在第一次流式获取时记录 SHA-256。后台自然刷新若在 同一版本下收到不同字节,它会拒绝把替换内容写入缓存,继续保留首见缓存副本,并记录 严重 tamper_detected 事件;配置了 webhook 时也可发送通知。如果首见副本已经被 LRU 淘汰,之后的 cache miss 会先存储并返回重新获取的字节,再与保留的哈希基线比较并告警; 此时旧副本已经无法恢复。

必须同时理解以下限制:

  • 检测是被动的,只在正常后台刷新或淘汰后的再次获取中比较,不主动探测上游。
  • 它是告警机制,不会返回 451,也不是通用的阻断策略。
  • “首见”是本地基线,不是签名或透明日志证明;第一次获取已经被污染时,哈希也会记录 被污染的字节。
  • 如果 LRU 已删除原始缓存副本,不一致的新字节在验证前已经被存储并可能提供给客户端; Depsilo 仍会使用旧哈希告警,但无法恢复旧字节。

把当前缓存中的制品集合提升为一个命名、哈希固定的快照,之后可以让实例只提供该快照 内的版本:

  • 在 Admin 的缓存页或 Admin API 中把缓存提升为命名快照,并导出 / 导入 depsilo/snapshot/v1 清单。
  • 打开快照模式后,恶意包门之后任何不在快照内的制品都会以 451 SNAPSHOT_BLOCKED 拒绝,成员查询 fail closed。
  • 快照固定篡改检测记录的 SHA-256;导入清单会为缺失的基线补种,所以与清单不一致的 首次获取仍然会产生篡改告警。

审计日志自 schema v8 起按行哈希链接(prev_hash / hash,UTC 归一化时间戳):

  • 审计页可以随时重新校验;GET /api/v1/admin/audit/integrity 返回完整报告; depsilo audit verify [--json] 可以离线对 SQLite 文件重算整条链。
  • 早期版本写入的行会作为“链前前缀”单独报告,不会被当作已被覆盖。
  • 编辑、删除或重排链上的行会报告第一个断裂的 id。

链本身不能发现“整库重写”:攻击者可以重算整条链。把链头检查点写到数据库之外才能 补上这一点——[audit] checkpoint_file 以 NDJSON 追加写入并 fsync,校验时逐条比对; [audit] checkpoint_url 还会把同一行 POST 到 WORM 网关或日志平台,失败后 60 秒重试, 远程投递状态(remote_head_id、remote_last_success_at、remote_last_error)在 完整性 API 与审计页可见。

SIEM 转发把同一条审计流以 NDJSON 或 Splunk HEC 信封投递到外部收集器:每个 exporter 维护自己的游标,投递至少一次;收集器故障时游标停住而不是丢记录,重启后从最后一个 成功批次继续,失败按 10 秒到 5 分钟退避。可以按动作和结果(blocked、miss 等) 过滤事件;新 exporter 从当前审计头开始。Admin API 与审计页覆盖增删、启停、延迟和 最近错误状态,以及一次走真实路径的测试投递。

多项目工作区里的运行时 SBOM 导出提供 CRA 模式与技术文件预设:携带 purl、篡改检测 记录的 SHA-256、supplier / license 字段和产品到组件的依赖关系。supplier 与 license 来自操作者声明的 [compliance.components] 注解,未声明的组件会明确导出为 unknown; 覆盖缺口会写进清单,而不是编造数值。配置 Ed25519 密钥后,sign=true 会返回对响应体 的分离签名。

常见的绕过路径包括:

  • pip 的额外直连 index;不要让 --extra-index-url 指向不受控上游。
  • Go 的 GOPROXY 使用 |direct 等会在错误后直连的 fallback。
  • Composer 在镜像 dist 被拒绝时可能回退到原始 dist URL。要硬性阻断,必须限制客户端 对原始下载地址的直接访问。

Depsilo 可转发生态自身的校验和或签名,但不会把未签名制品变成有来源证明的制品,也不 替代上游签名、构建来源证明、锁文件和 SBOM。最小发布年龄按生态绑定时间戳来源;恶意 列表阻断已知项目;篡改检测识别同版本字节变化;快照限制可提供的版本范围。这些控制 都不是对包安全性的完整证明。

隔离阻断、allow 绕过、批准与撤销、恶意包阻断与临时覆盖、篡改事件、快照阻断都会写入 数据库中的审计事件。可在 Admin 中按生态、动作和包查看,并按上面的方式校验完整性并 转发到 SIEM。配置 webhook 后,隔离和恶意包阻断可立即通知,篡改不匹配使用严重级别。

上线前至少验证一次:

  1. 为一个来源已绑定的生态(例如 npm)配置正阈值,确认年轻版本返回 451 QUARANTINED 并留下事件。
  2. 需要近似或首次观测生态时,确认启动被接受,且能力摘要显示 approximate / observed 或对应的安全暂停状态,而不是全绿。
  3. 用已审核的 Package Rule 或恶意数据集验证 451 与审计记录。
  4. 打开快照模式后确认快照外的版本返回 451 SNAPSHOT_BLOCKED。
  5. 运行 depsilo audit verify,并把 checkpoint 写到数据库所在的故障域之外。
  6. 客户端没有可绕过代理的备用 registry 或直连出口,审计记录和 webhook 到达预期的 值班通道。