diff --git a/deployment/requests/HLCC-HUMAN-MERGE-AUTO-DEPLOY-20260806.json b/deployment/requests/HLCC-HUMAN-MERGE-AUTO-DEPLOY-20260806.json index e23f8d6..5fe43f4 100644 --- a/deployment/requests/HLCC-HUMAN-MERGE-AUTO-DEPLOY-20260806.json +++ b/deployment/requests/HLCC-HUMAN-MERGE-AUTO-DEPLOY-20260806.json @@ -17,7 +17,19 @@ "module": { "unit": "lake-lamp-authz.service", "run_user": "guanghu-authz", - "install_root": "/opt/guanghu/lake-lamp-authz" + "install_root": "/opt/guanghu/lake-lamp-authz", + "environment_files": [ + "/etc/guanghu/secrets/lake-lamp/authorization.env" + ], + "writable_paths": [ + "/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" + ] }, "service_update": { "kind": "existing-systemd-service", diff --git a/server-tools/lake-lamp-authz/architecture-provision-broker.js b/server-tools/lake-lamp-authz/architecture-provision-broker.js index d743ce1..ff65030 100644 --- a/server-tools/lake-lamp-authz/architecture-provision-broker.js +++ b/server-tools/lake-lamp-authz/architecture-provision-broker.js @@ -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) { diff --git a/server-tools/lake-lamp-authz/architecture-provision-broker.test.js b/server-tools/lake-lamp-authz/architecture-provision-broker.test.js index a8aec75..43299a7 100644 --- a/server-tools/lake-lamp-authz/architecture-provision-broker.test.js +++ b/server-tools/lake-lamp-authz/architecture-provision-broker.test.js @@ -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"))); diff --git a/server-tools/lake-lamp-authz/lake-lamp-authz.service b/server-tools/lake-lamp-authz/lake-lamp-authz.service index fc8d350..92ae8f1 100644 --- a/server-tools/lake-lamp-authz/lake-lamp-authz.service +++ b/server-tools/lake-lamp-authz/lake-lamp-authz.service @@ -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