From 17a789a844c42c35bc250d19c070a2c15a59c6f2 Mon Sep 17 00:00:00 2001 From: pikastech Date: Mon, 20 Jul 2026 09:32:09 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=9D=E7=95=99=20registry=20bcrypt?= =?UTF-8?q?=20=E5=AD=97=E9=9D=A2=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/unidesk-cicd/SKILL.md | 2 ++ docs/reference/platform-infra.md | 3 ++ scripts/src/platform-infra-k3s-cluster.ts | 35 +++++++++++++++++++---- scripts/src/platform-infra-public-edge.ts | 8 +++++- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.agents/skills/unidesk-cicd/SKILL.md b/.agents/skills/unidesk-cicd/SKILL.md index cbdf957b..d64d57ca 100644 --- a/.agents/skills/unidesk-cicd/SKILL.md +++ b/.agents/skills/unidesk-cicd/SKILL.md @@ -368,6 +368,8 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - 公网 HTTPS 只允许鉴权 `GET`/`HEAD`,不得成为 push、cache export 或删除入口; - worker 保持既有镜像引用,由集群 owning YAML 渲染 endpoint 与 SecretRef; - registry 大流量不得经过 WireGuard/Flannel,也不得使用 CDN 或公共镜像站; + - bcrypt hash 必须以 Compose-safe 字面量写入 Secret source,且 + public-edge 用脱敏 material fingerprint 驱动凭据变更后的 recreate; - 验收统一走 `platform-infra k3s-cluster registry-smoke`,不手工 patch Caddy 或 containerd。 - worker 安装使用短启动并由 control-plane Node Ready 轮询收口,禁止让 `trans`/SSH 长挂等待 systemd notify。 - worker 加入集群不改变 PaC consumer 的 source commit authority,也不授权人工 PipelineRun、Argo sync 或业务 rollout。 diff --git a/docs/reference/platform-infra.md b/docs/reference/platform-infra.md index cd1c522a..62541598 100644 --- a/docs/reference/platform-infra.md +++ b/docs/reference/platform-infra.md @@ -201,6 +201,9 @@ - hostname、DNS、upstream、允许方法和鉴权 SecretRef 来自集群 owning YAML; - Caddy 只允许 `GET`、`HEAD`,其余方法返回拒绝; - Basic Auth 只消费 SecretRef 中的用户名与 bcrypt hash,不把值写入 Git、计划输出或 Caddyfile; + - bcrypt hash 在 Compose `env_file` 中必须使用单引号字面量,防止 `$` + 被 Compose 插值;compose 只携带 Secret material 的 SHA-256 标记, + 使规范化或轮换触发受控 recreate,禁止手工重启容器; - 可信 TLS 由 public-edge 管理,镜像层不得经过 CDN 代理。 - worker containerd 归集群 CLI 管理: - 既有 `127.0.0.1:5000/...` 镜像引用保持不变; diff --git a/scripts/src/platform-infra-k3s-cluster.ts b/scripts/src/platform-infra-k3s-cluster.ts index e5549674..ed9be5c0 100644 --- a/scripts/src/platform-infra-k3s-cluster.ts +++ b/scripts/src/platform-infra-k3s-cluster.ts @@ -129,18 +129,34 @@ function registryCredentialInit(target: K3sClusterTarget, confirm: boolean): Rec throw new Error(`${target.secrets.sourceRef} contains a partial legacy artifact registry credential; refusing to migrate`); } if (present.length === keys.length) { - if (!confirm || legacyPresent.length === 0) { + const source = readFileSync(path, "utf8"); + const normalizedSource = normalizeRegistryCredentialSource(source, auth.passwordHashKey, values[auth.passwordHashKey]!); + const normalizationNeeded = normalizedSource !== source; + if (!confirm || (!normalizationNeeded && legacyPresent.length === 0)) { return { ok: true, mutation: false, - mode: legacyPresent.length === 0 ? "already-present" : "dry-run-cleanup-legacy-source", + mode: normalizationNeeded + ? "dry-run-normalize-compose-literal" + : legacyPresent.length === 0 ? "already-present" : "dry-run-cleanup-legacy-source", targetId: target.id, registryAuth: registryAuthSummary(target), valuesPrinted: false, }; } - removeEnvKeys(target.secrets.sourceRef, clusterSource, keys); - return { ok: true, mutation: true, mode: "cleaned-legacy-source", targetId: target.id, registryAuth: registryAuthSummary(target), valuesPrinted: false }; + if (normalizationNeeded) { + writeFileSync(path, normalizedSource, { encoding: "utf8", mode: 0o600 }); + chmodSync(path, 0o600); + } + if (legacyPresent.length === keys.length) removeEnvKeys(target.secrets.sourceRef, clusterSource, keys); + return { + ok: true, + mutation: true, + mode: normalizationNeeded ? "normalized-compose-literal" : "cleaned-legacy-source", + targetId: target.id, + registryAuth: registryAuthSummary(target), + valuesPrinted: false, + }; } if (present.length > 0) throw new Error(`${path} contains a partial artifact registry credential; refusing to overwrite`); if (!confirm) { @@ -157,7 +173,7 @@ function registryCredentialInit(target: K3sClusterTarget, confirm: boolean): Rec const content = [ `${auth.usernameKey}=${credential.username}`, `${auth.passwordKey}=${credential.password}`, - `${auth.passwordHashKey}=${credential.passwordHash}`, + `${auth.passwordHashKey}='${credential.passwordHash}'`, "", ].join("\n"); if (existsSync(path)) appendFileSync(path, content, { encoding: "utf8", mode: 0o600 }); @@ -167,6 +183,15 @@ function registryCredentialInit(target: K3sClusterTarget, confirm: boolean): Rec return { ok: true, mutation: true, targetId: target.id, registryAuth: registryAuthSummary(target), valuesPrinted: false }; } +function normalizeRegistryCredentialSource(source: string, passwordHashKey: string, passwordHash: string): string { + const lines = source.split(/\r?\n/u).map((line) => { + const match = /^([A-Za-z_][A-Za-z0-9_]*)=/u.exec(line); + return match?.[1] === passwordHashKey ? `${passwordHashKey}='${passwordHash}'` : line; + }); + while (lines.at(-1) === "") lines.pop(); + return `${lines.join("\n")}\n`; +} + function removeEnvKeys(path: string, source: string, keys: string[]): void { const selected = new Set(keys); const retained = source.split(/\r?\n/u).filter((line) => { diff --git a/scripts/src/platform-infra-public-edge.ts b/scripts/src/platform-infra-public-edge.ts index 11d8738d..59af999f 100644 --- a/scripts/src/platform-infra-public-edge.ts +++ b/scripts/src/platform-infra-public-edge.ts @@ -707,7 +707,13 @@ export function renderPublicEdgeArtifacts(target: PublicEdgeTarget, sites: Resol const dnsServers = target.runtime.dnsServers.map((server) => ` - ${server}`).join("\n"); const envFiles = [...new Set(sites.flatMap((site) => site.accessPolicy === undefined ? [] : [site.accessPolicy.basicAuth.sourceRef]))]; const envFileBlock = envFiles.length === 0 ? "" : ` env_file:\n${envFiles.map((path) => ` - ${path}`).join("\n")}\n`; - const compose = `services:\n caddy:\n image: ${target.runtime.image}\n container_name: ${target.runtime.containerName}\n network_mode: host\n restart: unless-stopped\n${envFileBlock} dns:\n${dnsServers}\n volumes:\n - ${target.runtime.caddyfilePath}:/etc/caddy/Caddyfile:ro\n - ${target.runtime.dataDir}:/data\n - ${target.runtime.configDir}:/config\n`; + const authMaterialFingerprint = envFiles.length === 0 + ? null + : sha256Fingerprint(envFiles.map((path) => `${path}\n${readFileSync(path, "utf8")}`).join("\n")); + const authFingerprintBlock = authMaterialFingerprint === null + ? "" + : ` environment:\n UNIDESK_PUBLIC_EDGE_AUTH_MATERIAL_FINGERPRINT: ${authMaterialFingerprint}\n`; + const compose = `services:\n caddy:\n image: ${target.runtime.image}\n container_name: ${target.runtime.containerName}\n network_mode: host\n restart: unless-stopped\n${envFileBlock}${authFingerprintBlock} dns:\n${dnsServers}\n volumes:\n - ${target.runtime.caddyfilePath}:/etc/caddy/Caddyfile:ro\n - ${target.runtime.dataDir}:/data\n - ${target.runtime.configDir}:/config\n`; return { caddyfile: `${global}\n${blocks}`, compose }; }