@@ -49,6 +49,27 @@ function replaceRequired(source: string, target: string, replacement: string): s
4949const oneShotFunction = extractShellFunction ( "run_oneshot_command" ) ;
5050const resolveNormalizerFunction = extractShellFunction ( "resolve_mutable_config_normalizer" ) ;
5151const 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
5374describe ( "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
110183describe ( "nemoclaw-start one-shot command lifecycle" , ( ) => {
0 commit comments