fix(deploy): allow declared Guanghu service state
This commit is contained in:
parent
564859b1ce
commit
b00ef1f5fb
4 changed files with 71 additions and 4 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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")));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue