fix(sandbox): restrict /sandbox to read-only via Landlock (#804)#1121
fix(sandbox): restrict /sandbox to read-only via Landlock (#804)#1121prekshivyas wants to merge 34 commits intoNVIDIA:mainfrom
Conversation
…stem policy (NVIDIA#804) Tighten the Landlock filesystem policy so agents cannot write arbitrary files in the /sandbox home directory. Only explicitly declared paths remain writable (/sandbox/.openclaw-data, /sandbox/.nemoclaw, /tmp). - Set include_workdir to false (verified against OpenShell landlock.rs: when true, WORKDIR is added to read_write, overriding read_only) - Move /sandbox from read_write to read_only in the policy - Add /sandbox/.nemoclaw to read_write for plugin state/config writes - DAC-protect blueprints with root ownership (defense-in-depth) - Pre-create .bashrc/.profile at build time (read-only home prevents runtime writes); source proxy config from writable proxy-env.sh - Redirect tool dotfiles (npm, git, pip, bash, claude, node) to /tmp via env vars in both the entrypoint and the sourced proxy-env.sh so interactive connect sessions also get the redirects Closes NVIDIA#804
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughBuild-time hardening pre-creates and DAC-protects Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
…proach The entrypoint no longer writes proxy config directly to ~/.bashrc (read-only home). Tests now verify that proxy-env.sh is written to the writable data dir and that .bashrc sourcing works correctly.
The sed-extracted block contains the path in comments before the variable assignment. replace() only swaps the first occurrence (the comment), leaving the actual _PROXY_ENV_FILE assignment pointing at /sandbox/.openclaw-data/ which doesn't exist in CI.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
docs/deployment/sandbox-hardening.md (2)
85-86: Keep each sentence on its own source line in this intro.The first sentence is split across two source lines, and the second shares the same line as the end of the first. Please give each sentence its own line. As per coding guidelines, "One sentence per line in source (makes diffs readable). Flag paragraphs where multiple sentences appear on the same line."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/deployment/sandbox-hardening.md` around lines 85 - 86, The intro currently has two sentences on the same source line; split them so each sentence is on its own line: ensure "The sandbox Landlock policy restricts `/sandbox` (the agent's home directory) to read-only access." is one line and "Only explicitly declared directories are writable:" is the following line, updating the text in the same paragraph (no other changes).
103-105: Rewrite this in active voice and keep one sentence per line.
are pre-createdis passive, and the sentence is wrapped across multiple source lines. As per coding guidelines, "Active voice required. Flag passive constructions." and "One sentence per line in source (makes diffs readable). Flag paragraphs where multiple sentences appear on the same line."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/deployment/sandbox-hardening.md` around lines 103 - 105, Rewrite the two-line passive sentence into active voice and ensure each sentence sits on its own source line: change "Shell init files (`.bashrc`, `.profile`) are pre-created at image build time and source runtime proxy configuration from the writable `/sandbox/.openclaw-data/proxy-env.sh`." into two active-voice sentences such as "The image build process pre-creates shell init files `.bashrc` and `.profile`." and "These files source runtime proxy configuration from `/sandbox/.openclaw-data/proxy-env.sh`." Place each sentence on its own line in the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Dockerfile`:
- Around line 151-159: The Dockerfile currently only makes
/sandbox/.nemoclaw/blueprints root-owned; instead ensure the parent is locked:
in the RUN that touches /sandbox/.nemoclaw, set ownership and permissions on the
parent directory (chown root:root /sandbox/.nemoclaw && chmod 755
/sandbox/.nemoclaw) before adjusting the blueprints subtree, then create the
runtime dirs (/sandbox/.nemoclaw/state and /sandbox/.nemoclaw/migration) and
chown those to sandbox:sandbox so only those are writable; update the existing
RUN that uses chown/chmod/mkdir to apply root ownership and 755 permissions to
/sandbox/.nemoclaw itself (and keep /sandbox/.nemoclaw/blueprints root:root) and
then chown only the state and migration dirs to sandbox.
In `@scripts/nemoclaw-start.sh`:
- Around line 248-273: The proxy env file is written into a sandbox-writable
directory (_PROXY_ENV_FILE="/sandbox/.openclaw-data/proxy-env.sh") which allows
a sandbox user to replace it with malicious shell code; instead write the proxy
env to a non-user-writable, root-owned location (for example create and use a
system-owned directory like /etc/openclaw or /var/lib/openclaw and set
ownership/mode) and update whatever startup/profile sourcing to point at that
path; ensure the write is done atomically and safely (create a temporary file in
the root-owned dir, set owner to root, chmod 0644, then rename into place) and
avoid following attacker symlinks (use safe file creation APIs or the install
command rather than plain cat > "$_PROXY_ENV_FILE"); also remove or stop
auto-sourcing any file from the sandbox-writable tree so agent-controlled files
cannot be executed at session startup.
---
Nitpick comments:
In `@docs/deployment/sandbox-hardening.md`:
- Around line 85-86: The intro currently has two sentences on the same source
line; split them so each sentence is on its own line: ensure "The sandbox
Landlock policy restricts `/sandbox` (the agent's home directory) to read-only
access." is one line and "Only explicitly declared directories are writable:" is
the following line, updating the text in the same paragraph (no other changes).
- Around line 103-105: Rewrite the two-line passive sentence into active voice
and ensure each sentence sits on its own source line: change "Shell init files
(`.bashrc`, `.profile`) are pre-created at image build time and source runtime
proxy configuration from the writable `/sandbox/.openclaw-data/proxy-env.sh`."
into two active-voice sentences such as "The image build process pre-creates
shell init files `.bashrc` and `.profile`." and "These files source runtime
proxy configuration from `/sandbox/.openclaw-data/proxy-env.sh`." Place each
sentence on its own line in the file.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6c35344c-d7f9-4676-92dd-982615431c01
📒 Files selected for processing (5)
DockerfileDockerfile.basedocs/deployment/sandbox-hardening.mdnemoclaw-blueprint/policies/openclaw-sandbox.yamlscripts/nemoclaw-start.sh
There was a problem hiding this comment.
🧹 Nitpick comments (1)
test/service-env.test.js (1)
187-225: Add guard for empty sed extraction to improve debuggability.Unlike the
extractProxyVarshelper (lines 110-115) which throws a descriptive error when the sed extraction fails, this test would fail with a confusingENOENTerror at line 211 if the script structure changes andpersistBlockis empty.🛠️ Proposed fix to add consistency with existing pattern
const persistBlock = execFileSync( "sed", ["-n", "/^_PROXY_URL=/,/^chmod 644/p", scriptPath], { encoding: "utf-8" } ); + if (!persistBlock.trim()) { + throw new Error( + "Failed to extract proxy persistence block from scripts/nemoclaw-start.sh — " + + "the _PROXY_URL..chmod block may have been moved or renamed" + ); + } const wrapper = [🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/service-env.test.js` around lines 187 - 225, The test "entrypoint writes proxy-env.sh to writable data dir" extracts a persistBlock via sed but doesn't guard against an empty result, causing a confusing ENOENT later; add the same defensive check used by extractProxyVars (throw a descriptive error when the sed extraction returns an empty string) before writing/executing tmpFile so failures in script structure are reported clearly—specifically check the persistBlock variable after the execFileSync sed call in this test and throw or assert with a helpful message if it's empty (refer to persistBlock and the extractProxyVars pattern for the exact guard behavior).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@test/service-env.test.js`:
- Around line 187-225: The test "entrypoint writes proxy-env.sh to writable data
dir" extracts a persistBlock via sed but doesn't guard against an empty result,
causing a confusing ENOENT later; add the same defensive check used by
extractProxyVars (throw a descriptive error when the sed extraction returns an
empty string) before writing/executing tmpFile so failures in script structure
are reported clearly—specifically check the persistBlock variable after the
execFileSync sed call in this test and throw or assert with a helpful message if
it's empty (refer to persistBlock and the extractProxyVars pattern for the exact
guard behavior).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 21f50e8b-a606-4eeb-881c-034528b7744f
📒 Files selected for processing (1)
test/service-env.test.js
Address CodeRabbit review findings: - Lock /sandbox/.nemoclaw parent directory (root:root 755) so the agent cannot rename or replace the root-owned blueprints directory - Pre-create config.json and snapshots/ as sandbox-owned for runtime writes - Move proxy-env.sh from sandbox-writable .openclaw-data to /tmp where sticky-bit protection prevents the sandbox user from tampering with the root-owned file - Add rm -f before write to prevent symlink-following attacks - Add empty sed extraction guards in proxy persistence tests - Fix docs: one sentence per line, active voice Ref: NVIDIA#804
|
✨ Thanks for submitting this PR with a detailed summary, it proposes restricting the sandbox environment to address a potential security issue. |
…DIA#804) DAC tests (Docker-only, test/e2e-gateway-isolation.sh): - Tests 13-25: verify sandbox user cannot write to /sandbox, .nemoclaw parent, blueprints, .openclaw dir; verify sandbox CAN write to state, migration, snapshots, staging, config.json, .openclaw-data - Fix test 9: add missing `memory` symlink to verification list Landlock tests (OpenShell/Brev, checks/04-landlock-readonly.sh): - 8 tests verifying kernel-level read-only enforcement on /sandbox - Closes DAC gap: .bashrc/.profile are sandbox-owned but Landlock read_only prevents agent from injecting malicious env vars Signed-off-by: Prekshi Vimadalal <pvimadalal@nvidia.com>
…DIA#804) /sandbox is sandbox-owned (DAC allows writes). Read-only enforcement comes from Landlock at runtime, which is tested in the Brev e2e suite (checks/04-landlock-readonly.sh). Renumber remaining tests 13-24. Signed-off-by: Prekshi Vimadalal <pvimadalal@nvidia.com>
…IDIA#804) The base image on GHCR hasn't been rebuilt with pre-baked shell init files yet. Skip tests 23-24 gracefully instead of failing when the files don't exist. Tests will auto-activate after base image rebuild. Signed-off-by: Prekshi Vimadalal <pvimadalal@nvidia.com>
Signed-off-by: Prekshi Vimadalal <pvimadalal@nvidia.com>
Signed-off-by: Prekshi Vimadalal <pvimadalal@nvidia.com>
|
Nice work on the hardening, @prekshivyas. A few regression concerns we should validate before merging: OpenClaw agent testing needed — This changes the fundamental write surface available to agents at runtime. We need to verify OpenClaw's own tool use (pip, wget, python REPL, git-over-SSH, etc.) doesn't write dotfiles outside the redirect list. Any uncovered dotfile write will hard-fail under Landlock. Can we get an OpenClaw team member to run a representative agent session against a sandbox built from this branch? DAC gap for credentials.json and managed_swap — Base image rebuild sequencing — |
Re: DAC gap for
|
Re: Base image rebuild sequencingExisting running sandboxes are completely unaffected. Docker containers are snapshots — a running container doesn't auto-update when a new image is pushed to GHCR. Users would have to explicitly tear down and recreate their sandbox ( Agent processes are NOT affected even on a stale base image — the entrypoint ( export npm_config_cache="/tmp/.npm-cache"
export XDG_CACHE_HOME="/tmp/.cache"
export HISTFILE="/tmp/.bash_history"
export GIT_CONFIG_GLOBAL="/tmp/.gitconfig"
export PYTHONUSERBASE="/tmp/.local"Every child process (gateway, agent, tools) inherits these. No Only Rebuild is automated. The CI race on merge: No stale sandbox image is published — the workflow only builds locally for testing. Summary: Existing users unaffected. Agent functionality unaffected on any base image. Only manual |
…DIA#804) With Landlock enforcing /sandbox as read-only, tools that write to XDG base directories or tool-specific dotfiles under ~ fail at runtime. Adds redirects for all remaining gaps: - XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_STATE_HOME, XDG_RUNTIME_DIR - GNUPGHOME (git GPG probing), PYTHONHISTFILE (python3 REPL) - npm_config_prefix (npm install -g inside sandbox) Applied in both entrypoint exports and proxy-env.sh heredoc to cover gateway children and interactive shell sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…VIDIA#804) OpenClaw gateway writes logs, credentials, and sandbox state to ~/.openclaw/ at runtime. These directories had no symlinks to the writable .openclaw-data, causing silent write failures under the read-only .openclaw policy. Add symlinks for logs/, credentials/, sandbox/ in Dockerfile.base so these writes resolve to the writable .openclaw-data tree. Also pre-create all XDG redirect directories as sandbox:sandbox in the entrypoint before the gateway starts. Without this, the gateway (running as gateway user) could create them first with gateway:gateway ownership, blocking subsequent sandbox user writes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Re: OpenClaw agent testing neededAgreed — filed nvbug 6042599 as a QA test request covering this PR. It includes 24 test cases across 5 categories:
On the dotfile coverage gap concern: We audited every binary installed in the sandbox image (apt dependency tree from Additionally, commit 6c61285 pre-creates all redirected directories as |
NVIDIA#804) The GHCR base image may not have the logs/credentials/sandbox symlinks added in 6c61285 until it is rebuilt. Create them idempotently in the production Dockerfile so CI passes against the stale base. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…VIDIA#804) openclaw doctor --fix creates a real /sandbox/.openclaw/logs directory before our symlink layer runs. Replace any existing non-symlink entries with symlinks to .openclaw-data, preserving contents. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Restricts the
/sandboxhome directory to Landlock read-only, preventing agents from creating arbitrary files or modifying their runtime environment. Only explicitly declared paths remain writable.Key changes:
include_workdir: falsein the filesystem policy — verified against OpenShell'slandlock.rsthatinclude_workdir: trueadds WORKDIR toread_write, which would override ourread_onlyentry (Landlock grants the union of all matching rules)/sandboxfromread_writetoread_only/sandbox/.openclaw-data(agent state) and/sandbox/.nemoclaw(plugin state) asread_write/sandbox/.nemoclawparent (root:root 755) so the agent cannot rename or replace the root-ownedblueprints/directory. Onlystate/,migration/,snapshots/,staging/, andconfig.jsonare sandbox-owned for runtime writes..bashrc/.profileat image build time — they source proxy config from/tmp/nemoclaw-proxy-env.sh(sticky-bit protected, root-owned in root mode)/tmpinstead of sandbox-writable.openclaw-datato prevent agent content injection;rm -fbefore write prevents symlink-following attacks/tmpvia env vars in both the entrypoint and the sourcedproxy-env.sh(soopenshell sandbox connectsessions also get the redirects)Writable surface after this change:
/sandbox/sandbox/.openclaw/sandbox/.openclaw-data/sandbox/.nemoclaw/tmpRelated Issue
Closes #804
Changes
nemoclaw-blueprint/policies/openclaw-sandbox.yamlinclude_workdir: false,/sandbox→ read_only,/sandbox/.nemoclaw→ read_writeDockerfile.nemoclawparent + blueprints (root ownership), pre-create state/migration/snapshots/staging dirs and config.jsonDockerfile.base.bashrc/.profilesourcing/tmp/nemoclaw-proxy-env.shscripts/nemoclaw-start.sh/tmp/nemoclaw-proxy-env.shwith symlink protection, redirect tool dotfiles to/tmpdocs/deployment/sandbox-hardening.mdtest/service-env.test.jsTesting
nemoclaw onboardcompletes successfully (sandbox creation with new policy)openshell sandbox connect→ interactive shell works, proxy env vars are set/sandbox/.openclaw-data/workspace)/sandbox/(e.g.,touch /sandbox/testfails)openclaw gateway runstarts correctly (reads from read-only.openclaw/)/sandbox/.nemoclaw/state/)/sandbox/.nemoclaw/snapshots/,/sandbox/.nemoclaw/staging/)/sandbox/.nemoclaw/blueprints/is root-owned, parent is root-owned)Signed-off-by: Prekshi Vyas prekshivyas@gmail.com