diff --git a/deploy/containers/ql3-cluster-admin/Dockerfile b/deploy/containers/ql3-cluster-admin/Dockerfile index 88e5e07a..c5a3c535 100644 --- a/deploy/containers/ql3-cluster-admin/Dockerfile +++ b/deploy/containers/ql3-cluster-admin/Dockerfile @@ -94,7 +94,7 @@ COPY --chmod=0444 deploy/console/ql3-cluster-copilot/package-management-client-c COPY --chmod=0444 deploy/console/ql3-cluster-copilot/host-environment.example.json \ share/ql3-copilot-console/host-environment.example.json -FROM node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d AS runtime +FROM node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436 AS runtime ARG SOURCE_REVISION=uncommitted diff --git a/deploy/containers/ql3-cluster-control/Dockerfile b/deploy/containers/ql3-cluster-control/Dockerfile index a0484542..028beb96 100644 --- a/deploy/containers/ql3-cluster-control/Dockerfile +++ b/deploy/containers/ql3-cluster-control/Dockerfile @@ -89,7 +89,7 @@ COPY --from=workspace-ai /workspace/packages/ql3-ai/dist \ COPY --from=workspace-ai /workspace/packages/ql3-cluster-control/dist \ node_modules/@qinglong/cluster-control/dist -FROM node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d AS runtime-base +FROM node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436 AS runtime-base ARG SOURCE_REVISION=uncommitted diff --git a/deploy/containers/ql3-local-application/Dockerfile b/deploy/containers/ql3-local-application/Dockerfile index eab71c4d..9052d450 100644 --- a/deploy/containers/ql3-local-application/Dockerfile +++ b/deploy/containers/ql3-local-application/Dockerfile @@ -130,7 +130,7 @@ RUN rm -rf node_modules/.bin \ --exclude=@qinglong/ai \ && rm /tmp/ql3-prune-runtime-artifact.cjs -FROM node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d AS runtime +FROM node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436 AS runtime ARG SOURCE_REVISION=uncommitted diff --git a/deploy/containers/ql3-worker/Dockerfile b/deploy/containers/ql3-worker/Dockerfile index fad7ffe0..f4d20338 100644 --- a/deploy/containers/ql3-worker/Dockerfile +++ b/deploy/containers/ql3-worker/Dockerfile @@ -72,7 +72,7 @@ RUN chmod 0555 \ && rm -rf node_modules/.bin \ && find node_modules/@qinglong -type f -name '*.map' -delete -FROM node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d AS runtime +FROM node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436 AS runtime ARG SOURCE_REVISION=uncommitted diff --git a/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md b/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md index a057c7d3..66dfc499 100644 --- a/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md +++ b/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md @@ -4922,9 +4922,9 @@ timeline 1→2、fencing、`pg_rewind` 只读同步重入、双 fresh replicas 与总 gate,Docker HA 资源零残留;本机 OCI attested build 仍因 BuildKit SBOM scanner 网络超时未形成 live clean,GitHub-hosted 六矩阵及真实 GHCR 记录待取得; -- D-237/ADR-0254 已补齐三镜像的 base/OS high-critical 漏洞发布门,并修复 control/admin 仍可用 - `ARG NODE_IMAGE` 覆盖基础镜像的架构缺陷。control/admin/local build+runtime 统一固定 Node 24.18.0 Bookworm - multiarch digest;CI 复用六个 native build,release 在 publish 前另以只读六矩阵构建并用 Trivy 0.70.0 扫描 +- D-237/ADR-0254 已补齐五镜像的 base/OS high-critical 漏洞发布门,并修复 control/admin 仍可用 + `ARG NODE_IMAGE` 覆盖基础镜像的架构缺陷。四个 Dockerfile 的 build stage 固定 Node 24.18.0 Bookworm digest, + runtime stage 固定更小的 Node 24.18.0 Alpine 3.23 multiarch digest;CI/release 对五镜像双架构十个 native build 执行 Trivy 0.70.0 OS-only HIGH/CRITICAL,unfixed 不忽略、scanner cache 关闭。Action 固定事故后 v0.36.0 完整 commit;central exact-shape policy 当前零例外,任何例外必须绑定 CVE、OS purl、image、owner、ticket、理由且最长 30 天。 policy 8/8、image release 34/34、Cluster deployment 31/31;不增 package、runtime dependency 或设备常驻成本。 @@ -5594,7 +5594,7 @@ | D-234 | Worker management 客户端 CA 轮换必须由 caller-driven 三阶段证据证明,而不能只依赖提交态摘要或最终 rollout 截图。old、overlap、new 每阶段都读取相同 endpoint/server trust/inspect command 下的独立 old/new client identity,服务端 TLS trust CA 与客户端证书 issuer CA 必须显式分离;service-side trust 集合精确为 old、old+new、new,且每个 active issuer 恰有一份当前 CRL,真实访问矩阵固定为 old `[200,200,401]`、new `[401,200,200]`。collector 只能 get exact Deployment/list selected Pods;每阶段必须观察同一 Deployment UID、递增 generation、唯一 resourceVersion、两个跨 Node Ready/tokenless Pod、CA/CRL 摘要注解与相邻世代完整 Pod UID 替换。old/overlap state 和最终报告用 canonical `0600` no-replace 摘要链,最终报告再绑定 D-229 ceremony 与 D-230 durable audit,禁止原始证书/CA/CRL/JWT/key/Kube material/Secret/DSN/Pod/Node identity;D-232 同 CA 单证书吊销保持独立。不得新增 package、依赖、migration、controller、watcher、timer、sidecar、listener、Pool 或连接,Edge/Standalone/Worker 零稳态成本 | Accepted(ADR-0250:三阶段 runner/auditor 定向 8/8,覆盖 exact trust/access/CRL/三代六 Pod happy path、overlap 缺 old CA、generation 未变、Pod UID 复用、collector Secret read 扩权、ceremony identity 漂移、false/widened/sensitive/malformed report 与 exact CLI;顶层 evidence/audit 命令和完整运维 runbook 已落地。workspace package 与第三方依赖不变;生产外部 PKI/IdP/ingress 多节点联合报告待采集) | 只看 final 401/200 无法证明 overlap 可用、全部副本加载相同 trust 或旧 Pod 已退役;把 server CA 当 client issuer 会拒绝正确的双 PKI 拓扑;复用 D-232 会混淆证书吊销与 CA rollover;让 runner 自改 Secret/Deployment 会合并变更与证明 authority。三个只读 observation、摘要链、真实双 client 请求和独立离线重判提供最小完整证据,同时不给路由设备增加常驻成本 | | D-235 | Worker management 的 external ceremony、durable audit、同 issuer leaf revocation 与 client CA rollover 必须形成一个可独立重判的统一 release evidence,不能由人工核对四个 `passed` 布尔值。聚合器只接受 D-229 v1、D-230 v1、D-232 v2、D-234 v1 四份 canonical owner-private source,重跑原 validator、复算 durable→ceremony 及 PKI/CA→ceremony+durable 摘要链,并要求相同 external issuer/profile/operator、endpoint/server trust/inspect command、cluster/collector/Deployment UID。D-232/D-234 可在不同受审窗口执行,不伪造跨协议 generation 连续性;各自内部 rollout/Pod replacement 仍须成立。final 只含 source/identity/transport/deployment 摘要与低敏事实,以 `0600` no-replace+fsync 发布;独立 auditor 必须同时读取 final 和四份 source、重建 canonical report 后完全相等,拒绝 v1 PKI、source 替换、operator/transport/deployment 漂移、false/extra/sensitive 和时间倒置。不得新增 package、依赖、migration、服务、controller、timer、watcher、listener、Pool 或 Profile artifact | Accepted(ADR-0252:runner/source-aware auditor 7/7,覆盖完整聚合、摘要链断裂、operator/transport/Deployment 漂移、source false gate、final claimed drift/extra/false/sensitive、时间倒置与 exact CLI;顶层 evidence/audit 命令和生产归档 runbook 已完成。workspace 保持 19 个 QL3 package、零新依赖;真实 production 四 source + final 归档仍是发布门) | 单看最终 gate 不能证明 source 文件或跨报告作用域一致;把全部 source 嵌入 final 会复制事实和 schema;强求两个正交维护操作的 generation 连续会制造伪约束;另建 package/daemon 会增加路由器和发布面成本。source digest graph、共同 authority/transport/deployment binding 与必须携带 source 的离线重判形成最小可复验发布证明 | | D-236 | Worker management 的五份私密 release evidence 必须在镜像发布前完成 source-aware 重判,但不得上传到 GitHub artifact/cache,也不得与 registry/OIDC 写 authority 共处。唯一 image release workflow 只允许显式 dispatch 到与 version 完全一致的 protected `v3` tag;受 `ql3-production-release-evidence` environment 保护的 JIT self-hosted runner 从 commit-scoped `/run/qinglong3-release-evidence/$GITHUB_SHA` 固定名称挂载读取 final + 四 source,要求 canonical owner-private、D-235 audit 兼容、final 年龄不超过 24 小时且未来偏差不超过 5 分钟。顶层/evidence job 只有 contents read;publish 无条件 needs evidence,随后才在 GitHub-hosted runner 获得 GHCR/OIDC/attestation 写权限。禁止自动 tag push、用户路径、artifact/cache、source job output 与复用 runner;ephemeral 标签必须由外部 JIT provisioner 的一次注册/执行/销毁兑现。不增 package、依赖、migration、Profile artifact 或常驻资源 | Accepted(ADR-0253:release evidence gate 4 个新增场景与原 D-235 共 11/11;image release 静态/变异契约 26/26,覆盖 bypass、缺 dependency、persistent runner、artifact upload、非 commit mount、write authority 和 movable action。`audit:image-release:ql3` 返回 source-aware/private ephemeral/86400 秒/no artifact;真实 protected environment、JIT runner 与 evidence-backed GHCR 发布记录待取得) | 把 source 上传 artifact 扩大 custody/retention;只传 final 无法重判 source;publisher 同时读取证据与持有写 token 会合并安全域;自动 tag trigger 留下审批竞态;跨 workflow 查询成功状态产生 commit/tag 重放复杂度。同一 dispatch 内只读私密 pre-job 与强 needs 是最小失败关闭边界 | -| D-237 | 三个 production image 必须在发布前对 amd64/arm64 的真实 OS package 执行 HIGH/CRITICAL 扫描,且扫描对象必须由不可覆盖的 base digest 决定。control/admin/local 的 build/runtime stage 统一固定 exact Node 24.18.0 Bookworm multiarch digest,删除 control/admin `ARG NODE_IMAGE`;CI 复用六个 native image,release 另建只读六矩阵 candidate job,publisher 同时 needs D-236 evidence 与全部 scan。Trivy 固定 0.70.0,action 固定 v0.36.0 full commit;OS-only、unfixed 不忽略、exit 1、cache false,scanner/DB 不可用失败。central exact JSON policy 当前空;例外只允许唯一排序 CVE、image subset、OS purl、owner、ticket、理由与未来最多 30 天 expiry,并生成每 image 的 `0600` no-replace YAML;禁止裸 ignore、path skip、allow-all Rego、永久 VEX 或 library 漏洞混入。不增 package、runtime dependency、migration 或 Profile 稳态资源 | Accepted(ADR-0254:policy 8/8、image release contract 34/34、Cluster deployment 31/31;静态审计返回 exact scanner/action、双架构、HIGH/CRITICAL、OS-only、ignoreUnfixed=false、30-day maximum,当前 exceptionCount=0。本机官方 scanner 下载无进展后中止并清理,未声称 live clean;GitHub-hosted 六矩阵和 evidence-backed GHCR 发布记录待取得) | 只扫 amd64 不能证明 arm64;发布后扫已经污染 registry;`ignore-unfixed` 是无 owner/expiry 的全局例外;直接维护 Trivy YAML 缺 image scope 与最长周期;mutable base/action tag 已有真实供应链风险。digest-pinned native preflight 与严格生成式例外把构建身份、漏洞判断和风险接受闭合在 publish 前 | +| D-237 | 五个 production image 必须在发布前对 amd64/arm64 的真实 OS package 执行 HIGH/CRITICAL 扫描,且扫描对象必须由不可覆盖的 base digest 决定。四个 Dockerfile 的 build stage 固定 exact Node 24.18.0 Bookworm multiarch digest,runtime stage 固定 exact Node 24.18.0 Alpine 3.23 multiarch digest,删除 control/admin `ARG NODE_IMAGE`;CI/release 对 control/control-ai/admin/local/worker 执行十个 native scan,publisher 同时 needs D-236 evidence 与全部 scan。Trivy 固定 0.70.0,action 固定 v0.36.0 full commit;OS-only、unfixed 不忽略、exit 1、cache false,scanner/DB 不可用失败。central exact JSON policy 当前空;例外只允许唯一排序 CVE、image subset、OS purl、owner、ticket、理由与未来最多 30 天 expiry,并生成每 image 的 `0600` no-replace YAML;禁止裸 ignore、path skip、allow-all Rego、永久 VEX 或 library 漏洞混入。不增 package、runtime dependency、migration 或 Profile 稳态资源 | Accepted(ADR-0254:2026-08-22 同版 Bookworm/Trixie/Distroless 对比后,固定 Alpine multiarch digest 在 Trivy 0.70.0 同库扫描下 amd64/arm64 均为 0 HIGH/CRITICAL;静态/负向审计继续约束双基座 exact digest、双架构、OS-only、ignoreUnfixed=false 与 30-day maximum,当前 exceptionCount=0;完整十镜像远端扫描和 evidence-backed GHCR 发布记录仍是合并门) | 只扫 amd64 不能证明 arm64;发布后扫已经污染 registry;`ignore-unfixed` 是无 owner/expiry 的全局例外;直接维护 Trivy YAML 缺 image scope 与最长周期;把完整 Bookworm 工具链带入运行镜像既扩大路由器体积又放大攻击面。digest-pinned split build/runtime、native preflight 与严格生成式例外把构建身份、漏洞判断和风险接受闭合在 publish 前 | | D-238 | Release 漏洞门必须证明“被扫描的 OCI graph 就是被发布的 OCI graph”,相同 source 的二次 build 不得代替 artifact identity。六个 native job 各自只 build 一次带 SBOM/provenance 的 OCI layout tar,Trivy 对 exact tar 执行 D-237 gate;成功后解包、完整审计并生成绑定 platform/source/native root/manifest/config/attestation 的 evidence,以 run ID + attempt + image + architecture 唯一命名、1-day、no-overwrite artifact 传给 publisher。publisher 禁止 QEMU/Buildx/Dockerfile/rebuild,只能重验两份 native graph、确定性构造双架构 index 和 final OS vulnerability predicate;checksum-pinned regctl 只按 `IMAGE@DIGEST` import。远端 digest 的 Cosign、SLSA、CycloneDX、OS vulnerability、manifest 与 rollout 全部通过后,version/完整 commit tag 才作为最后一步创建。私密 D-236 evidence 不进入 artifact;不增 package、runtime dependency、migration 或 Profile 稳态资源 | Accepted(ADR-0255:OCI/release 定向 57/57;workflow YAML 与 repository audit 全绿,输出 `buildOnce=true`、`attestedToPublishedDigest=true`、`rebuildAfterScan=false`、`tagAfterVerification=true`。PostgreSQL 18.4 arm64 HA 同轮重跑 `gates.passed=true` 且 Docker HA 容器/卷/网络零残留;本机 OCI attested build 因 BuildKit SBOM scanner 网络超时未取得 live clean,GitHub-hosted 六矩阵、regctl→GHCR exact-digest import 与最终 tag 记录待取得) | 扫描一次后按相同 source 再 build 仍允许构建环境或依赖漂移;candidate tag 在 gate 前污染 registry且可移动;publisher 保留 build authority 会重新打开替换面;提前打 tag 会把未完成证明的 digest 暴露给部署者。build-once、同 run immutable graph、repository merger、digest-only import 和 tag-last 将漏洞判断与最终发布身份闭合 | | D-239 | Fresh Edge/Standalone 必须提供受支持的 TaskDefinition create/update/enable/disable/inspect/list 产品入口,不能要求部署者直接写 SQLite 或调用 Repository。既有 `ql3-task` binary 只从 deployment root 内当前 UID `0600` 非 symlink command file 接受 `task.put|task.inspect|task.list`;put 以 `expectedRevision=null|current` 区分 create/update,enable/disable 都追加 immutable revision,不原地修改或删除。每次命令只建立短生命周期进程和单 SQLite authority,要求 strong User 与 `task.create|task.update|task.read` Policy;写事务内重新验证 credential/Identity/pepper、Project/RoleBinding fence,原子提交 allowed audit、Task head/revision、mutation replay 和适用的 local execution revision。exact replay 绑定 mutation event、request、actor、fence 与 immutable occurredAt,semantic/audit/revision 漂移失败关闭;CLI 只返回有界 current-head 元数据,不回显 spec/config/command/SecretRef/credential/path。实现只增加现有四包 subpath,workspace 保持 19,不增依赖、migration、daemon、timer、watcher、listener、Pool 或端口。Cluster 不复用本机 credential/SQLite CLI,必须后续提供 PostgreSQL/RBAC/多副本管理 transport | Accepted(ADR-0256:真实 SQLite/Owner CLI 5/5 覆盖 create/exact replay/disable/inspect/list、operator allow/viewer deny、credential/Policy race 原子回滚、Task/audit semantic drift、private command file 与低敏输出;Owner CLI closure strict TypeScript 通过。PostgreSQL 18.4 arm64 HA 同轮 `gates.passed=true`,完成 remote_apply、timeline 1→2、old-primary fence/rewind/read-only rejoin、双 fresh control,Docker 容器/网络/卷零残留;该证据只声明 Cluster 基线未回归) | 新建单用途 package 会反转 D-207 的 19 包收敛;把写 authority 暴露给常驻 application 会增加低配设备空闲成本;直接 SQL 绕过认证、Policy、audit、semantic registry 与 replay;原地 enabled bool 会破坏历史;把 SQLite command file 当集群协议会丢失 PostgreSQL HA/RBAC fence。既有短生命周期 CLI、事务内复验与 append-only revision 是当前最小可用且可审计闭环 | diff --git a/docs/adr/ADR-0128-exact-cluster-image-sbom-and-attested-multiarch-release.md b/docs/adr/ADR-0128-exact-cluster-image-sbom-and-attested-multiarch-release.md index eb9b9e51..f3201f71 100644 --- a/docs/adr/ADR-0128-exact-cluster-image-sbom-and-attested-multiarch-release.md +++ b/docs/adr/ADR-0128-exact-cluster-image-sbom-and-attested-multiarch-release.md @@ -11,8 +11,9 @@ > 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。 +> `NODE_IMAGE` build arg,并增加跨架构 OS vulnerability pre-publish gate。2026-08-22 的 ADR-0254 修订进一步把 +> build stage 保留在固定 Bookworm digest、runtime stage 切到零 HIGH/CRITICAL 的固定 Alpine 3.23 digest,覆盖 +> `control|control-ai|admin|local|worker` 十个 native image。 ## 背景 diff --git a/docs/adr/ADR-0254-digest-pinned-cross-architecture-os-vulnerability-release-gate.md b/docs/adr/ADR-0254-digest-pinned-cross-architecture-os-vulnerability-release-gate.md index e077db32..46d50d9b 100644 --- a/docs/adr/ADR-0254-digest-pinned-cross-architecture-os-vulnerability-release-gate.md +++ b/docs/adr/ADR-0254-digest-pinned-cross-architecture-os-vulnerability-release-gate.md @@ -19,18 +19,19 @@ commit SHA。D-237 不能用 movable `@v0.x` 或 `latest` 引入一个拥有 wor ## 决策 -1. control、admin、local 的 build/runtime stage 统一固定 - `node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d`。 - control/admin 删除 `ARG NODE_IMAGE`,不得允许 build arg 覆盖 base;三个镜像继续拥有独立 Dockerfile、应用闭包和 - 最终 image digest。 +1. control、control-ai、admin、local、worker 的 dependency/build stage 固定 + `node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d`,最终 runtime stage 固定更小的 + `node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436`。 + control/admin 删除 `ARG NODE_IMAGE`,所有 runtime base 都不得由 build arg 覆盖;四个 Dockerfile 继续拥有独立应用闭包和 + 最终 image digest。构建与运行基座分离,避免把编译工具链和 Bookworm 的 Perl/util-linux/ncurses 包带入低配设备与集群运行镜像。 2. scanner 固定为 Trivy `v0.70.0`,GitHub Action 固定完整 commit `ed142fd0673e97e23eac54620cfb913e5ce36c25`(action `v0.36.0`)。禁止 tag、`master`、`latest`、 `continue-on-error` 和 scanner cache;网络、release asset 或 vulnerability DB 不可用均失败关闭。 3. 只扫描 `vuln-type=os`、`scanners=vuln`、severity `HIGH,CRITICAL`,`ignore-unfixed=false`、exit code 1。 npm/library advisory 继续由既有 production dependency audit 独立负责,不能用 OS 例外隐藏应用依赖漏洞。 -4. `.github/workflows/ql3-ci.yml` 复用已经存在的 native build:control/admin 各 amd64+arm64,local - amd64+arm64,共六个真实本地 image。每个 build 后生成 image-scoped ignore view 并执行 Trivy。 -5. release workflow 增加独立只读 `os-vulnerability` 六矩阵 job,在发布前用 exact Dockerfile、base digest、lock 和 +4. `.github/workflows/ql3-ci.yml` 复用已经存在的 native build:control、control-ai、admin、local、worker + 各 amd64+arm64,共十个真实本地 image。每个 build 后生成 image-scoped ignore view 并执行 Trivy。 +5. release workflow 增加独立只读 `os-vulnerability` 十矩阵 job,在发布前用 exact Dockerfile、base digest、lock 和 source revision 构建 native candidate。`publish` 必须同时依赖 D-236 private evidence 与全部 OS scan 成功, 才能获得 GHCR/OIDC/attestation 写权限。scan job 不读取 D-236 私密 source,不参与签名或 publish。 6. 唯一例外事实源为 `deploy/containers/ql3-os-vulnerability-exceptions.json`。顶层和每条 entry 都是 exact shape; @@ -83,9 +84,9 @@ commit SHA。D-237 不能用 movable `@v0.x` 或 `latest` 引入一个拥有 wor - image release contract 34/34:新增覆盖 CI/release action pin、六矩阵、publish dependency、unfixed、OS-only、 cache 与既有签名/SBOM/provenance 门; - Cluster deployment 31/31,包含 control/admin mutable/overridable base 负向测试; -- 本机 arm64 exact Dockerfile build 已通过:control `sha256:58ca2905c6489d3051663804f07adb0ea24dce734cc96863f882c8e5cb73579c`、 - admin `sha256:79f8eda7194cf63669ba144917eabecd11faa0546858ac1feb7ee52ccffc5f27`,均为 arm64、 - `10001:10001`;验证 image 已删除; +- 2026-08-22 CI 的 Bookworm runtime 扫描在 amd64/arm64 同时发现 30 个 HIGH/CRITICAL;同版 Trixie Slim 为 51 个, + Debian 13 distroless 为 1 个无修复 HIGH。固定的 Alpine 3.23 multiarch digest 使用同一 Trivy 0.70.0 与同一数据库扫描, + amd64/arm64 均为 0 HIGH/CRITICAL,且 Node 版本仍为 24.18.0;完整十镜像远端扫描仍是合并门; - `audit:image-release:ql3` 返回 Trivy 0.70.0、exact action commit、amd64/arm64、OS-only、HIGH/CRITICAL、 `ignoreUnfixed=false`、maximum exception 30 days; - 当前 policy `exceptionCount=0`;workspace 保持 19 个 package,无新增 runtime dependency。 diff --git a/docs/operations/ql3-image-os-vulnerability-gate.md b/docs/operations/ql3-image-os-vulnerability-gate.md index 96403f22..3c9055af 100644 --- a/docs/operations/ql3-image-os-vulnerability-gate.md +++ b/docs/operations/ql3-image-os-vulnerability-gate.md @@ -1,6 +1,6 @@ # QingLong 3.0 镜像 OS 漏洞门 -本流程覆盖 `control|admin|local × amd64|arm64` 六个 release candidate 的 Debian/base image package,不替代 npm +本流程覆盖 `control|control-ai|admin|local|worker × amd64|arm64` 十个 release candidate 的 OS/base image package,不替代 npm production dependency audit、CycloneDX、BuildKit SBOM/provenance、Cosign 或 GitHub attestation。 ## 日常检查 @@ -17,7 +17,7 @@ pnpm audit:cluster-deployment:ql3 {"schemaVersion":1,"fixture":"qinglong/image-os-vulnerability-exceptions@v1","compatible":true,"exceptionCount":0,"imageExceptionCounts":{"admin":0,"control":0,"local":0}} ``` -真实 scan 在 QL3 CI 已有六个 native build 后执行;受保护 release 的六个 native job 各自只构建一次 OCI layout tar, +真实 scan 在 QL3 CI 已有十个 native build 后执行;受保护 release 的十个 native job 各自只构建一次 OCI layout tar, Trivy 直接扫描该 tar。扫描成功后才解包、审计并上传同一 OCI graph;publisher 只合并 amd64/arm64 graph,不再 build。 只有六份 artifact 和 D-236 私密 evidence job 全部成功,publisher 才会启动。scanner/DB 网络故障与漏洞命中使用相同 的失败关闭结果,但日志会区分下载错误和 finding。 @@ -80,8 +80,8 @@ pnpm audit:image-release:ql3 1. 阅读 Trivy 官方 release 与 security advisory,确认目标版本不在已知暴露窗口; 2. 解析 signed/immutable action release 到完整 commit SHA,审查 composite action 的所有 nested action pin; 3. 固定 scanner exact version,禁止 `latest`;保持 cache false、OS-only、unfixed 不忽略; -4. 对 Node base 的多架构 manifest digest 执行签名/来源核验;三个 production Dockerfile 必须同步更新 exact digest; -5. 更新 ADR、静态审计 expectation 与 mutation tests;执行六个 native scan 后才能发布; +4. 对 Node base 的多架构 manifest digest 执行签名/来源核验;四个 production Dockerfile 的 runtime stage 必须同步更新 exact digest; +5. 更新 ADR、静态审计 expectation 与 mutation tests;执行十个 native scan 后才能发布; 6. 修复后删除已不需要的 exceptions,不等待 `expiresOn`。 ## 失败恢复 diff --git a/scripts/ql3-cluster-deployment-audit.cjs b/scripts/ql3-cluster-deployment-audit.cjs index d213b422..812f5378 100644 --- a/scripts/ql3-cluster-deployment-audit.cjs +++ b/scripts/ql3-cluster-deployment-audit.cjs @@ -517,8 +517,10 @@ function assertExactExternalClosure(readFile, root, findings) { } function assertDockerfile(readFile, root, findings) { - const pinnedNodeBase = + const pinnedBuildNodeBase = 'node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d'; + const pinnedRuntimeNodeBase = + 'node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436'; const dockerfile = readFile( path.join(root, 'deploy/containers/ql3-cluster-control/Dockerfile'), 'utf8', @@ -596,11 +598,16 @@ function assertDockerfile(readFile, root, findings) { ), ); } - if (dockerfile.split(`FROM ${pinnedNodeBase}`).length - 1 !== 2) { + if ( + !dockerfile.includes( + `FROM ${pinnedBuildNodeBase} AS dependency-manifest`, + ) || + !dockerfile.includes(`FROM ${pinnedRuntimeNodeBase} AS runtime-base`) + ) { findings.push( finding( 'QL3_CLUSTER_DOCKERFILE_BASE_IMAGE_NOT_PINNED', - 'Cluster control build and runtime stages must use the exact immutable Node base digest', + 'Cluster control build and runtime stages must use their exact immutable Node base digests', ), ); } @@ -652,11 +659,16 @@ function assertDockerfile(readFile, root, findings) { ); } } - if (adminDockerfile.split(`FROM ${pinnedNodeBase}`).length - 1 !== 2) { + if ( + !adminDockerfile.includes( + `FROM ${pinnedBuildNodeBase} AS dependency-manifest`, + ) || + !adminDockerfile.includes(`FROM ${pinnedRuntimeNodeBase} AS runtime`) + ) { findings.push( finding( 'QL3_CLUSTER_ADMIN_DOCKERFILE_BASE_IMAGE_NOT_PINNED', - 'Cluster admin build and runtime stages must use the exact immutable Node base digest', + 'Cluster admin build and runtime stages must use their exact immutable Node base digests', ), ); } diff --git a/scripts/ql3-local-image-audit.cjs b/scripts/ql3-local-image-audit.cjs index 747c1c31..eaffdb48 100644 --- a/scripts/ql3-local-image-audit.cjs +++ b/scripts/ql3-local-image-audit.cjs @@ -6,8 +6,10 @@ const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs'); const IMAGE_DIRECTORY = 'deploy/containers/ql3-local-application'; const QL3_VERSION = readReleaseIdentity(path.resolve(__dirname, '..')).version; -const NODE_IMAGE = +const BUILD_NODE_IMAGE = 'node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d'; +const RUNTIME_NODE_IMAGE = + 'node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436'; const BUILD_DEPENDENCIES = Object.freeze({ croner: '7.0.8', 'drizzle-orm': '1.0.0-rc.4', @@ -169,14 +171,26 @@ function counts(values) { } function auditDockerfile(contents, findings) { - const exactBasePattern = new RegExp( - `^FROM ${NODE_IMAGE.replace( - /[.*+?^${}()|[\]\\]/g, - '\\$&', - )} AS (?:dependency-manifest|runtime)$`, + const escapedBuildNodeImage = BUILD_NODE_IMAGE.replace( + /[.*+?^${}()|[\]\\]/g, + '\\$&', + ); + const escapedRuntimeNodeImage = RUNTIME_NODE_IMAGE.replace( + /[.*+?^${}()|[\]\\]/g, + '\\$&', + ); + const exactBuildBasePattern = new RegExp( + `^FROM ${escapedBuildNodeImage} AS dependency-manifest$`, 'gm', ); - if ([...contents.matchAll(exactBasePattern)].length !== 2) { + const exactRuntimeBasePattern = new RegExp( + `^FROM ${escapedRuntimeNodeImage} AS runtime$`, + 'gm', + ); + if ( + [...contents.matchAll(exactBuildBasePattern)].length !== 1 || + [...contents.matchAll(exactRuntimeBasePattern)].length !== 1 + ) { addFinding(findings, 'BASE_IMAGE_NOT_EXACTLY_PINNED'); } if (/(?:^|\n)\s*ARG\s+NODE_IMAGE\b/.test(contents)) { @@ -323,7 +337,8 @@ function auditLocalImageContract(root) { return Object.freeze({ schemaVersion: 1, image: 'local-application', - nodeImage: NODE_IMAGE, + nodeImage: RUNTIME_NODE_IMAGE, + buildNodeImage: BUILD_NODE_IMAGE, runtimePackages: Object.freeze( [ ...RUNTIME_PACKAGES.map((name) => `@qinglong/${name.slice(4)}`), diff --git a/scripts/ql3-worker-deployment-audit.cjs b/scripts/ql3-worker-deployment-audit.cjs index a0b317db..d308cf91 100644 --- a/scripts/ql3-worker-deployment-audit.cjs +++ b/scripts/ql3-worker-deployment-audit.cjs @@ -78,6 +78,7 @@ const dockerfile = fs.readFileSync( ); for (const required of [ 'node:24.18.0-bookworm-slim@sha256:', + 'node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436', 'npm ci --omit=dev --ignore-scripts', 'packages/ql3-runtime-core', 'packages/ql3-local-process', diff --git a/test/back/ql3ClusterDeploymentAudit.test.cjs b/test/back/ql3ClusterDeploymentAudit.test.cjs index a837e55c..69b3bfc5 100644 --- a/test/back/ql3ClusterDeploymentAudit.test.cjs +++ b/test/back/ql3ClusterDeploymentAudit.test.cjs @@ -409,13 +409,15 @@ test('keeps Prompt output active-key rotation caller-driven and least privilege' }); test('rejects mutable or overridable Cluster image bases', () => { - const digest = + const buildDigest = '@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d'; + const runtimeDigest = + '@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436'; const control = auditClusterDeployment({ root: ROOT, readFile: intercept( 'deploy/containers/ql3-cluster-control/Dockerfile', - (source) => source.replace(digest, ''), + (source) => source.replace(buildDigest, ''), ), }); assert.equal(control.compatible, false); @@ -432,7 +434,7 @@ test('rejects mutable or overridable Cluster image bases', () => { 'deploy/containers/ql3-cluster-admin/Dockerfile', (source) => `ARG NODE_IMAGE=node:24.18.0-bookworm-slim\n${source.replaceAll( - `node:24.18.0-bookworm-slim${digest}`, + `node:24.18.0-bookworm-slim${buildDigest}`, '${NODE_IMAGE}', )}`, ), @@ -445,6 +447,21 @@ test('rejects mutable or overridable Cluster image bases', () => { ), true, ); + + const runtimeControl = auditClusterDeployment({ + root: ROOT, + readFile: intercept( + 'deploy/containers/ql3-cluster-control/Dockerfile', + (source) => source.replace(runtimeDigest, ''), + ), + }); + assert.equal(runtimeControl.compatible, false); + assert.equal( + runtimeControl.findings.some( + ({ code }) => code === 'QL3_CLUSTER_DOCKERFILE_BASE_IMAGE_NOT_PINNED', + ), + true, + ); }); test('keeps authenticated Plugin Package management optional and bounded', () => { diff --git a/test/back/ql3LocalImageAudit.test.cjs b/test/back/ql3LocalImageAudit.test.cjs index 08320311..aa09bc55 100644 --- a/test/back/ql3LocalImageAudit.test.cjs +++ b/test/back/ql3LocalImageAudit.test.cjs @@ -43,6 +43,14 @@ test('accepts the exact AI-excluded local application image contract', () => { const report = auditLocalImageContract(root); assert.equal(report.compatible, true); assert.deepEqual(report.findings, []); + assert.equal( + report.nodeImage, + 'node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436', + ); + assert.equal( + report.buildNodeImage, + 'node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d', + ); assert.deepEqual(report.runtimePackages, [ '@qinglong/local-admin', '@qinglong/local-application', @@ -57,6 +65,30 @@ test('accepts the exact AI-excluded local application image contract', () => { ]); }); +test('rejects a mutable runtime base image', () => { + const current = fixture(); + try { + const dockerfilePath = path.join(current.target, 'Dockerfile'); + const dockerfile = fs.readFileSync(dockerfilePath, 'utf8'); + fs.writeFileSync( + dockerfilePath, + dockerfile.replace( + '@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436', + '', + ), + ); + const report = auditLocalImageContract(current.root); + assert.equal(report.compatible, false); + assert.ok( + report.findings.some( + ({ code }) => code === 'BASE_IMAGE_NOT_EXACTLY_PINNED', + ), + ); + } finally { + current.close(); + } +}); + test('rejects a mutable or build-argument-controlled base image', () => { const current = fixture(); try {