Files
qinglong/docs/adr/ADR-0128-exact-cluster-image-sbom-and-attested-multiarch-release.md
T

229 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0128:精确 Cluster 镜像 SBOM 与证明化多架构发布
- 状态:Acceptedcontrol/admin 双镜像的精确 SBOM、原生双架构 CI、OCI
digest graph 与独立 GHCR/Cosign/GitHub attestation 发布契约已完成;control
已有本地 arm64 inventory 与 amd64/arm64 OCI 实证,admin 已有本地 arm64
inventory,真实 admin 双架构 OCI 和远端受保护发布记录仍是 Release Gate
- 日期:2026-07-24
- 关联 RFCQL-RFC-0001 D-14、D-61、D-85、D-105、D-124、D-126
- 关联 ADRADR-0038、ADR-0042、ADR-0062、ADR-0088、ADR-0090、ADR-0126
> ADR-0196 已把该供应链扩展为 `control|admin|local` 三个 profile,并将唯一
> 发布入口迁移到 `.github/workflows/ql3-image-release.yml`。本 ADR 以下
> control/admin 数字保留为建立该机制时的历史证据。ADR-0254 随后删除 control/admin 可覆盖的
> `NODE_IMAGE` build arg,将两个 build/runtime stage 与 local 一起固定到同一 Node 多架构 digest,并增加六矩阵
> OS vulnerability pre-publish gate。
## 背景
ADR-0126 已建立可构建的 Cluster 镜像,但“存在 lock”和“生成 SBOM”都不能单独
证明实际运行时闭包。对 builder/runtime 共用、同时声明 `@types/pg` 的 npm
manifest 执行 `npm ci --omit=dev` 时,Drizzle 的 optional peer 会让
`@types/pg``@types/node``undici-types` 仍进入生产镜像。另一方面,
`npm sbom --package-lock-only --omit=dev` 又会漏掉 lock 中标为
`devOptional`、但通过 `pg` 实际运行时可达的八个包。
发布侧如果只推 tag,则无法证明部署拿到哪个 manifest;只生成 CI 文件也不能
证明它已经在远端成功签名。因此需要把源码闭包、实际镜像清单、不可变 digest、
签名和证明拆成可独立判定的证据。
## 决策
### 1. Builder 与 production dependency root 分离
每个镜像分别保留两个 npm dependency root
- builder manifest/lock 包含 TypeScript 和受审的 exact type dependency
- `runtime-dependencies/package.json` 与 lock 只包含五个 exact production
root dependency。
两者的 package name、version 和 production dependency map 必须完全相同。
production stage 只能对 production lock 执行 `npm ci --omit=dev`。不能依靠
`npm prune`、手工删除目录或 SBOM allowlist 隐藏 builder 泄漏。
control 与 admin 不能共用一个 production rootcontrol 的第五个根是
`@aws-sdk/client-s3`admin 的第五个根是 `@kubernetes/client-node`。两者拥有
独立 manifest/lock、Dockerfile、image repository 和 digest,防止短生命周期
Kubernetes/admin authority 进入常驻 control 镜像。
该目录是镜像构建输入,不是 workspace package,不增加 21 importer hard cap。
### 2. SBOM 从生产锁的可达图生成,并与实际镜像逐包对账
`ql3-cluster-image-sbom.cjs` 从五个 root dependency 遍历 npm v3 lock 中
`dependencies + optionalDependencies` 的实际解析位置。任何可达节点即使被 npm
标成 `devOptional` 也进入图;peer dependency 不因出现在 builder lock 就自动
进入生产图。
SBOM 必须显式选择 exact `control|admin` profile,生成的 CycloneDX 1.5 文档
分别固定包含:
- control43 个外部 runtime component`runtime-core``cluster-postgres`
`cluster-control` 三个内部 component,一个 image dependency root47 个
dependency node
- admin85 个外部 runtime component`runtime-core``cluster-postgres`
`cluster-admin` 三个内部 component,一个独立 image dependency root89 个
dependency node。
组件使用 `name@version` purl 作为 `bom-ref`registry 包携带 lock integrity 的
SHA-512 和 distribution URL。内部 `workspace:*` 必须解析到 exact version。
重复 ref、缺边、多包、少包、版本漂移、`typescript`、profile/lock 错配或
closure 外的类型包全部失败关闭。admin production graph 中
`@kubernetes/client-node` 自身把 `@types/js-yaml` 声明为 runtime dependency
该节点必须因 exact lock 可达而保留,不能继续用 `@types/*` 名称前缀冒充依赖
可达性判断。
CI 还必须在 UID 10001、只读根、`no-new-privileges` 容器中枚举实际
`/opt/qinglong/node_modules`,并要求其 `name@version` 集合与所选 profile 的
control 46 或 admin 87 个 SBOM component 完全相同。源码 SBOM 成功不能替代这一步,也不能
拿 control 的 46-component 文档证明 admin 镜像。
### 3. CI 原生验证 amd64 与 arm64
`ql3-ci.yml` 使用 `control|admin × amd64|arm64` 四项矩阵,在 GitHub 原生
`ubuntu-24.04``ubuntu-24.04-arm` runner 分别构建所选 production
Dockerfile,验证:
- runner Node architecture
- image architecture 和 `10001:10001`
- SBOM 正/负向测试;
- 部署与发布静态契约;
- 实际镜像 46/88 包闭包。
该 job 成功后只证明两个单架构构建;它不等同于 registry 已存在一个多架构
manifest。
另一个独立 OCI evidence job 同样以 `control|admin` 为矩阵,必须使用 QEMU +
Buildx 分别构建 `linux/amd64,linux/arm64`,同时启用 SPDX SBOM 与 maximum
SLSA provenance,再离线审计每个完整 OCI layout。审计必须:
- 对每个 content-addressed blob 流式复算 SHA-256 并核对 descriptor size
- 拒绝未引用 blob、缺平台、重复/未绑定 attestation 和超出容量的图;
- 分别复验两个 config 的 architecture、UID/GID、entrypoint、工作目录、端口、
production env、OCI label 和 source revision
- 要求每个平台恰有一个 attestation manifest,且包含 SPDX 与 SLSA 两个
in-toto predicate
- 从 Syft SPDX 中只选择 `/opt/qinglong/node_modules`,与所选 CycloneDX 的
control 46 或 admin 87 个 npm purl 精确对账;
- 只允许 source revision 和无 credential 的标准 proxy build arg 进入
provenance。
本地 2026-07-24 证据的 root index 为
`sha256:7859b32b136f7d82f4504b0e0895c560348e4acab460d4ece0127d47a62bea5e`
- amd64 manifest
`sha256:d6cecd239953d0ed4791c58ee769d78656f913e7e158cdab373db468d6fb2814`
compressed layer bytes 83,746,881
- arm64 manifest
`sha256:f7b0b7e1ab868e09617527ca0d28ff8becab27d81ce008af685597dc1be768b7`
compressed layer bytes 83,624,682
- layout 共 27 个 blob、172,835,866 bytes;两个平台都对账 46 个应用 npm
package,并各自拥有 SPDX-2.3 与 SLSA provenance v1。
admin 历史本地 arm64 镜像已在 UID/GID 10001、只读根、
`no-new-privileges` 下完成实际 inventory 对账。ADR-0218 删除未使用的 Croner 后,
当前 lock/SBOM 为 84 external + 3 internal、87 component/88 dependency node
更新后真实镜像 inventory 仍待依赖物化复验。本机 admin 双架构 OCI 实证尚未取得:
2026-07-25 两次 Buildx
尝试均在拉取官方 `docker/buildkit-syft-scanner:stable-1` 的 Docker Hub OAuth
token 阶段超时,未进入项目 Dockerfile 构建;该外部失败不能记为 OCI 通过。
### 4. Release 只围绕每个 pushed digest 建立 authority
独立 release workflow 只接受 `v3.*` tag 或显式 QingLong 3 SemVer dispatch。
它以 exact matrix 分别把 `qinglong3-cluster-control`
`qinglong3-cluster-admin``linux/amd64,linux/arm64` manifest 发布到不同
GHCR repository,并以每个 build 返回的 `sha256:*` digest 作为该镜像唯一证明
subject。两个 job 不共享 digest、SBOM 或证明;tag 只用于发现,Kubernetes
生产 overlay 必须分别 pin 两个 digest。
提交的 CloudNativePG control/recovery overlay 使用不可拉取的全零 digest 作为
fail-closed 占位。私有部署输入必须分别替换 control/admin digest;静态部署门禁
拒绝任一 production-oriented overlay 回退到 `newTag`,也不允许用同一份
control 证明替代 admin Job。
每个 digest 必须同时具备:
1. BuildKit `sbom: true`
2. BuildKit `provenance: mode=max`
3. Cosign GitHub OIDC keyless signature
4. `actions/attest` 生成并推送的 SLSA provenance
5. `actions/attest` 生成并推送的受审 CycloneDX 应用 SBOM。
创建签名和证明之后,每个 matrix job 自身还必须从 GHCR 读取自己的
`IMAGE@DIGEST` 并失败关闭:
1. `docker buildx imagetools inspect --raw` 的 root index 必须只有
`linux/amd64``linux/arm64` 两个 runnable manifest,以及与二者一对一
digest 绑定的两个 BuildKit attestation manifest
2. Cosign verification 必须精确绑定当前 repository、release workflow path
`GITHUB_REF` 组成的 certificate identity,并固定 GitHub Actions OIDC
issuer,禁止宽泛 identity regexp
3. GitHub SLSA 与 CycloneDX 必须作为两次独立
`gh attestation verify` 执行,从 OCI registry 读取 bundle,绑定 repository、
signer workflow、`GITHUB_SHA``GITHUB_REF`,并拒绝 self-hosted runner
4. CycloneDX verification 必须显式要求
`https://cyclonedx.org/bom` predicate,不能让默认 SLSA verification
冒充应用 SBOM 已验证。
workflow 只授予 `contents: read``packages: write``id-token: write`
`attestations: write``artifact-metadata: write`。Pull Request 不得触发
发布。digest 形态不合法时必须在签名/证明前失败。release job 的 checkout、
Node、Docker、Cosign 和 attestation action 必须固定到已核验 tag 对应的完整
commit SHA;可移动 major tag 只保留为行尾可读注释,不能作为执行 ref。
## 不代表什么
本 ADR 当前本地证据不代表:
- release workflow 已在 GitHub-hosted runner 成功;本地双架构 OCI 证据不能
代替远端受保护发布环境;
- GHCR 已存在可拉取且彼此独立的 control/admin QingLong 3 digest
- Cosign signature 或 GitHub attestation 已在远端 registry 可验证;
- 当前 base image 或 npm graph 没有漏洞;
- image 在目标 Kubernetes 节点满足容量、冷启动或网络策略要求;
- PostgreSQL operator、STONITH、Pod 分区和 CA 重叠轮换 Gate 已完成。
只有实际 release run、不可变 digest 拉取和独立 verify 记录才能关闭前三项。
workflow YAML 通过静态审计只证明发布意图没有被静默降级。
## 替代方案
- **直接使用 `npm sbom --package-lock-only --omit=dev`**:拒绝。当前 lock
实测漏掉八个实际 runtime package。
- **只用 `npm ci --omit=dev` 和共用 manifest**:拒绝。optional peer 会留下
三个类型相关 package。
- **构建后手工删除 `@types`**:拒绝。依赖变化时会静默漂移,且 lock 不再代表
安装输入。
- **只上传 SBOM 文件**:拒绝。未绑定 pushed digest 的文件不能证明部署镜像。
- **只依赖可变 tag**:拒绝。tag 可移动,不能作为 rollout 或审计 authority。
- **PR 构建时直接发布**:拒绝。扩大 token/registry 写权限和不受信代码风险。
- **写权限 job 使用 action major tag**:拒绝。tag 可移动,release action 必须
使用完整 commit SHA。
## 验证
- `pnpm sbom:cluster-image:ql3`
- `pnpm sbom:cluster-image:ql3 --image=admin`
- `pnpm audit:cluster-image-release:ql3`
- `node --test test/back/ql3ClusterImageSbom.test.cjs`
- `node --test test/back/ql3ClusterImageReleaseAudit.test.cjs`
- `node --test test/back/ql3ClusterOciLayoutAudit.test.cjs`
- `node --test test/back/ql3ClusterRemoteManifestAudit.test.cjs`
- `node --test test/back/ql3ClusterDeploymentAudit.test.cjs`
- 生产 Dockerfile 实际 build
- UID 10001、只读根容器内以 `--image=control|admin --inventory-root`
分别对账 46/88 个 component
- CI 的 control/admin × 原生 amd64/arm64 image job
- 每个 Buildx amd64/arm64 OCI output 后执行
`ql3-cluster-oci-layout-audit.cjs --image=control|admin --layout=...
--expected-revision=...`
- release 后由 workflow 审计远端 manifest/attestation 一对一绑定,并以 exact
workflow identity/source revision/source ref 分别执行 Cosign、GitHub SLSA
和 CycloneDX verify;独立发布记录仍必须保存该结果。
静态负向门必须拒绝缺 admin 或 control profile、profile/SBOM 错配、缺 arm64、
缺 OIDC 权限、缺 digest 签名、缺应用 SBOM证明、缺实际 image inventory、缺
远端 manifest audit、宽泛证书 identity、证明未绑定 source、CycloneDX predicate
漂移或 PR 发布。