Repository navigation
fix(security): isolate gateway process from sandbox agent #721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
8c79539
d8a77d2
0c03d96
5675007
350cba0
e2b955b
e6cc705
c8b8247
63b0d72
72d7d3a
f02d161
c7cd3bf
e80dbf0
cf56a34
24821c8
88f98ce
2f0bf3b
006a916
9614a44
46a822f
e63e942
d6b711e
027b9fc
430ceb9
1dcc184
6e41834
38be700
6f68ca3
0a3242c
4aedfcb
99f8785
153ddc0
a75e55e
5d88963
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| 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 | ||
|
|
||
|
|
@@ -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 \ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These ACL lines are dead code. The
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 [] | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
If backward compat with older images matters, gate it on an env var like |
||||||||
| 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 | ||||||||
|
|
@@ -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}/" | ||||||||
|
|
@@ -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 & | ||||||||
|
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 | ||||||||
|
|
@@ -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 | ||||||||
|
|
@@ -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}') | ||||||||
|
|
@@ -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 | ||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||
Uh oh!
There was an error while loading. Please reload this page.