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) {