fix(images): minimize ql3 runtime base

This commit is contained in:
whyour
2026-08-22 12:44:49 +08:00
parent ca8ddf34b8
commit f1cca9a3ed
13 changed files with 119 additions and 40 deletions
@@ -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
@@ -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
@@ -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
+1 -1
View File
@@ -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
+4 -4
View File
@@ -4922,9 +4922,9 @@
timeline 1→2、fencing、`pg_rewind` 只读同步重入、双 fresh replicas 与总 gateDocker HA 资源零残留;本机 OCI
attested build 仍因 BuildKit SBOM scanner 网络超时未形成 live cleanGitHub-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 digestCI 复用六个 native buildrelease 在 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 digestCI/release 对五镜像双架构十个 native build 执行 Trivy 0.70.0
OS-only HIGH/CRITICALunfixed 不忽略、scanner cache 关闭。Action 固定事故后 v0.36.0 完整 commitcentral
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 identityD-232 同 CA 单证书吊销保持独立。不得新增 package、依赖、migration、controller、watcher、timer、sidecar、listener、Pool 或连接,Edge/Standalone/Worker 零稳态成本 | AcceptedADR-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 | AcceptedADR-0252runner/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 readpublish 无条件 needs evidence,随后才在 GitHub-hosted runner 获得 GHCR/OIDC/attestation 写权限。禁止自动 tag push、用户路径、artifact/cache、source job output 与复用 runnerephemeral 标签必须由外部 JIT provisioner 的一次注册/执行/销毁兑现。不增 package、依赖、migration、Profile artifact 或常驻资源 | AcceptedADR-0253release evidence gate 4 个新增场景与原 D-235 共 11/11image 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 无法重判 sourcepublisher 同时读取证据与持有写 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 imagerelease 另建只读六矩阵 candidate jobpublisher 同时 needs D-236 evidence 与全部 scan。Trivy 固定 0.70.0action 固定 v0.36.0 full commitOS-only、unfixed 不忽略、exit 1、cache falsescanner/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 稳态资源 | AcceptedADR-0254policy 8/8、image release contract 34/34、Cluster deployment 31/31;静态审计返回 exact scanner/action、双架构、HIGH/CRITICAL、OS-only、ignoreUnfixed=false30-day maximum,当前 exceptionCount=0。本机官方 scanner 下载无进展后中止并清理,未声称 live cleanGitHub-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 digestruntime 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 scanpublisher 同时 needs D-236 evidence 与全部 scan。Trivy 固定 0.70.0action 固定 v0.36.0 full commitOS-only、unfixed 不忽略、exit 1、cache falsescanner/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 稳态资源 | AcceptedADR-02542026-08-22 同版 Bookworm/Trixie/Distroless 对比后,固定 Alpine multiarch digest 在 Trivy 0.70.0 同库扫描下 amd64/arm64 均为 0 HIGH/CRITICAL;静态/负向审计继续约束双基座 exact digest、双架构、OS-only、ignoreUnfixed=false30-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 tarTrivy 对 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 predicatechecksum-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 稳态资源 | AcceptedADR-0255OCI/release 定向 57/57workflow 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 cleanGitHub-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/updateenable/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 occurredAtsemantic/audit/revision 漂移失败关闭;CLI 只返回有界 current-head 元数据,不回显 spec/config/command/SecretRef/credential/path。实现只增加现有四包 subpathworkspace 保持 19,不增依赖、migration、daemon、timer、watcher、listener、Pool 或端口。Cluster 不复用本机 credential/SQLite CLI,必须后续提供 PostgreSQL/RBAC/多副本管理 transport | AcceptedADR-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 controlDocker 容器/网络/卷零残留;该证据只声明 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 是当前最小可用且可审计闭环 |
@@ -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。
## 背景
@@ -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 buildcontrol/admin 各 amd64+arm64local
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 buildcontrol、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。
@@ -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 graphpublisher 只合并 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`
## 失败恢复
+17 -5
View File
@@ -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',
),
);
}
+23 -8
View File
@@ -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)}`),
+1
View File
@@ -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',
+20 -3
View File
@@ -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', () => {
+32
View File
@@ -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 {