Skip to content

Commit f91f2b0

Browse files
committed
test(openclaw): prove guard ownership boundary
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
1 parent 77c6836 commit f91f2b0

1 file changed

Lines changed: 92 additions & 19 deletions

File tree

test/nemoclaw-start-perms.test.ts

Lines changed: 92 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,27 @@ function replaceRequired(source: string, target: string, replacement: string): s
4949
const oneShotFunction = extractShellFunction("run_oneshot_command");
5050
const resolveNormalizerFunction = extractShellFunction("resolve_mutable_config_normalizer");
5151
const configGuardFunction = extractShellFunction("run_openclaw_config_guard");
52+
const canRunPrivilegedPermissionFixture =
53+
process.platform === "linux" &&
54+
spawnSync("sudo", ["-n", "test", "-x", "/usr/bin/setpriv"], { stdio: "ignore" }).status === 0;
55+
56+
function useTestRuntimeDirectory(source: string): string {
57+
let result = replaceRequired(
58+
source,
59+
" /run/nemoclaw || return 1",
60+
' "$NEMOCLAW_TEST_RUNTIME_DIR" || return 1',
61+
);
62+
result = replaceRequired(
63+
result,
64+
" /run/nemoclaw/openclaw-config-guard || return 1",
65+
' "$NEMOCLAW_TEST_RUNTIME_DIR/openclaw-config-guard" || return 1',
66+
);
67+
return replaceRequired(
68+
result,
69+
'output_file="/run/nemoclaw/openclaw-config-guard/.$$.output"',
70+
'output_file="$NEMOCLAW_TEST_RUNTIME_DIR/openclaw-config-guard/.$$.output"',
71+
);
72+
}
5273

5374
describe("nemoclaw-start config guard output permissions", () => {
5475
it("keeps the shared runtime path traversable while startup-owner output stays private (#9357)", () => {
@@ -60,28 +81,12 @@ describe("nemoclaw-start config guard output permissions", () => {
6081
fs.mkdirSync(runtimeDir, { mode: 0o700 });
6182
fs.writeFileSync(caBundle, "corporate CA\n", { mode: 0o444 });
6283

63-
let testFunction = configGuardFunction.replaceAll(
64-
"install -d -o root -g root -m",
65-
"install -d -m",
66-
);
67-
testFunction = replaceRequired(
68-
testFunction,
69-
"install -d -m 755 /run/nemoclaw || return 1",
70-
'install -d -m 755 "$NEMOCLAW_TEST_RUNTIME_DIR" || return 1',
71-
);
72-
testFunction = replaceRequired(
73-
testFunction,
74-
"install -d -m 700 /run/nemoclaw/openclaw-config-guard || return 1",
75-
'install -d -m 700 "$NEMOCLAW_TEST_RUNTIME_DIR/openclaw-config-guard" || return 1',
76-
);
77-
testFunction = replaceRequired(
78-
testFunction,
79-
'output_file="/run/nemoclaw/openclaw-config-guard/.$$.output"',
80-
'output_file="$NEMOCLAW_TEST_RUNTIME_DIR/openclaw-config-guard/.$$.output"',
84+
const testFunction = useTestRuntimeDirectory(
85+
configGuardFunction.replaceAll("install -d -o root -g root -m", "install -d -m"),
8186
);
8287
const script = [
8388
"set -euo pipefail",
84-
'python3() { find "$NEMOCLAW_TEST_RUNTIME_DIR" -type f -name \'.*.output\' -print >"$NEMOCLAW_TEST_OBSERVED_OUTPUT"; printf \'private guard output\\n\'; }',
89+
"python3() { find \"$NEMOCLAW_TEST_RUNTIME_DIR\" -type f -name '.*.output' -print >\"$NEMOCLAW_TEST_OBSERVED_OUTPUT\"; printf 'private guard output\\n'; }",
8590
"_OPENCLAW_CONFIG_GUARD=/tmp/openclaw-config-guard.py",
8691
testFunction,
8792
"run_openclaw_config_guard publish-startup-ready --startup-owner",
@@ -105,6 +110,74 @@ describe("nemoclaw-start config guard output permissions", () => {
105110
fs.rmSync(root, { recursive: true, force: true });
106111
}
107112
});
113+
114+
it.runIf(canRunPrivilegedPermissionFixture)(
115+
"keeps the CA readable while denying a distinct unprivileged user access to live guard output (#9357)",
116+
() => {
117+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-config-guard-root-output-"));
118+
const runtimeDir = path.join(root, "nemoclaw");
119+
const privateDir = path.join(runtimeDir, "openclaw-config-guard");
120+
const caBundle = path.join(runtimeDir, "managed-startup-ca-bundle.pem");
121+
const observedAccess = path.join(root, "observed-access");
122+
const nobodyUid = spawnSync("id", ["-u", "nobody"], { encoding: "utf-8" }).stdout.trim();
123+
const nobodyGid = spawnSync("id", ["-g", "nobody"], { encoding: "utf-8" }).stdout.trim();
124+
fs.chmodSync(root, 0o755);
125+
fs.mkdirSync(runtimeDir, { mode: 0o700 });
126+
fs.writeFileSync(caBundle, "corporate CA\n", { mode: 0o444 });
127+
128+
const testFunction = useTestRuntimeDirectory(configGuardFunction);
129+
const script = [
130+
"set -euo pipefail",
131+
"python3() {",
132+
' local output_path=""',
133+
' output_path="$(find "$NEMOCLAW_TEST_PRIVATE_DIR" -maxdepth 1 -type f -name \'.*.output\' -print -quit)"',
134+
' test -n "$output_path"',
135+
' /usr/bin/setpriv --reuid="$NEMOCLAW_TEST_NOBODY_UID" --regid="$NEMOCLAW_TEST_NOBODY_GID" --clear-groups -- sh -eu -c \'test ! -r "$1"; ! ls -A "$2" >/dev/null 2>&1; grep -Fqx "corporate CA" "$3"\' sh "$output_path" "$NEMOCLAW_TEST_PRIVATE_DIR" "$NEMOCLAW_TEST_CA_BUNDLE"',
136+
" printf 'denied\\n' >\"$NEMOCLAW_TEST_OBSERVED_ACCESS\"",
137+
" printf 'private guard output\\n'",
138+
"}",
139+
'chown root:root "$NEMOCLAW_TEST_CA_BUNDLE"',
140+
"_OPENCLAW_CONFIG_GUARD=/tmp/openclaw-config-guard.py",
141+
testFunction,
142+
"run_openclaw_config_guard publish-startup-ready --startup-owner",
143+
].join("\n");
144+
145+
try {
146+
const result = spawnSync(
147+
"sudo",
148+
[
149+
"-n",
150+
"env",
151+
`PATH=${process.env.PATH ?? "/usr/bin:/bin"}`,
152+
`NEMOCLAW_TEST_CA_BUNDLE=${caBundle}`,
153+
`NEMOCLAW_TEST_NOBODY_GID=${nobodyGid}`,
154+
`NEMOCLAW_TEST_NOBODY_UID=${nobodyUid}`,
155+
`NEMOCLAW_TEST_OBSERVED_ACCESS=${observedAccess}`,
156+
`NEMOCLAW_TEST_PRIVATE_DIR=${privateDir}`,
157+
`NEMOCLAW_TEST_RUNTIME_DIR=${runtimeDir}`,
158+
"bash",
159+
"-c",
160+
script,
161+
],
162+
{ encoding: "utf-8", timeout: 10_000 },
163+
);
164+
expect(result.status, result.stderr).toBe(0);
165+
expect(fs.readFileSync(observedAccess, "utf-8")).toBe("denied\n");
166+
expect(mode(runtimeDir)).toBe(0o755);
167+
expect(fs.statSync(runtimeDir).uid).toBe(0);
168+
expect(mode(caBundle)).toBe(0o444);
169+
expect(fs.statSync(caBundle).uid).toBe(0);
170+
expect(mode(privateDir)).toBe(0o700);
171+
expect(fs.statSync(privateDir).uid).toBe(0);
172+
expect(fs.readdirSync(privateDir)).toEqual([]);
173+
} finally {
174+
const cleanup = spawnSync("sudo", ["-n", "/usr/bin/rm", "-rf", "--", root], {
175+
encoding: "utf-8",
176+
});
177+
expect(cleanup.status, cleanup.stderr).toBe(0);
178+
}
179+
},
180+
);
108181
});
109182

110183
describe("nemoclaw-start one-shot command lifecycle", () => {

0 commit comments

Comments
 (0)