从容器镜像到依赖树:SBOM、来源证明与 Attestation 的实践教训

2026-09-12 22 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:14 分钟

当一个 PostgreSQL 扩展项目从“能构建”走向“值得被依赖”,真正棘手的问题往往不再是 Dockerfile 能否跑通,而是:镜像里到底有什么?这些文件来自哪里?依赖是否完整?漏洞扫描器能否看见它们?未来出现新的供应链事件时,谁负责重建和发布?

围绕 CNPG-Extensions 的实践说明,SBOM、provenance 和 attestation 不是发布流程末尾的装饰品,而是构建系统、依赖管理、许可证识别和项目治理共同组成的一套工程约束。

SBOM 不只是列出几个软件包

容器镜像的 SBOM 常被理解为一个软件包清单,但真实项目通常有多层依赖:

  • Debian 包管理器安装的系统库和工具;
  • PostgreSQL 扩展自身的构建产物;
  • Rust 项目的直接依赖和传递依赖;
  • 只存在于 Cargo.lock 中的包;
  • 通过 Git 地址引用、没有发布到 crates.io 的依赖;
  • 最终被复制进运行时镜像的文件与库。

如果 SBOM 只描述源码目录,或者只描述构建环境,而没有反映最终镜像内容,扫描结果就可能出现两类问题:把并未发布的依赖算进去,或者漏掉实际运行时会加载的库。

对于基于 pgrx 的扩展,完整的 Rust 依赖图尤其重要。依赖树深处的包也可能存在 RUSTSEC 漏洞;只扫描顶层扩展名称无法回答“这个镜像里是否包含有问题的 Rust 包”。

可以在本地用下面的命令检查一个镜像。这里假设已经安装了最新版本的 Syft 和 Trivy,并且镜像名是 example/pg-extension:dev

#!/usr/bin/env bash
set -euo pipefail

IMAGE="example/pg-extension:dev"

# 生成 SPDX 格式的镜像 SBOM
syft "docker:${IMAGE}" -o spdx-json=sbom-syft.spdx.json

# 让 Trivy 读取镜像并输出漏洞与依赖信息
trivy image \
  --scanners vuln,secret \
  --format table \
  --severity HIGH,CRITICAL \
  "${IMAGE}"

# 直接从 Cargo.lock 生成 Rust 依赖 SBOM
trivy fs \
  --format cyclonedx \
  --output sbom-cargo.cdx.json \
  --scanners vuln \
  .

实际接入 CI 时,应固定工具版本并把 SBOM 作为构建产物保存。否则扫描器升级后可能改变解析结果,导致同一份源码在不同时间得到难以比较的报告。

工具各有盲区,组合比单押更可靠

容器构建工具链通常会使用 Syft 生成默认 SBOM,但默认结果并不等于完整结果。实践中,Debian 包的许可证识别可能不够准确或不够完整,尤其是许可证文件位置、命名和包元数据不统一时。

许可证扫描也可能被现实中的大文件卡住。例如某些扩展包含很大的许可证文件,扫描工具可能直接失败。一个可行的补救方式是按稳定的许可证起始标记把文件拆分,分别扫描后再合并结果。这个过程不够优雅,却比悄悄丢失许可证信息更可靠。

Trivy 对 Rust 项目的支持也需要留意版本。较旧版本可能无法解析使用 workspace 的 Cargo.lock,而最新版本仍可能遗漏只通过 Git 地址声明的依赖。与此同时,Trivy 生成 SPDX SBOM 时未必能提供项目所需的 CONTAINS 关系,因此仍然需要 Syft 来帮助建立“最终复制进扩展容器的文件属于哪些包”的反向关联。

这意味着 SBOM 流程更像一个交叉验证系统:

  • 用 Syft 观察最终镜像和文件层级;
  • 用 Trivy 检查漏洞、Cargo.lock 和多种依赖来源;
  • 用专门的许可证扫描工具补齐包许可证信息;
  • 对关键包和异常结果保留人工复核路径。

不要因为某个工具成功生成了 JSON 文件,就把它当成供应链事实的完整证明。格式正确只说明流程结束了,不说明数据完整。

自动更新的边界:版本号不总能表达语义

Renovate 可以显著降低依赖更新成本,但自动更新的前提是:版本字段能够可靠地描述项目状态。

一些 PostgreSQL 扩展的 SQL 版本与操作系统包版本完全不同。Renovate 可以看到包版本变化,却无法仅凭版本号知道 SQL 脚本是否需要更新。要确认这一点,可能仍需检查源码,甚至启动完整的测试容器。

预发布版本也是类似问题。betarc 或其他预发布标记在不同扩展中的表达方式并不统一。如果稳定渠道无条件接受最新版本,自动化就可能把测试版本推给生产用户。

一种保守的配置方式是:对大多数依赖保持自动更新,但对历史上出现过预发布版本的扩展设置人工审批或版本约束。下面是一个可以改造的 Renovate 配置示例,具体包名和规则需要按项目实际情况调整:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"],
  "prConcurrentLimit": 5,
  "packageRules": [
    {
      "description": "Stable channel must not accept prereleases",
      "matchPackageNames": [
        "example/mysql-fdw",
        "example/pl-debugger"
      ],
      "ignoreUnstable": true,
      "automerge": false
    }
  ]
}

这里的 ignoreUnstable 不能替代发布前验证,因为扩展可能用非标准方式编码开发版本。更稳妥的做法是把版本判断、SQL 版本检查和容器启动测试放进 CI,并把“是否允许进入 stable”作为明确的机器可判定条件。

Provenance 与签名:证明什么,比签名本身更重要

Attestation 的价值在于把构建结果与构建过程联系起来,例如源码提交、工作流、构建平台和镜像摘要。签名则帮助使用者验证这份声明确实由预期的身份发布。

常见路线包括 Cosign/Sigstore 和 GitHub Artifact Attestations。选择时不能只看 API 是否顺手,还要检查:

  • 使用者能否在自己的环境中验证;
  • 镜像是否会被镜像仓库或其他渠道转发;
  • 组织或仓库权限是否影响验证和发布;
  • 是否存在企业版订阅或跨仓库场景下的功能限制;
  • 签名密钥、OIDC 身份和撤销策略由谁维护。

例如,使用 Cosign 的基本流程可以这样实践。下面的命令假设 GitHub Actions 已通过 OIDC 获得身份,并且镜像已经推送到仓库:

#!/usr/bin/env bash
set -euo pipefail

IMAGE="ghcr.io/example/pg-extension"
DIGEST="sha256:replace-with-the-pushed-digest"
REF="${IMAGE}@${DIGEST}"

# 对镜像摘要签署 provenance 文件
cosign attest \
  --yes \
  --predicate provenance.json \
  --type slsaprovenance \
  "${REF}"

# 上传并签署 SBOM
cosign attest \
  --yes \
  --predicate sbom-syft.spdx.json \
  --type spdxjson \
  "${REF}"

# 使用者验证 attestation
cosign verify-attestation \
  --type slsaprovenance \
  "${REF}"

生产环境中不要把示例摘要替换成 tag 后就结束。Tag 可以被移动,摘要才是具体镜像内容的稳定标识。验证时还应检查证书身份、工作流来源和仓库范围,而不只是检查“签名存在”。

构建速度也是供应链设计的一部分

为了生成 Rust 依赖清单,某些方案会在构建过程中编译 cargo-aboutcargo-cyclonedx 等工具。对于大型 pgrx 扩展,这些额外编译可能非常耗时,甚至让 GitHub Actions 触发六小时超时。

因此,SBOM 工具应该像编译器一样被缓存、固定版本并单独评估成本:

  • 将工具安装和扩展编译分成可缓存的步骤;
  • 缓存 Cargo registry、Git 依赖和构建目标目录;
  • 只在需要时构建许可证或 SBOM 工具;
  • 对大型扩展设置独立 job,避免拖慢所有架构和 PostgreSQL 版本矩阵;
  • 记录每个 job 的耗时,及时发现工具升级造成的回归。

而对于 C++ 扩展,例如 pg-duckdb,现成的 Rust 依赖工具无法直接解决问题,可能需要专门的依赖处理脚本。AI 可以帮助快速编写这类脚本,但生成代码并不会消除长期维护成本。只有当该扩展确实值得持续发布和扫描时,定制构建系统才值得留下。

项目治理决定了安全工作的有效期

GitHub Actions 能够免费提供大量开源计算资源,使一个项目同时覆盖多个架构、多个 Debian 基础镜像和多个 CloudNativePG 版本成为可能。但 job 数量增长后,真正的难题会从“能不能构建”转向“谁在维护这套系统”。

贡献门槛、代码审查深度、贡献者的长期承诺,以及如何处理大型一次性代码提交,都会影响项目未来的安全响应能力。一个没人愿意重建的镜像,即使今天拥有漂亮的 SBOM 和 provenance,明天也可能因为新漏洞出现而失去价值。

可以用 OpenSSF Scorecard 一类的检查来辅助评估仓库安全实践,但分数不能替代维护者判断。更实际的发布检查表包括:

  • 构建是否来自可追踪的提交和固定版本工具链;
  • 镜像是否同时发布 SBOM 和 provenance;
  • SBOM 是否覆盖运行时实际文件,而不只是源码依赖;
  • Rust workspace 和 Git 依赖是否经过验证;
  • 稳定渠道是否明确拒绝 beta 和 rc;
  • 新漏洞出现后,是否有人负责重建和重新发布;
  • 签名验证是否不依赖发布者本地环境;
  • 新贡献是否带来可持续的维护责任。

结语:把“可验证”当作发布能力

SBOM、许可证、漏洞扫描、provenance 和 attestation 之间没有一个工具能够包办全部工作。更现实的工程目标是建立一条可重复、可审计、能在工具出错时暴露问题的流水线。

自动化应当尽可能减少重复审批,但不能掩盖版本语义不清、依赖解析不完整和维护责任缺失。对于 PostgreSQL 扩展这类同时涉及数据库、系统包、编译器和多架构镜像的项目,最值得投资的不是某个漂亮的报告文件,而是让使用者能够回答三个问题:镜像里有什么、它是怎么构建的、出了问题谁会修复。


相关推荐