Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 49 additions & 17 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,52 @@ verify_config_integrity() {
fi
}

validate_openclaw_symlinks() {
local entry name target expected
for entry in /sandbox/.openclaw/*; do
[ -L "$entry" ] || continue
name="$(basename "$entry")"
target="$(readlink -f "$entry" 2>/dev/null || true)"
expected="/sandbox/.openclaw-data/$name"
if [ "$target" != "$expected" ]; then
echo "[SECURITY] Symlink $entry points to unexpected target: $target (expected $expected)" >&2
return 1
fi
done
}

harden_openclaw_symlinks() {
local entry hardened failed
hardened=0
failed=0

if ! command -v chattr >/dev/null 2>&1; then
echo "[SECURITY] chattr not available — relying on DAC + Landlock for .openclaw hardening" >&2
return 0
fi

if chattr +i /sandbox/.openclaw 2>/dev/null; then
hardened=$((hardened + 1))
else
failed=$((failed + 1))
fi

for entry in /sandbox/.openclaw/*; do
[ -L "$entry" ] || continue
if chattr +i "$entry" 2>/dev/null; then
hardened=$((hardened + 1))
else
failed=$((failed + 1))
fi
done

if [ "$failed" -gt 0 ]; then
echo "[SECURITY] Immutable hardening applied to $hardened path(s); $failed path(s) could not be hardened — continuing with DAC + Landlock" >&2
elif [ "$hardened" -gt 0 ]; then
echo "[SECURITY] Immutable hardening applied to /sandbox/.openclaw and validated symlinks" >&2
fi
}

write_auth_profile() {
if [ -z "${NVIDIA_API_KEY:-}" ]; then
return
Expand Down Expand Up @@ -365,6 +411,7 @@ if [ "$(id -u)" -ne 0 ]; then
exit 1
fi
configure_messaging_channels
validate_openclaw_symlinks
write_auth_profile

if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then
Expand Down Expand Up @@ -420,29 +467,14 @@ chmod 600 /tmp/auto-pair.log

# Verify ALL symlinks in .openclaw point to expected .openclaw-data targets.
# Dynamic scan so future OpenClaw symlinks are covered automatically.
for entry in /sandbox/.openclaw/*; do
[ -L "$entry" ] || continue
name="$(basename "$entry")"
target="$(readlink -f "$entry" 2>/dev/null || true)"
expected="/sandbox/.openclaw-data/$name"
if [ "$target" != "$expected" ]; then
echo "[SECURITY] Symlink $entry points to unexpected target: $target (expected $expected)" >&2
exit 1
fi
done
validate_openclaw_symlinks

# Lock .openclaw directory after symlink validation: set the immutable flag
# so symlinks cannot be swapped at runtime even if DAC or Landlock are
# bypassed. chattr requires cap_linux_immutable which the entrypoint has
# as root; the sandbox user cannot remove the flag.
# Ref: https://github.com/NVIDIA/NemoClaw/issues/1019
if command -v chattr >/dev/null 2>&1; then
chattr +i /sandbox/.openclaw 2>/dev/null || true
for entry in /sandbox/.openclaw/*; do
[ -L "$entry" ] || continue
chattr +i "$entry" 2>/dev/null || true
done
fi
harden_openclaw_symlinks

# Start the gateway as the 'gateway' user.
# SECURITY: The sandbox user cannot kill this process because it runs
Expand Down
18 changes: 14 additions & 4 deletions test/e2e-gateway-isolation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,19 @@ else
fail "iptables not found — sandbox network policies will not be enforced: $OUT"
fi

# ── Test 11: Sandbox user cannot kill gateway-user processes ─────
# ── Test 11: chattr is available for immutable hardening ─────────

info "11. Sandbox user cannot kill gateway-user processes"
info "11. chattr is available for immutable symlink hardening"
OUT=$(run_as_root "command -v chattr 2>/dev/null || true")
if [ -n "$OUT" ]; then
pass "chattr available at $OUT"
else
fail "chattr not found — nemoclaw-start immutable hardening will be skipped"
fi

# ── Test 12: Sandbox user cannot kill gateway-user processes ─────

info "12. Sandbox user cannot kill gateway-user processes"
# Start a dummy process as gateway, try to kill it as sandbox
OUT=$(docker run --rm --entrypoint "" "$IMAGE" bash -c '
gosu gateway sleep 60 &
Expand All @@ -187,9 +197,9 @@ else
fail "sandbox CAN kill gateway processes: $OUT"
fi

# ── Test 12: Dangerous capabilities are dropped by entrypoint ────
# ── Test 13: Dangerous capabilities are dropped by entrypoint ────

info "12. Entrypoint drops dangerous capabilities from bounding set"
info "13. Entrypoint drops dangerous capabilities from bounding set"
# Run capsh directly with the same --drop flags as the entrypoint, then
# check CapBnd. This avoids running the full entrypoint which starts
# gateway services that fail in CI without a running OpenShell environment.
Expand Down
Loading