fix: 保留 registry bcrypt 字面量

This commit is contained in:
pikastech
2026-07-20 09:32:09 +02:00
parent a4296e8b4a
commit 17a789a844
4 changed files with 42 additions and 6 deletions
+2
View File
@@ -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。
+3
View File
@@ -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/...` 镜像引用保持不变;
+30 -5
View File
@@ -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) => {
+7 -1
View File
@@ -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 };
}