Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8c79539
fix(install): prevent nvm/login shell from resetting PATH in subshells
cv Mar 22, 2026
d8a77d2
Merge remote-tracking branch 'upstream/main' into fix/install-subshel…
cv Mar 23, 2026
0c03d96
fix(ci): drop stray '.' arg from pyright pre-push hook
cv Mar 23, 2026
5675007
chore: apply shfmt formatting to shell scripts
cv Mar 23, 2026
350cba0
fix(ci): exclude test files from pyright strict type-checking
cv Mar 23, 2026
e2b955b
fix(ci): drop stray '.' arg from pyright in blueprint Makefile
cv Mar 23, 2026
e6cc705
fix(security): isolate gateway process from sandbox agent
ericksoa Mar 23, 2026
c8b8247
Merge remote-tracking branch 'origin/main' into fix/install-subshell-…
cv Mar 23, 2026
63b0d72
chore: apply shfmt formatting to brev-setup.sh
cv Mar 23, 2026
72d7d3a
fix(security): harden entrypoint against symlink injection, PATH hija…
ericksoa Mar 23, 2026
f02d161
fix(security): pin apt and pip package versions in Dockerfile
cv Mar 23, 2026
c7cd3bf
fix: address CodeRabbit review feedback on gateway isolation
ericksoa Mar 23, 2026
e80dbf0
chore: merge upstream/main and resolve conflicts
cv Mar 23, 2026
cf56a34
test: add E2E test for gateway process isolation (10 checks)
ericksoa Mar 23, 2026
24821c8
fix: remove hadolint ignore and quote pyyaml version pin
cv Mar 23, 2026
88f98ce
fix: prevent infinite recursion when openshell passes nemoclaw-start …
ericksoa Mar 23, 2026
2f0bf3b
fix: document -h short alias in usage output
cv Mar 23, 2026
006a916
fix: suppress hadolint DL3002 — root USER is intentional for gosu pri…
ericksoa Mar 23, 2026
9614a44
Merge branch 'pr-651-temp' into fix/gateway-process-isolation
ericksoa Mar 23, 2026
46a822f
chore: consolidate apt-get layers (merge gosu into main install)
ericksoa Mar 23, 2026
e63e942
fix: address review feedback on gateway isolation PR
ericksoa Mar 24, 2026
d6b711e
merge: resolve conflicts with main
ericksoa Mar 24, 2026
027b9fc
ci: allow 'merge' type in commitlint config
cv Mar 24, 2026
430ceb9
fix: add hadolint ignore for intentional root USER and fix CI image b…
ericksoa Mar 24, 2026
1dcc184
Merge branch 'fix/gateway-process-isolation' of https://github.com/NV…
ericksoa Mar 24, 2026
6e41834
fix: resolve remaining CI failures and address symlink review comment
cv Mar 24, 2026
38be700
Merge branch 'fix/gateway-process-isolation' of github.com:NVIDIA/Nem…
cv Mar 24, 2026
6f68ca3
chore: apply shfmt formatting
ericksoa Mar 24, 2026
0a3242c
merge: resolve shfmt formatting conflict
ericksoa Mar 24, 2026
4aedfcb
fix: upgrade gosu from 1.14 (Debian bookworm) to 1.19 (2025-09)
ericksoa Mar 24, 2026
99f8785
fix: add hadolint DL4006 ignore for gosu checksum pipe
ericksoa Mar 24, 2026
153ddc0
fix: apply shfmt indent to case pattern in nemoclaw-start
cv Mar 24, 2026
a75e55e
chore: re-sign with verified SSH key
ericksoa Mar 24, 2026
5d88963
Merge branch 'fix/gateway-process-isolation' of https://github.com/NV…
ericksoa Mar 24, 2026
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
37 changes: 32 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
iproute2 \
&& rm -rf /var/lib/apt/lists/*

# Create sandbox user (matches OpenShell convention)
RUN groupadd -r sandbox && useradd -r -g sandbox -d /sandbox -s /bin/bash sandbox \
# Install gosu for privilege separation (gateway vs sandbox user)
# hadolint ignore=DL3008
Comment thread
cv marked this conversation as resolved.
Outdated
RUN apt-get update && apt-get install -y --no-install-recommends gosu \
&& rm -rf /var/lib/apt/lists/*

# Create sandbox user (matches OpenShell convention) and gateway user.
# The gateway runs as 'gateway' so the 'sandbox' user (agent) cannot
# kill it or restart it with a tampered HOME/config.
RUN groupadd -r gateway && useradd -r -g gateway -d /sandbox -s /usr/sbin/nologin gateway \
&& groupadd -r sandbox && useradd -r -g sandbox -d /sandbox -s /bin/bash sandbox \
&& mkdir -p /sandbox/.nemoclaw \
&& chown -R sandbox:sandbox /sandbox

Expand Down Expand Up @@ -146,12 +154,31 @@ RUN openclaw doctor --fix > /dev/null 2>&1 || true \
# The Landlock policy (/sandbox/.openclaw in read_only) provides defense-in-depth
# once OpenShell enables enforcement.
# Ref: https://github.com/NVIDIA/NemoClaw/issues/514
# Lock the entire .openclaw directory tree.
# SECURITY: chmod 755 (not 1777) — the sandbox user can READ but not WRITE
# to this directory. This prevents the agent from replacing symlinks
# (e.g., pointing /sandbox/.openclaw/hooks to an attacker-controlled path).
# The writable state lives in .openclaw-data, reached via the symlinks.
USER root
RUN chown root:root /sandbox/.openclaw \
&& find /sandbox/.openclaw -mindepth 1 -maxdepth 1 -exec chown -h root:root {} + \
&& chmod 1777 /sandbox/.openclaw \
&& chmod 755 /sandbox/.openclaw \
&& chmod 444 /sandbox/.openclaw/openclaw.json
USER sandbox

ENTRYPOINT ["/bin/bash"]
# Pin config hash at build time so the entrypoint can verify integrity.
# Prevents the agent from creating a copy with a tampered config and
# restarting the gateway pointing at it.
RUN sha256sum /sandbox/.openclaw/openclaw.json > /sandbox/.openclaw/.config-hash \
&& chmod 444 /sandbox/.openclaw/.config-hash \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These ACL lines are dead code. The acl package isn't installed in the image, so setfacl will always fail. The fallbacks are no-ops:

  • chmod o+rx /sandbox/.openclaw — already 755 from line 159
  • || true — does nothing

Since gateway reads via "other" bits (755 on dir, 444 on file), just delete these.

&& chown root:root /sandbox/.openclaw/.config-hash

# Allow the gateway user to read the openclaw config and data dirs
RUN setfacl -m u:gateway:rx /sandbox/.openclaw 2>/dev/null \
|| chmod o+rx /sandbox/.openclaw
RUN setfacl -m u:gateway:r /sandbox/.openclaw/openclaw.json 2>/dev/null \
|| true

# Entrypoint runs as root to start the gateway as the gateway user,
# then drops to sandbox for agent commands. See nemoclaw-start.sh.
ENTRYPOINT ["/usr/local/bin/nemoclaw-start"]
CMD []
88 changes: 72 additions & 16 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,45 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# NemoClaw sandbox entrypoint. Configures OpenClaw and starts the dashboard
# gateway inside the sandbox so the forwarded host port has a live upstream.
# NemoClaw sandbox entrypoint. Runs as root (via ENTRYPOINT) to start the
# gateway as the 'gateway' user, then drops to 'sandbox' for agent commands.
#
# SECURITY: The gateway runs as a separate user so the sandboxed agent cannot
# kill it or restart it with a tampered config (CVE: fake-HOME bypass).
# The config hash is verified at startup to detect tampering.
#
# Optional env:
# NVIDIA_API_KEY API key for NVIDIA-hosted inference
# CHAT_UI_URL Browser origin that will access the forwarded dashboard

set -euo pipefail

# SECURITY: Lock down PATH so the agent cannot inject malicious binaries
# into commands executed by the entrypoint or auto-pair watcher.
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

NEMOCLAW_CMD=("$@")
CHAT_UI_URL="${CHAT_UI_URL:-http://127.0.0.1:18789}"
PUBLIC_PORT=18789
OPENCLAW="$(command -v openclaw)" # Resolve once, use absolute path everywhere

# ── Config integrity check ──────────────────────────────────────
# The config hash was pinned at build time. If it doesn't match,
# someone (or something) has tampered with the config.

verify_config_integrity() {
local hash_file="/sandbox/.openclaw/.config-hash"
if [ ! -f "$hash_file" ]; then
echo "[SECURITY] Config hash file missing — cannot verify openclaw.json integrity"
return 0 # Don't block on older images that predate this check
fi

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail-open on a missing hash file means the integrity check can be bypassed by simply deleting the hash. For a security fix, fail closed:

Suggested change
fi
echo "[SECURITY] Config hash file missing — refusing to start without integrity verification"
return 1

If backward compat with older images matters, gate it on an env var like NEMOCLAW_ALLOW_MISSING_HASH=1 rather than silently skipping.

if ! (cd /sandbox/.openclaw && sha256sum -c "$hash_file" --status 2>/dev/null); then
echo "[SECURITY] openclaw.json integrity check FAILED — config may have been tampered with"
echo "[SECURITY] Expected hash: $(cat "$hash_file")"
echo "[SECURITY] Actual hash: $(sha256sum /sandbox/.openclaw/openclaw.json)"
return 1
fi
}

write_auth_profile() {
if [ -z "${NVIDIA_API_KEY:-}" ]; then
Expand All @@ -40,19 +67,18 @@ PYAUTH
print_dashboard_urls() {
local token chat_ui_base local_url remote_url

token="$(
python3 - <<'PYTOKEN'
token="$(python3 - <<'PYTOKEN'
import json
import os
path = os.path.expanduser('~/.openclaw/openclaw.json')
path = '/sandbox/.openclaw/openclaw.json'
try:
cfg = json.load(open(path))
except Exception:
print('')
else:
print(cfg.get('gateway', {}).get('auth', {}).get('token', ''))
PYTOKEN
)"
)"

chat_ui_base="${CHAT_UI_URL%/}"
local_url="http://127.0.0.1:${PUBLIC_PORT}/"
Expand All @@ -67,11 +93,15 @@ PYTOKEN
}

start_auto_pair() {
nohup python3 - <<'PYAUTOPAIR' >>/tmp/gateway.log 2>&1 &
# Run auto-pair as sandbox user (it talks to the gateway via CLI)
# SECURITY: Pass resolved openclaw path to prevent PATH hijacking
OPENCLAW_BIN="$OPENCLAW" nohup gosu sandbox python3 - <<'PYAUTOPAIR' >> /tmp/gateway.log 2>&1 &
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
import json
import os
import subprocess
import time

OPENCLAW = os.environ.get('OPENCLAW_BIN', 'openclaw')
DEADLINE = time.time() + 600
QUIET_POLLS = 0
APPROVED = 0
Expand All @@ -81,7 +111,7 @@ def run(*args):
return proc.returncode, proc.stdout.strip(), proc.stderr.strip()

while time.time() < DEADLINE:
rc, out, err = run('openclaw', 'devices', 'list', '--json')
rc, out, err = run(OPENCLAW, 'devices', 'list', '--json')
if rc != 0 or not out:
time.sleep(1)
continue
Expand All @@ -101,7 +131,7 @@ while time.time() < DEADLINE:
request_id = (device or {}).get('requestId')
if not request_id:
continue
arc, aout, aerr = run('openclaw', 'devices', 'approve', request_id, '--json')
arc, aout, aerr = run(OPENCLAW, 'devices', 'approve', request_id, '--json')
if arc == 0:
APPROVED += 1
print(f'[auto-pair] approved request={request_id}')
Expand All @@ -127,17 +157,43 @@ PYAUTOPAIR
echo "[gateway] auto-pair watcher launched (pid $!)"
}

# ── Main ─────────────────────────────────────────────────────────

echo 'Setting up NemoClaw...'
# openclaw doctor --fix and openclaw plugins install already ran at build time
# (Dockerfile Step 28). At runtime they fail with EPERM against the locked
# /sandbox/.openclaw directory and accomplish nothing.
write_auth_profile

# Verify config integrity before starting anything
verify_config_integrity

# Write auth profile as sandbox user (needs writable .openclaw-data)
gosu sandbox bash -c "$(declare -f write_auth_profile); write_auth_profile"

# If a command was passed (e.g., "openclaw agent ..."), run it as sandbox user
if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then
exec "${NEMOCLAW_CMD[@]}"
exec gosu sandbox "${NEMOCLAW_CMD[@]}"
fi

nohup openclaw gateway run >/tmp/gateway.log 2>&1 &
echo "[gateway] openclaw gateway launched (pid $!)"
# SECURITY: Protect gateway log from sandbox user tampering
touch /tmp/gateway.log
chown gateway:gateway /tmp/gateway.log
chmod 600 /tmp/gateway.log

# Verify symlinks in .openclaw point to expected targets (prevent symlink injection)
for link in agents extensions workspace skills hooks identity devices canvas cron; do
target="$(readlink -f "/sandbox/.openclaw/$link" 2>/dev/null || true)"
expected="/sandbox/.openclaw-data/$link"
if [ "$target" != "$expected" ]; then
echo "[SECURITY] Symlink /sandbox/.openclaw/$link points to unexpected target: $target (expected $expected)"
exit 1
fi
done

# Start the gateway as the 'gateway' user.
# SECURITY: The sandbox user cannot kill this process because it runs
# under a different UID. The fake-HOME attack no longer works because
# the agent cannot restart the gateway with a tampered config.
nohup gosu gateway "$OPENCLAW" gateway run > /tmp/gateway.log 2>&1 &
GATEWAY_PID=$!
echo "[gateway] openclaw gateway launched as 'gateway' user (pid $GATEWAY_PID)"

start_auto_pair
print_dashboard_urls
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading