Skip to content

Commit 1f4bf49

Browse files
committed
fix(policy): grant managed startup CA bundle read access (#9391)
Managed startup writes the active CA bundle to `/run/nemoclaw/managed-startup-ca-bundle.pem`, but the shipping filesystem policies do not allow agents to read that file. This change grants exact-file read access without granting access to the mutable `/run/nemoclaw` directory. Fixes #9360 - Grant the managed startup CA bundle exact read-only access in each shipping OpenClaw, Hermes, and LangChain Deep Agents Code baseline and permissive policy. - Add a real-policy regression test that covers every shipping managed-image agent and rejects read or write grants to `/run` and `/run/nemoclaw`. - [x] Code change (feature, bug fix, or refactor) - [ ] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) - [x] Tests added or updated for changed behavior - [ ] Existing tests cover changed behavior — justification: - [ ] Tests not applicable — justification: - [x] Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging) - [x] Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: [PR Review Advisor](https://github.com/NVIDIA/NemoClaw/actions/runs/32094112349) reported 0 blockers, 0 warnings, and 0 suggestions on exact head `53efaff8d6`; maintainer security review found no findings. - [ ] Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue: - [ ] Tested on DGX Station - Tested commit: - Station profile/scenario: - Result: - Supporting evidence: - [x] PR description includes a `Signed-off-by:` line and every commit appears as `Verified` in GitHub - [x] Normal `pre-commit`, `commit-msg`, and `pre-push` hooks passed, or `npm run validate:pr` passed after refreshing `origin/main` when hooks were skipped or unavailable - [x] Targeted behavior tests pass for the current change set, or tests are marked not applicable above — command/result or justification: `npx vitest run --project cli src/lib/onboard/initial-policy-real-policy.test.ts` passed 59/59; `npm run validate:configs` validated all 53 configuration files. - [ ] Applicable broad gate passed — `npm test` for broad runtime/test-harness changes; `npm run check` for repo-wide validation/coverage changes — command/result: Not run; this diff changes six policy entries and one focused regression test. - [x] Quality Gates section completed with required justifications or waivers - [x] No secrets, API keys, or credentials committed - [ ] `npm run docs` builds without warnings (doc changes only) - [ ] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) --- Signed-off-by: Aaron Erickson <aerickson@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> * **Bug Fixes** * Updated sandbox policies to provide read-only access to the managed startup certificate bundle. * Ensured startup certificate access does not broaden permissions for other runtime directories. * Improved certificate availability during startup while preserving existing filesystem protections across supported managed agents. * **Tests** * Added coverage verifying the certificate bundle is readable, not writable, and restricted to the intended path across supported managed agents. * Expanded policy validation to cover all shipped agent and sandbox policy combinations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
1 parent 3cf9a57 commit 1f4bf49

7 files changed

Lines changed: 97 additions & 21 deletions

File tree

agents/hermes/policy-additions.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ filesystem_policy:
2121
- /proc
2222
- /dev/urandom
2323
- /app
24+
- /run/nemoclaw/managed-startup-ca-bundle.pem
2425
- /etc
2526
- /var/log
2627
- /var/lib/dpkg # Allow package-version inspection without package mutation.

agents/hermes/policy-permissive.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ filesystem_policy:
2222
- /proc
2323
- /dev/urandom
2424
- /app
25+
- /run/nemoclaw/managed-startup-ca-bundle.pem
2526
- /etc
2627
- /var/log
2728
- /var/lib/dpkg # Allow package-version inspection without package mutation.

agents/langchain-deepagents-code/policy-additions.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ filesystem_policy:
1818
- /proc
1919
- /dev/urandom
2020
- /app
21+
- /run/nemoclaw/managed-startup-ca-bundle.pem
2122
- /etc
2223
- /var/log
2324
- /var/lib/dpkg # Allow package-version inspection without package mutation.

agents/openclaw/policy-permissive.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ filesystem_policy:
1818
- /proc
1919
- /dev/urandom
2020
- /app
21+
- /run/nemoclaw/managed-startup-ca-bundle.pem
2122
- /etc
2223
- /var/log
2324
- /var/lib/dpkg # Allow package-version inspection without package mutation.

nemoclaw-blueprint/policies/openclaw-sandbox-permissive.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ filesystem_policy:
2323
- /proc
2424
- /dev/urandom
2525
- /app
26+
- /run/nemoclaw/managed-startup-ca-bundle.pem
2627
- /etc
2728
- /var/log
2829
- /var/lib/dpkg # Allow package-version inspection without package mutation.

nemoclaw-blueprint/policies/openclaw-sandbox.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ filesystem_policy:
2626
- /proc
2727
- /dev/urandom
2828
- /app
29+
- /run/nemoclaw/managed-startup-ca-bundle.pem
2930
- /etc
3031
- /var/log
3132
- /var/lib/dpkg # Allow package-version inspection without package mutation.

src/lib/onboard/initial-policy-real-policy.test.ts

Lines changed: 91 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import path from "node:path";
77
import { afterEach, describe, expect, it } from "vitest";
88
import YAML from "yaml";
99

10+
import { SHIPPED_MANAGED_IMAGE_AGENTS } from "./managed-image/contract";
11+
import { MANAGED_STARTUP_MERGED_CA_FILE } from "./managed-startup/image-runtime";
1012
import { prepareInitialSandboxCreatePolicy } from "./initial-policy";
1113

1214
type PolicyRule = {
@@ -50,6 +52,20 @@ function repoPath(...segments: string[]): string {
5052
return path.join(import.meta.dirname, "..", "..", "..", ...segments);
5153
}
5254

55+
function normalizeFilesystemPolicyPath(policyPath: string): string {
56+
return path.posix.normalize(policyPath).replace(/\/+$/, "") || "/";
57+
}
58+
59+
function filesystemPolicyAncestors(policyPath: string): string[] {
60+
const segments = normalizeFilesystemPolicyPath(policyPath).split("/").filter(Boolean);
61+
return [
62+
"/",
63+
...segments
64+
.slice(0, -1)
65+
.map((_, index) => `/${segments.slice(0, index + 1).join("/")}`),
66+
];
67+
}
68+
5369
function readPreparedPolicy(prepared: {
5470
policyPath: string;
5571
cleanup?: () => boolean;
@@ -59,6 +75,69 @@ function readPreparedPolicy(prepared: {
5975
}
6076

6177
describe("initial sandbox policy real preset merge", () => {
78+
const managedImagePolicyPathsByAgent = {
79+
openclaw: [
80+
["nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"],
81+
["nemoclaw-blueprint", "policies", "openclaw-sandbox-permissive.yaml"],
82+
["agents", "openclaw", "policy-permissive.yaml"],
83+
],
84+
hermes: [
85+
["agents", "hermes", "policy-additions.yaml"],
86+
["agents", "hermes", "policy-permissive.yaml"],
87+
],
88+
"langchain-deepagents-code": [
89+
["agents", "langchain-deepagents-code", "policy-additions.yaml"],
90+
],
91+
} as const satisfies Record<
92+
(typeof SHIPPED_MANAGED_IMAGE_AGENTS)[number],
93+
readonly (readonly string[])[]
94+
>;
95+
96+
const managedImagePolicyCases = SHIPPED_MANAGED_IMAGE_AGENTS.flatMap((agent) =>
97+
managedImagePolicyPathsByAgent[agent].map((policyPath) => ({ path: policyPath, agent })),
98+
);
99+
const shippingPolicyCases = managedImagePolicyCases.filter(
100+
({ agent }) => agent !== "langchain-deepagents-code",
101+
);
102+
103+
it("covers the complete shipped managed startup CA policy matrix", () => {
104+
const policyIdentities = managedImagePolicyCases.map(
105+
({ path: policyPath, agent }) => `${agent}:${policyPath.join("/")}`,
106+
);
107+
108+
expect(Object.keys(managedImagePolicyPathsByAgent)).toEqual([...SHIPPED_MANAGED_IMAGE_AGENTS]);
109+
expect(policyIdentities).toHaveLength(6);
110+
expect(new Set(policyIdentities).size).toBe(policyIdentities.length);
111+
});
112+
113+
it.each(managedImagePolicyCases)(
114+
"grants $agent policy $path exact read-only access to the managed startup CA bundle (#9360)",
115+
(policyCase) => {
116+
const prepared = prepareInitialSandboxCreatePolicy(repoPath(...policyCase.path), [], {
117+
agentName: policyCase.agent,
118+
});
119+
const policy = readPreparedPolicy(prepared);
120+
const readOnly = policy.filesystem_policy?.read_only ?? [];
121+
const readWrite = policy.filesystem_policy?.read_write ?? [];
122+
const normalizedReadOnly = readOnly.map(normalizeFilesystemPolicyPath);
123+
const normalizedReadWrite = readWrite.map(normalizeFilesystemPolicyPath);
124+
const managedCaAncestors = filesystemPolicyAncestors(MANAGED_STARTUP_MERGED_CA_FILE);
125+
126+
expect(readOnly, policyCase.path.join("/")).toContain(MANAGED_STARTUP_MERGED_CA_FILE);
127+
expect(normalizedReadWrite, policyCase.path.join("/")).not.toContain(
128+
MANAGED_STARTUP_MERGED_CA_FILE,
129+
);
130+
expect(
131+
normalizedReadOnly.filter((candidate) => managedCaAncestors.includes(candidate)),
132+
policyCase.path.join("/"),
133+
).toEqual([]);
134+
expect(
135+
normalizedReadWrite.filter((candidate) => managedCaAncestors.includes(candidate)),
136+
policyCase.path.join("/"),
137+
).toEqual([]);
138+
},
139+
);
140+
62141
it.each([
63142
{
64143
path: ["nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"],
@@ -177,23 +256,16 @@ describe("initial sandbox policy real preset merge", () => {
177256
}
178257
});
179258

180-
it("grants read-only package database access in every shipping sandbox policy (#8467)", () => {
181-
const policyCases = [
182-
{ path: ["nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"], agent: "openclaw" },
183-
{
184-
path: ["nemoclaw-blueprint", "policies", "openclaw-sandbox-permissive.yaml"],
185-
agent: "openclaw",
186-
},
187-
{ path: ["agents", "openclaw", "policy-permissive.yaml"], agent: "openclaw" },
188-
{ path: ["agents", "hermes", "policy-additions.yaml"], agent: "hermes" },
189-
{ path: ["agents", "hermes", "policy-permissive.yaml"], agent: "hermes" },
190-
{
191-
path: ["agents", "langchain-deepagents-code", "policy-additions.yaml"],
192-
agent: "langchain-deepagents-code",
193-
},
194-
];
195-
196-
for (const policyCase of policyCases) {
259+
it.each(
260+
managedImagePolicyCases.flatMap((policyCase) =>
261+
["/", "/var", "/var/lib", "/var/lib/dpkg"].map((writableAncestor) => ({
262+
policyCase,
263+
writableAncestor,
264+
})),
265+
),
266+
)(
267+
"grants $policyCase.agent policy $policyCase.path read-only package access without writable $writableAncestor (#8467)",
268+
({ policyCase, writableAncestor }) => {
197269
const prepared = prepareInitialSandboxCreatePolicy(repoPath(...policyCase.path), [], {
198270
agentName: policyCase.agent,
199271
});
@@ -202,10 +274,8 @@ describe("initial sandbox policy real preset merge", () => {
202274
const readWrite = policy.filesystem_policy?.read_write ?? [];
203275

204276
expect(readOnly, policyCase.path.join("/")).toContain("/var/lib/dpkg");
205-
for (const writableAncestor of ["/", "/var", "/var/lib", "/var/lib/dpkg"]) {
206-
expect(readWrite, policyCase.path.join("/")).not.toContain(writableAncestor);
207-
}
208-
}
277+
expect(readWrite, policyCase.path.join("/")).not.toContain(writableAncestor);
278+
},
209279
});
210280

211281
it("preserves baseline writable paths in effective OpenClaw permissive create policies", () => {

0 commit comments

Comments
 (0)