fix(deploy): allow declared Guanghu service state

This commit is contained in:
冰朔 2026-08-06 13:38:31 +08:00
commit b00ef1f5fb
4 changed files with 71 additions and 4 deletions

View file

@ -66,6 +66,16 @@ function validateServiceUpdateManifest(manifest, unit) {
if (!item || !/^http:\/\/127\.0\.0\.1:\d{2,5}\/[A-Za-z0-9._/?=&-]*$/.test(String(item.url || ""))) throw new Error("invalid_acceptance_check_url");
if (!item.expected || typeof item.expected !== "object" || Array.isArray(item.expected)) throw new Error("invalid_acceptance_expectation");
}
const environmentFiles = manifest.module.environment_files || [];
const writablePaths = manifest.module.writable_paths || [];
if (!Array.isArray(environmentFiles) || environmentFiles.some(item =>
!/^\/etc\/guanghu\/(?:secrets|persona-secrets)\/[A-Za-z0-9._/-]+$/.test(String(item || ""))
|| String(item).split("/").includes("..")
)) throw new Error("invalid_update_environment_files");
if (!Array.isArray(writablePaths) || writablePaths.some(item =>
!/^-?\/var\/lib\/guanghu\/[A-Za-z0-9._/-]+$/.test(String(item || ""))
|| String(item).split("/").includes("..")
)) throw new Error("invalid_update_writable_paths");
return {
kind: "existing-service-update",
unit,
@ -76,6 +86,8 @@ function validateServiceUpdateManifest(manifest, unit) {
sourcePaths: [unitSource, ...manifest.files.map(item => item.source)],
runtimeCheck: check,
acceptanceChecks,
environmentFiles,
writablePaths,
};
}
@ -109,14 +121,26 @@ function validateUnit(text, expectedUser = "guanghu", policy = {}) {
return value;
}
function validateUpdateUnit(text, expectedUser, installRoot) {
function validateUpdateUnit(text, expectedUser, installRoot, policy = {}) {
const value = String(text || "");
if (!value.includes("[Service]") || !/^NoNewPrivileges=(true|yes)$/m.test(value) || !/^ProtectSystem=strict$/m.test(value) || !/^ProtectHome=(true|yes)$/m.test(value) || !/^PrivateTmp=(true|yes)$/m.test(value)) throw new Error("unit_hardening_required");
if (!/^[a-z_][a-z0-9_-]{0,30}$/.test(expectedUser) || expectedUser === "root" || !new RegExp(`^User=${expectedUser}$`, "m").test(value) || !new RegExp(`^Group=${expectedUser}$`, "m").test(value)) throw new Error("dedicated_service_user_required");
if (!value.includes(`WorkingDirectory=${installRoot}`) || !value.includes(`ReadOnlyPaths=${installRoot}`)) throw new Error("service_install_root_not_confined");
const execStart = value.match(/^ExecStart=(.+)$/m);
if (!execStart || !execStart[1].includes(`${installRoot}/`) || /[;&|`$<>]/.test(execStart[1])) throw new Error("service_exec_start_not_confined");
if (/^(SupplementaryGroups|AmbientCapabilities|BindPaths|BindReadOnlyPaths|RootDirectory|RootImage|DeviceAllow|EnvironmentFile|ReadWritePaths)=/m.test(value)) throw new Error("privileged_unit_directive_forbidden");
if (/^(SupplementaryGroups|AmbientCapabilities|BindPaths|BindReadOnlyPaths|RootDirectory|RootImage|DeviceAllow)=/m.test(value)) throw new Error("privileged_unit_directive_forbidden");
const environmentFiles = Array.isArray(policy.environment_files) ? policy.environment_files : [];
const writablePaths = Array.isArray(policy.writable_paths) ? policy.writable_paths : [];
for (const match of value.matchAll(/^EnvironmentFile=(.+)$/gm)) {
const candidate = String(match[1]).replace(/^-/, "");
if (!environmentFiles.includes(candidate) || !/^\/etc\/guanghu\/(?:secrets|persona-secrets)\//.test(candidate)) throw new Error("environment_file_not_declared");
}
for (const match of value.matchAll(/^ReadWritePaths=(.+)$/gm)) {
for (const item of declaredPaths(match[1])) {
const candidate = item.replace(/^-/, "");
if (!candidate.startsWith("/var/lib/guanghu/") || !pathAllowed(item, writablePaths)) throw new Error("writable_path_not_declared");
}
}
return value;
}
@ -198,7 +222,12 @@ async function updateExistingService(context) {
}
fs.mkdirSync(releaseRoot, { recursive: true, mode: 0o755 });
for (const relative of checked.sourcePaths) copyDeclaredFile(repoDir, releaseRoot, relative);
const unitText = validateUpdateUnit(fs.readFileSync(path.join(releaseRoot, checked.unitSource), "utf8"), String(manifest.module.run_user || ""), checked.installRoot);
const unitText = validateUpdateUnit(
fs.readFileSync(path.join(releaseRoot, checked.unitSource), "utf8"),
String(manifest.module.run_user || ""),
checked.installRoot,
{ environment_files: checked.environmentFiles, writable_paths: checked.writablePaths },
);
fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
backupFile(installedUnit, path.join(backupDir, "systemd", checked.unit), backups);
for (const item of checked.files) {

View file

@ -52,6 +52,31 @@ test("unit permits a declared persona user, shared secret and state directory",
assert.throws(() => validateUnit(unit.replaceAll("kezhou", "root"), "root", policy), /dedicated_service_user_required/);
});
test("existing service update permits only declared Guanghu state and secret paths", () => {
const unit = `[Service]
User=guanghu-authz
Group=guanghu-authz
WorkingDirectory=/opt/guanghu/lake-lamp-authz
ExecStart=/usr/bin/node /opt/guanghu/lake-lamp-authz/server.js
EnvironmentFile=/etc/guanghu/secrets/lake-lamp/authorization.env
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/opt/guanghu/lake-lamp-authz
ReadWritePaths=/var/lib/guanghu/lake-lamp-authz -/var/lib/guanghu/deployment-events
`;
const policy = {
environment_files: ["/etc/guanghu/secrets/lake-lamp/authorization.env"],
writable_paths: ["/var/lib/guanghu/lake-lamp-authz", "/var/lib/guanghu/deployment-events"],
};
assert.equal(validateUpdateUnit(unit, "guanghu-authz", "/opt/guanghu/lake-lamp-authz", policy), unit);
assert.throws(
() => validateUpdateUnit(unit.replace("/var/lib/guanghu/lake-lamp-authz", "/etc/systemd/system"), "guanghu-authz", "/opt/guanghu/lake-lamp-authz", policy),
/writable_path_not_declared/,
);
});
test("AI discovery update package declares all four route maps and passes the existing-service policy", () => {
const root = path.resolve(__dirname, "../..");
const request = JSON.parse(fs.readFileSync(path.join(root, "deployment", "requests", "AI-DISCOVERY-ICE-P-ROUTE-20260727.json")));

View file

@ -16,6 +16,7 @@ NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/opt/guanghu/lake-lamp-authz
ReadWritePaths=/var/lib/guanghu/lake-lamp-authz /var/lib/guanghu/repo-authorizations /var/lib/guanghu/repo-push-uploads /var/lib/guanghu/deployment-events /var/lib/guanghu/forgejo/repositories/bingshuo/hololake-platform.git /var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/repositories/bingshuo/guanghu-ice-heart.git -/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/repositories/bingshuo/hololake-knowledge-base.git
ReadOnlyPaths=-/etc/guanghu/secrets/hololake-ai-providers.json
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6