fix(security): patch Node Rust and isolated Go service runtimes
This commit is contained in:
parent
1b6425b552
commit
eba7ef55e6
@ -1,13 +1,13 @@
|
||||
# 基础镜像(使用 Alpine,手动处理 OpenSSL 兼容性)
|
||||
# 2026-08-12 通过 `docker buildx imagetools inspect node:18-alpine` 核验的
|
||||
# 2026-10-22 核验 Node.js 安全更新,固定 Docker Hub 官方多架构清单。
|
||||
# Docker Hub 官方多架构清单 digest。生产默认值必须是不可变引用;本地测试
|
||||
# 如需改镜像源,仍可显式传入 BASE_IMAGE,但发布脚本不得覆盖此默认值。
|
||||
ARG BASE_IMAGE=node:18-alpine@sha256:8d6421d663b4c28fd3ebc498332f249011d118945588d0a35cb9bc4b8ca09d9e
|
||||
ARG BASE_IMAGE=node:22.23.4-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914
|
||||
ARG ALPINE_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/alpine
|
||||
# Local/CI builds remain possible without Git metadata. Production deployment
|
||||
# supplies the exact SHA and rejects this sentinel before any container starts.
|
||||
ARG VCS_REF=0000000000000000000000000000000000000000
|
||||
ARG DETAIL_RENDERER_IMAGE=node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402
|
||||
ARG DETAIL_RENDERER_IMAGE=node:22.23.4-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914
|
||||
FROM ${BASE_IMAGE} AS base
|
||||
|
||||
# Fabric's Node renderer has a separate dependency tree and Node runtime.
|
||||
@ -26,7 +26,7 @@ RUN node -e "require('canvas').createCanvas(1,1); console.log('isolated Canvas s
|
||||
FROM base AS deps
|
||||
ARG ALPINE_MIRROR
|
||||
RUN sed -i "s|https://dl-cdn.alpinelinux.org/alpine|${ALPINE_MIRROR}|g" /etc/apk/repositories && \
|
||||
apk add --no-cache libc6-compat python3 make g++ cairo-dev jpeg-dev pango-dev giflib-dev
|
||||
apk add --no-cache libc6-compat python3 make g++ cairo-dev jpeg-dev pango-dev giflib-dev openssl
|
||||
WORKDIR /app
|
||||
|
||||
# package-lock.json 是生产构建的必需输入,缺失时 COPY 直接失败。
|
||||
|
||||
@ -1,21 +1,15 @@
|
||||
# 由壹点3D素材 - 基础镜像(包含所有依赖)
|
||||
# 用途:一次性构建,后续部署直接使用,零安装、零复制
|
||||
# 旧基础镜像入口仍可使用;依赖必须由当前锁文件在同一 Node ABI 下重建。
|
||||
# 不再导入历史 Node 18 的 APK / node_modules 缓存。
|
||||
|
||||
ARG BASE_IMAGE=node:18-alpine
|
||||
ARG BASE_IMAGE=node:22.23.4-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制已解压的 Alpine 包并安装(一次性)
|
||||
COPY cache/alpine-apk /tmp/alpine-apk
|
||||
RUN echo "✅ 安装系统包(一次性)" && \
|
||||
apk add --no-network --allow-untrusted /tmp/alpine-apk/*.apk && \
|
||||
rm -rf /tmp/alpine-apk
|
||||
|
||||
# 复制已解压的 node_modules(一次性)
|
||||
COPY cache/node_modules ./node_modules
|
||||
RUN echo "✅ node_modules 已就绪" && \
|
||||
chmod -R +x node_modules/.bin
|
||||
RUN apk add --no-cache libc6-compat python3 make g++ cairo-dev jpeg-dev pango-dev giflib-dev openssl
|
||||
COPY package.json package-lock.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm_config_build_from_source=canvas npm_config_nodedir=/usr/local npm ci --no-audit --no-fund --legacy-peer-deps && \
|
||||
node -e "require('canvas').createCanvas(1,1); require('sharp'); console.log('native ABI verified')"
|
||||
|
||||
# 复制 Prisma schema 并生成 Client(一次性)
|
||||
COPY prisma ./prisma
|
||||
@ -26,6 +20,6 @@ RUN echo "✅ 生成 Prisma Client(一次性)" && \
|
||||
# 标记为基础镜像
|
||||
LABEL purpose="youyidian-3d-base" \
|
||||
description="包含所有依赖的基础镜像" \
|
||||
version="1.0"
|
||||
version="2.0-node22"
|
||||
|
||||
CMD ["echo", "这是基础镜像,不应该直接运行"]
|
||||
|
||||
@ -35,6 +35,10 @@
|
||||
|
||||
## 验证与本地打包
|
||||
|
||||
Pi 的项目运行时固定为 Node 24.22.1 或同一 24.x 线的后续安全版本。
|
||||
开发时先安装已校验官方 SHA-256 的 Node 到 `runtime/`;不借用全局 Node。
|
||||
主进程、独立 SDK 入口及打包工具都校验版本,旧版本不会启动模型桥。
|
||||
|
||||
```powershell
|
||||
cd apps/detail-agent-pi
|
||||
npm ci --ignore-scripts
|
||||
|
||||
2
apps/detail-agent-pi/package-lock.json
generated
2
apps/detail-agent-pi/package-lock.json
generated
@ -16,7 +16,7 @@
|
||||
"quickjs-emscripten": "0.32.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.16.0"
|
||||
"node": ">=24.22.1 <25"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"verify:live": "node scripts/acceptance.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.16.0"
|
||||
"node": ">=24.22.1 <25"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-agent-core": "1.3.0",
|
||||
|
||||
@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { assertPiNodeVersion } from '../src/runtime-policy.mjs';
|
||||
|
||||
// A local portable build. Never copy a profile, credential, project or request journal.
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
@ -11,7 +12,7 @@ if (!binaryArg || !destinationArg) throw Error('指定已验证的原生程序
|
||||
const destination = path.resolve(destinationArg), binary = path.resolve(binaryArg), node = path.resolve(nodeArg);
|
||||
if (destination === root || destination.startsWith(root + path.sep) || !path.isAbsolute(binary)) throw Error('输出目录必须独立于源码');
|
||||
const version = execFileSync(node, ['--version'], { encoding: 'utf8', windowsHide: true }).trim();
|
||||
if (!/^v(?:2[4-9]|[3-9]\d)\./.test(version)) throw Error('本地运行组件需要 Node 24 或以上');
|
||||
assertPiNodeVersion(version);
|
||||
await fs.mkdir(path.dirname(destination), { recursive: true });
|
||||
await fs.mkdir(destination); // Refuse to overwrite an existing client or user data.
|
||||
await fs.copyFile(binary, path.join(destination, 'photocraft.exe'));
|
||||
|
||||
@ -13,6 +13,7 @@ import { auditResponse } from './response-audit.mjs';
|
||||
import { projectContext, promptChars } from './context-budget.mjs';
|
||||
import { taskFor } from './rule-context.mjs';
|
||||
import { responseUsage } from './usage.mjs';
|
||||
import { assertPiNodeVersion } from './runtime-policy.mjs';
|
||||
|
||||
const SYSTEM = `你是由壹点详情页的本地 Agent。使用中文。先看当前上下文;只有用户要求修改画布时才读取和操作画布。
|
||||
只信当前注入的店铺/品类/品牌规则,不沿用另一店铺或旧规则;品牌规范不是产品参数的证据。
|
||||
@ -61,6 +62,7 @@ function toolsFor(host) {
|
||||
/** Coding Agent SDK owns the transcript/compaction; our host owns all I/O boundaries. */
|
||||
export class DetailPiAgent {
|
||||
static async open({ store, broker, canvas, transport, protocolMode = 'native-tools', callBudget = 12, onEvent = () => {} }) {
|
||||
assertPiNodeVersion(process.versions.node);
|
||||
if (!['native-tools', 'structured-json'].includes(protocolMode)) throw Error('工具协议模式无效');
|
||||
if (!Number.isInteger(callBudget) || callBudget < 1 || callBudget > 12) throw Error('单轮模型调用预算无效');
|
||||
const host = new DetailPiAgent();
|
||||
|
||||
@ -5,6 +5,9 @@ import { ProjectStore, atomic } from './store.mjs';
|
||||
import { StdioCanvas } from './stdio-canvas.mjs';
|
||||
import path from 'node:path';
|
||||
import { resolveRules } from './rules.mjs';
|
||||
import { assertPiNodeVersion } from './runtime-policy.mjs';
|
||||
|
||||
assertPiNodeVersion(process.versions.node);
|
||||
|
||||
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
||||
let initial, serial = 0, closed = false;
|
||||
|
||||
7
apps/detail-agent-pi/src/runtime-policy.mjs
Normal file
7
apps/detail-agent-pi/src/runtime-policy.mjs
Normal file
@ -0,0 +1,7 @@
|
||||
/** Pi's supported Node 24 line must include the 2026-10-21 security patches. */
|
||||
export function assertPiNodeVersion(version) {
|
||||
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(version || '');
|
||||
if (!match || Number(match[1]) !== 24 || Number(match[2]) < 22 || (Number(match[2]) === 22 && Number(match[3]) < 1)) {
|
||||
throw Error('Agent运行组件需要Node 24.22.1或更新的24.x安全版本,请更新完整客户端');
|
||||
}
|
||||
}
|
||||
10
apps/detail-agent-pi/test/runtime-policy.test.mjs
Normal file
10
apps/detail-agent-pi/test/runtime-policy.test.mjs
Normal file
@ -0,0 +1,10 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { assertPiNodeVersion } from '../src/runtime-policy.mjs';
|
||||
|
||||
test('client packaging and startup reject unpatched and unverified Node lines', () => {
|
||||
for (const version of ['18.20.8', '22.23.4', '24.16.0', '24.22.0', '25.0.0', '26.12.1', 'v24.22.1-rc.1', 'garbage']) {
|
||||
assert.throws(() => assertPiNodeVersion(version), /24.22.1/);
|
||||
}
|
||||
for (const version of ['v24.22.1', '24.22.2', '24.23.0']) assert.doesNotThrow(() => assertPiNodeVersion(version));
|
||||
});
|
||||
10
apps/detail-photocraft/Cargo.lock
generated
10
apps/detail-photocraft/Cargo.lock
generated
@ -6114,9 +6114,9 @@ checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.15"
|
||||
version = "0.103.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
@ -6968,7 +6968,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.3",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix 1.1.5",
|
||||
"windows-sys 0.61.2",
|
||||
@ -7929,9 +7929,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.9"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
|
||||
checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
@ -39,6 +39,8 @@ cargo xtask wasm
|
||||
“接手”、人工修改和切换文档会停止剩余自动操作。
|
||||
- 正常“编辑画布”发送入口已由本地 Pi SDK 接管,保留现有原生界面。输入区选择项目的品牌/品类规则;完整版本包从素材库授权接口读取,本机项目固定快照。长历史压缩后仍能检索原文,重启恢复同一会话。规则细则与模板按任务加载,不整份重复注入。
|
||||
- 本机包须包含 `agent/node.exe`、`agent/src/desktop-host.mjs` 和锁定依赖;缺组件会明确报错,不能退回旧Agent冒充接通。构建及隔离工具约束见 `../detail-agent-pi/README.md`。
|
||||
Node 使用 24.22.1 或同线后续安全版本。原生 HTTPS 依赖已更新至
|
||||
`rustls-webpki 0.103.16`、`webpki-root-certs 1.0.10`;连接仍执行完整证书校验。
|
||||
- 右侧“生图”使用选中的产品图片及用户自己的 Key、所选生图模型,返回图片作为独立图层。
|
||||
“重生成”只替换当前屏选中的一张图片,保留文字、字体和其他屏。按原图框等比裁切,
|
||||
替换及必要的裁切蒙版在同一次撤销内;重复替换保持同一图框。
|
||||
|
||||
@ -925,6 +925,17 @@ pub fn start(root: PathBuf, ctx: egui::Context) -> Workspace {
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
#[ignore = "read-only public TLS acceptance; no account, API Key or inference request"]
|
||||
fn jiansun_security_revoked_intermediates_fail_tls_validation() {
|
||||
let client = Client::builder().timeout(Duration::from_secs(15)).redirect(reqwest::redirect::Policy::none()).build().unwrap();
|
||||
assert!(client.head("https://nodejs.org/").send().is_ok(), "trusted control connection must work");
|
||||
for host in ["dv-g3b-emudhra.we-have-a-problem.com", "dv-g3b-aaa-emudhra.we-have-a-problem.com"] {
|
||||
let error = client.head(format!("https://{host}/")).send().expect_err("compromised intermediate must be rejected");
|
||||
assert!(error.is_connect(), "failure must occur during connection verification");
|
||||
assert!(format!("{error:?}").contains("InvalidCertificate"), "must be certificate rejection, not an unrelated network failure: {error:?}");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn retained_results_accept_original_bodies_without_removing_the_image_array() {
|
||||
let image = json!({"created":1,"data":[{"b64_json":"test"}]});
|
||||
assert_eq!(retained_body(image.clone()).unwrap(), image);
|
||||
|
||||
@ -55,7 +55,7 @@ fn runtime() -> R<(PathBuf, PathBuf)> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let script = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../detail-agent-pi/src/desktop-host.mjs");
|
||||
let node = PathBuf::from(if cfg!(windows) { "C:/Program Files/nodejs/node.exe" } else { "/usr/bin/node" });
|
||||
let node = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../detail-agent-pi/runtime").join(if cfg!(windows) { "node.exe" } else { "node" });
|
||||
if script.is_file() && node.is_file() { return Ok((node, script)); }
|
||||
}
|
||||
Err("客户端缺少Agent运行组件,请保留项目并安装完整版本".into())
|
||||
|
||||
@ -44,32 +44,18 @@ function Upload-File {
|
||||
|
||||
$RemoteBuildDir = "/opt/youyidian-3d-base-build"
|
||||
|
||||
# 步骤 1:检查缓存
|
||||
Write-Host "📦 步骤 1:检查缓存文件" -ForegroundColor Magenta
|
||||
$cacheCheck = Invoke-BuildServer -Command "test -d /opt/build-cache/alpine-apk && test -d /opt/build-cache/node_modules && echo 'OK' || echo 'MISSING'" -Silent
|
||||
if ($cacheCheck -notmatch "OK") {
|
||||
Write-Host "❌ 缓存文件不完整,请先运行 setup-build-cache.ps1" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "✅ 缓存文件已就绪" -ForegroundColor Green
|
||||
# 历史 Node 18 缓存不能跨 ABI 复用;由当前锁文件重新安装原生依赖。
|
||||
Write-Host "📦 步骤 1:核对依赖锁文件" -ForegroundColor Magenta
|
||||
if (!(Test-Path 'package-lock.json')) { throw '缺少 package-lock.json,拒绝构建' }
|
||||
Write-Host ""
|
||||
|
||||
# 步骤 2:准备构建目录
|
||||
Write-Host "📦 步骤 2:准备构建目录" -ForegroundColor Magenta
|
||||
Invoke-BuildServer -Command "mkdir -p $RemoteBuildDir/cache" -Silent | Out-Null
|
||||
|
||||
# 使用硬链接复制缓存(零额外空间,瞬间完成)
|
||||
Write-Host " 准备缓存文件(使用硬链接,零额外空间)..." -ForegroundColor Yellow
|
||||
Invoke-BuildServer -Command @"
|
||||
cd $RemoteBuildDir && \
|
||||
rm -rf cache/alpine-apk cache/node_modules 2>/dev/null || true && \
|
||||
mkdir -p cache && \
|
||||
cp -al /opt/build-cache/alpine-apk cache/ && \
|
||||
cp -al /opt/build-cache/node_modules cache/ && \
|
||||
echo 'OK'
|
||||
"@ -Silent | Out-Null
|
||||
Invoke-BuildServer -Command "mkdir -p $RemoteBuildDir" -Silent | Out-Null
|
||||
|
||||
Upload-File -LocalPath "Dockerfile.base" -RemotePath "$RemoteBuildDir/Dockerfile"
|
||||
Upload-File -LocalPath "package.json" -RemotePath "$RemoteBuildDir/package.json"
|
||||
Upload-File -LocalPath "package-lock.json" -RemotePath "$RemoteBuildDir/package-lock.json"
|
||||
Upload-File -LocalPath "prisma/schema.prisma" -RemotePath "$RemoteBuildDir/schema.prisma"
|
||||
|
||||
Invoke-BuildServer -Command "mkdir -p $RemoteBuildDir/prisma && mv $RemoteBuildDir/schema.prisma $RemoteBuildDir/prisma/" -Silent | Out-Null
|
||||
|
||||
@ -7,7 +7,7 @@ services:
|
||||
build:
|
||||
target: dev
|
||||
args:
|
||||
BASE_IMAGE: docker.m.daocloud.io/library/node:22-alpine
|
||||
BASE_IMAGE: node:22.23.4-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914
|
||||
image: gongxiangp2-local-dev:latest
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
|
||||
@ -71,7 +71,7 @@ services:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
BASE_IMAGE: docker.m.daocloud.io/library/node:18-alpine
|
||||
BASE_IMAGE: node:22.23.4-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914
|
||||
image: gongxiangp2-local-test:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
|
||||
77
docs/runtime-security-upgrade-2026-10-22.md
Normal file
77
docs/runtime-security-upgrade-2026-10-22.md
Normal file
@ -0,0 +1,77 @@
|
||||
# 运行时安全升级与回滚
|
||||
|
||||
## 范围
|
||||
|
||||
| 组件 | 本次目标 | 边界 |
|
||||
| --- | --- | --- |
|
||||
| 素材库 Web、独立渲染进程 | Node 22.23.4,固定官方镜像清单 SHA-256 | 重新编译 Canvas / Sharp,保留现有应用、权限与存储契约 |
|
||||
| 通知中心 | Node 22.23.4 | 独立仓库;临时 SQLite 回归,不发送真实通知 |
|
||||
| 本机 Pi | Node 24.22.1 或同一 24.x 线后续安全版本 | 随客户端打包;主入口、SDK 入口、打包均拒绝旧版本 |
|
||||
| 原生客户端 HTTPS | rustls-webpki 0.103.16、webpki-root-certs 1.0.10 | 保留 Rustls 校验,不关闭 TLS 验证 |
|
||||
| MinIO | 原 2025-04-22 源码,Go 1.26.10 重编译 | 原协议、命令、数据卷与版本格式不变 |
|
||||
| frpc | 原 0.67.0 源码,Go 1.26.10 重编译 | 配置不变,四个入口逐个切换、逐个检查 |
|
||||
| TrueNAS 自带 Docker、系统 rclone | **另行安排 NAS 维护** | 用户已确认分开处理,本轮不改只读系统、不重启 Docker / NAS,不宣称已修复 |
|
||||
|
||||
生产链路没有 PHP / PHP-FPM;不为本次更新新增 PHP。
|
||||
不修改其他项目的全局 Node、操作系统配置或模型上游。
|
||||
|
||||
## 依据与可重建性
|
||||
|
||||
- [Node 官方 2026-10 安全更新](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases)
|
||||
- [Rustls 证书验证修复](https://rustsec.org/advisories/RUSTSEC-2026-0382.html)、[内置证书修复](https://rustsec.org/advisories/RUSTSEC-2026-0383.html)
|
||||
- [Go 官方证书信任修复 / CVE-2026-111943](https://go.dev/issue/82345)
|
||||
- [TrueNAS 官方开发模式限制](https://www.truenas.com/docs/scale/systemsettings/advanced/developermode/)
|
||||
|
||||
Go 源码的提交、归档哈希和版本在 `scripts/security-runtime/go-sources.json`;
|
||||
构建镜像、参数和上游许可证保留在 `go-rebuild.Dockerfile` 的构建结果中。
|
||||
`GOTOOLCHAIN=local` 防止模块声明把工具链降回旧版本。自编译包不冒充上游新发行版。
|
||||
|
||||
```powershell
|
||||
node scripts/security-runtime/prepare-go-context.mjs output/security-go-build
|
||||
docker build --target artifacts -f output/security-go-build/Dockerfile output/security-go-build
|
||||
```
|
||||
|
||||
`tls-probe.go` 只执行 TLS 握手:正常站点必须通过,两个失信证书样例必须被证书验证拒绝;
|
||||
DNS、超时和网络错误不能算修复通过。不发送 HTTP、API Key 或业务数据。
|
||||
本机 Node 使用相同的正反例验证;Rust 的显式联网验收测试默认 ignored,须单独执行。
|
||||
|
||||
历史 `Dockerfile.base` 改为按锁文件重新安装原生依赖,禁止导入 Node 18 的二进制缓存。
|
||||
旧 `setup-build-cache.ps1` 不是当前生产链路;新基础镜像构建不读取它生成的缓存。
|
||||
开发 Compose 与本地验收 Compose 也固定同一安全版本,避免参数覆盖回旧 Node。
|
||||
|
||||
## 本地兼容门禁
|
||||
|
||||
已完成:
|
||||
|
||||
- Node 22 下账号、素材权限、分片上传、下载、原图和报告外部接口、Agent 规则与异常恢复:15 个套件、155 项通过。
|
||||
- 渲染器:中文字体、图片比例、PNG、长图、可编辑 PSD、非法输入等 6 项通过。
|
||||
- Pi:31 项回归;客户端主程序:182 项通过、严格 Clippy 通过、31 个 crate 分层检查通过。
|
||||
- 真实本地 1440 × 12337 多屏文件:中文编辑、撤销重做、保存重开、分层 PSD 重读通过。
|
||||
- 更新后的本地客户端恢复原账户、56 条对话及原画布;原生模型桥只读检查通过。没有新增模型请求。
|
||||
- MinIO 在同一隔离测试卷上依次运行原版、修复版、回退原版;图片、PDF、ZIP、RAR 的逐字节校验、Range、分页、元数据均通过。
|
||||
- frpc 0.67.0 的配置校验、TLS、Token 鉴权和 TCP 转发在隔离网络通过。
|
||||
- Node、Rust、Go 均通过正常 HTTPS / 失信证书正反例。实际生产替换后须再次核对运行版本与证书行为。
|
||||
|
||||
图形界面和模型调用流程不在本次重写。没有用新的付费调用代替兼容回归;未知请求不重发。
|
||||
|
||||
## 数据保护与生产切换
|
||||
|
||||
备份位于生产机私有目录:
|
||||
`/mnt/PoolOne/apps/youyidian-3d/security-upgrade-20261022/backup`。
|
||||
PostgreSQL 使用一致性逻辑备份并验证 TOC;通知 SQLite 使用在线备份并通过完整性检查。
|
||||
MinIO 数据集和上传素材数据集均建立 `security-upgrade-20261022` 快照并加 hold。
|
||||
环境、容器配置和原镜像保留供回滚;不得把这些敏感备份纳入 Git 或日志输出。
|
||||
|
||||
1. 只打包已提交的源代码,不把工作区无关改动混入发布。
|
||||
2. 镜像归档逐份校验哈希、Linux/amd64、真实 config ID 与版本标签。
|
||||
3. 记录 DB / Redis 容器身份、素材挂载、队列及未知请求指纹;有活动任务先等待。
|
||||
4. MinIO 与通知中心分别替换,故障立即切回原容器;禁止两个 MinIO 同时写生产数据卷。
|
||||
5. frpc 先用原服务环境校验配置,再逐个重启;每个入口核对原业务健康,失败恢复原二进制。
|
||||
6. Web 按现有增量发布流程部署。无数据库迁移、数据恢复、素材清理或自动补发模型。
|
||||
7. 检查公网页面、权限、健康端点、真实素材读取 / Range、通知队列、Node 和 Go 身份。
|
||||
|
||||
回滚优先恢复程序和镜像,**不能自动回滚素材 ZFS 快照**,避免覆盖切换后新增的用户文件。
|
||||
数据库引擎、库结构和用户文件均不属于本次修改范围。
|
||||
|
||||
本地构建与验收原始证据保留在 `output/security-upgrade-20261022/`。
|
||||
客户端包为本机未签名开发包,不能当作已签名正式安装程序分发。
|
||||
3
package-lock.json
generated
3
package-lock.json
generated
@ -74,6 +74,9 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvas": "^2.11.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.23.4 <23 || >=24.22.1 <25"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
|
||||
@ -96,6 +96,11 @@
|
||||
"canvas": "^2.11.2"
|
||||
},
|
||||
"overrides": {
|
||||
"fabric": { "canvas": "2.11.2" }
|
||||
"fabric": {
|
||||
"canvas": "2.11.2"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.23.4 <23 || >=24.22.1 <25"
|
||||
}
|
||||
}
|
||||
|
||||
@ -907,7 +907,7 @@ test('container supply chain and migrator image are immutable while SOP keeps co
|
||||
|
||||
assert.match(
|
||||
dockerfile,
|
||||
/^ARG BASE_IMAGE=node:18-alpine@sha256:8d6421d663b4c28fd3ebc498332f249011d118945588d0a35cb9bc4b8ca09d9e$/m,
|
||||
/^ARG BASE_IMAGE=node:22.23.4-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914$/m,
|
||||
)
|
||||
assert.match(deps, /^COPY package\.json package-lock\.json \.\/$/m)
|
||||
assert.match(deps, /npm ci --prefer-offline --no-audit --no-fund --legacy-peer-deps/)
|
||||
|
||||
49
scripts/security-runtime/go-rebuild.Dockerfile
Normal file
49
scripts/security-runtime/go-rebuild.Dockerfile
Normal file
@ -0,0 +1,49 @@
|
||||
# Rebuild the deployed sources without changing service protocols or storage format.
|
||||
ARG GO_IMAGE=golang:1.26.10-bookworm@sha256:632dff7351799a5a9954a48dc06670ade72ee44e8c351cdc0e5a57af2fea21af
|
||||
ARG NODE_IMAGE=node:22.23.4-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914
|
||||
FROM ${GO_IMAGE} AS source
|
||||
ENV GOTOOLCHAIN=local CGO_ENABLED=0 GOMAXPROCS=2 GOMEMLIMIT=512MiB GOFLAGS=-p=1
|
||||
COPY *.tar.gz sources.sha256 /sources/
|
||||
RUN cd /sources && sha256sum -c sources.sha256 && \
|
||||
mkdir /frpc /rclone /minio /out && \
|
||||
tar -xzf frpc.tar.gz -C /frpc --strip-components=1 && \
|
||||
tar -xzf rclone.tar.gz -C /rclone --strip-components=1 && \
|
||||
tar -xzf minio.tar.gz -C /minio --strip-components=1
|
||||
COPY tls-probe.go /probe/main.go
|
||||
RUN cd /probe && go build -trimpath -o /out/tls-probe main.go
|
||||
|
||||
FROM ${NODE_IMAGE} AS frp-web
|
||||
COPY --from=source /frpc/web /web
|
||||
WORKDIR /web/frpc
|
||||
RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --no-fund && npm run build
|
||||
WORKDIR /web/frps
|
||||
RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --no-fund && npm run build
|
||||
|
||||
FROM source AS frpc
|
||||
WORKDIR /frpc
|
||||
COPY --from=frp-web /web/frpc/dist ./web/frpc/dist
|
||||
COPY --from=frp-web /web/frps/dist ./web/frps/dist
|
||||
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
|
||||
go build -trimpath -tags frpc -ldflags '-s -w' -o /out/frpc ./cmd/frpc && \
|
||||
go build -trimpath -tags frps -ldflags '-s -w' -o /out/frps ./cmd/frps && \
|
||||
go version -m /out/frpc > /out/frpc.buildinfo && \
|
||||
cp LICENSE /out/frpc.LICENSE
|
||||
|
||||
FROM source AS rclone
|
||||
WORKDIR /rclone
|
||||
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
|
||||
go build -trimpath -ldflags '-s -w -X github.com/rclone/rclone/fs.Version=v1.67.0' -o /out/rclone . && \
|
||||
go version -m /out/rclone > /out/rclone.buildinfo && \
|
||||
cp COPYING /out/rclone.LICENSE
|
||||
|
||||
FROM source AS minio
|
||||
WORKDIR /minio
|
||||
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
|
||||
go build -trimpath -tags kqueue -ldflags '-s -w -X github.com/minio/minio/cmd.Version=2025-04-22T22:12:26Z -X github.com/minio/minio/cmd.ReleaseTag=RELEASE.2025-04-22T22-12-26Z -X github.com/minio/minio/cmd.CopyrightYear=2025 -X github.com/minio/minio/cmd.CommitID=0d7408fc9969caf07de6a8c3a84f9fbb10a6739e -X github.com/minio/minio/cmd.ShortCommitID=0d7408fc9969' -o /out/minio . && \
|
||||
go version -m /out/minio > /out/minio.buildinfo && \
|
||||
cp LICENSE /out/minio.LICENSE
|
||||
|
||||
FROM scratch AS artifacts
|
||||
COPY --from=frpc /out/ /out/
|
||||
COPY --from=rclone /out/ /out/
|
||||
COPY --from=minio /out/ /out/
|
||||
26
scripts/security-runtime/go-sources.json
Normal file
26
scripts/security-runtime/go-sources.json
Normal file
@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"name": "frpc",
|
||||
"repo": "fatedier/frp",
|
||||
"tag": "v0.67.0",
|
||||
"commit": "5f575b84420a19350467290c5c2bf9f4c7b27e57",
|
||||
"bytes": 517108,
|
||||
"sha256": "a6c6aa5d221f3810d76efdbee15f5784d0945f8e636861b236a22be0447a6791"
|
||||
},
|
||||
{
|
||||
"name": "rclone",
|
||||
"repo": "rclone/rclone",
|
||||
"tag": "v1.67.0",
|
||||
"commit": "93e8a976ef686a4bd8e3afaf2016734c81881507",
|
||||
"bytes": 17233524,
|
||||
"sha256": "7d61dd139534892aa35611f7aac3915dee3f2d74561f494e4e215f56d52284b0"
|
||||
},
|
||||
{
|
||||
"name": "minio",
|
||||
"repo": "minio/minio",
|
||||
"tag": "RELEASE.2025-04-22T22-12-26Z",
|
||||
"commit": "0d7408fc9969caf07de6a8c3a84f9fbb10a6739e",
|
||||
"bytes": 23956843,
|
||||
"sha256": "7eb30a913fea30f18069abf194e1e78e4983b558cc526911ae1c11396a9859a5"
|
||||
}
|
||||
]
|
||||
29
scripts/security-runtime/prepare-go-context.mjs
Normal file
29
scripts/security-runtime/prepare-go-context.mjs
Normal file
@ -0,0 +1,29 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {createHash} from 'node:crypto';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const target = path.resolve(process.argv[2] || 'output/security-go-build');
|
||||
await fs.mkdir(target, {recursive:true});
|
||||
const pins = JSON.parse(await fs.readFile(path.join(here, 'go-sources.json')));
|
||||
const checks = [];
|
||||
for (const pin of pins) {
|
||||
const file = path.join(target, pin.name + '.tar.gz');
|
||||
let bytes;
|
||||
try { bytes = await fs.readFile(file); } catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
const response = await fetch(`https://codeload.github.com/${pin.repo}/tar.gz/${pin.commit}`);
|
||||
if (!response.ok) throw Error(`Source download: ${pin.name} HTTP ${response.status}`);
|
||||
bytes = Buffer.from(await response.arrayBuffer());
|
||||
if (createHash('sha256').update(bytes).digest('hex') !== pin.sha256) throw Error(`Source checksum: ${pin.name}`);
|
||||
await fs.writeFile(file, bytes, {flag:'wx'});
|
||||
}
|
||||
if (createHash('sha256').update(bytes).digest('hex') !== pin.sha256) throw Error(`Source checksum: ${pin.name}`);
|
||||
checks.push(`${pin.sha256} ${pin.name}.tar.gz`);
|
||||
}
|
||||
await fs.writeFile(path.join(target, 'sources.sha256'), checks.join('\n') + '\n');
|
||||
for (const [from,to] of [['go-rebuild.Dockerfile','Dockerfile'],['tls-probe.go','tls-probe.go']]) {
|
||||
await fs.copyFile(path.join(here, from), path.join(target,to));
|
||||
}
|
||||
console.log(JSON.stringify({target, sourcesVerified:pins.length, go:'1.26.10'}));
|
||||
37
scripts/security-runtime/tls-probe.go
Normal file
37
scripts/security-runtime/tls-probe.go
Normal file
@ -0,0 +1,37 @@
|
||||
// TLS-only acceptance: no HTTP requests, accounts, API Keys, or business data.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
targets := []string{"nodejs.org", "dv-g3b-emudhra.we-have-a-problem.com", "dv-g3b-aaa-emudhra.we-have-a-problem.com"}
|
||||
results := []map[string]any{}
|
||||
passed := true
|
||||
for i, host := range targets {
|
||||
c, err := tls.DialWithDialer(&net.Dialer{Timeout: 10 * time.Second}, "tcp", host+":443", &tls.Config{ServerName: host, MinVersion: tls.VersionTLS12})
|
||||
row := map[string]any{"host": host}
|
||||
if err == nil {
|
||||
c.Close()
|
||||
row["result"] = "accepted"
|
||||
if i != 0 { passed = false }
|
||||
} else {
|
||||
row["error"] = err.Error()
|
||||
row["result"] = "inconclusive"
|
||||
if strings.Contains(err.Error(), "x509:") { row["result"] = "certificate_rejected" }
|
||||
if i == 0 || row["result"] != "certificate_rejected" { passed = false }
|
||||
}
|
||||
results = append(results, row)
|
||||
}
|
||||
b, _ := json.Marshal(map[string]any{"go":runtime.Version(), "httpRequests":0, "passed":passed, "results":results})
|
||||
fmt.Println(string(b))
|
||||
if !passed { os.Exit(1) }
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user