Skip to content

fix(sandbox): restrict /sandbox to read-only via Landlock (#804)#1121

Open
prekshivyas wants to merge 34 commits intoNVIDIA:mainfrom
prekshivyas:security/read-only-sandbox-filesystem
Open

fix(sandbox): restrict /sandbox to read-only via Landlock (#804)#1121
prekshivyas wants to merge 34 commits intoNVIDIA:mainfrom
prekshivyas:security/read-only-sandbox-filesystem

Conversation

@prekshivyas
Copy link
Copy Markdown
Contributor

@prekshivyas prekshivyas commented Mar 30, 2026

Summary

Restricts the /sandbox home directory to Landlock read-only, preventing agents from creating arbitrary files or modifying their runtime environment. Only explicitly declared paths remain writable.

Key changes:

  • Set include_workdir: false in the filesystem policy — verified against OpenShell's landlock.rs that include_workdir: true adds WORKDIR to read_write, which would override our read_only entry (Landlock grants the union of all matching rules)
  • Move /sandbox from read_write to read_only
  • Keep /sandbox/.openclaw-data (agent state) and /sandbox/.nemoclaw (plugin state) as read_write
  • DAC-protect /sandbox/.nemoclaw parent (root:root 755) so the agent cannot rename or replace the root-owned blueprints/ directory. Only state/, migration/, snapshots/, staging/, and config.json are sandbox-owned for runtime writes.
  • Pre-create .bashrc/.profile at image build time — they source proxy config from /tmp/nemoclaw-proxy-env.sh (sticky-bit protected, root-owned in root mode)
  • Write proxy-env.sh to /tmp instead of sandbox-writable .openclaw-data to prevent agent content injection; rm -f before write prevents symlink-following attacks
  • 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 openshell sandbox connect sessions also get the redirects)

Writable surface after this change:

Path Access Purpose
/sandbox read-only Home directory
/sandbox/.openclaw read-only Immutable gateway config
/sandbox/.openclaw-data read-write Agent state, workspace, plugins (via symlinks)
/sandbox/.nemoclaw read-write (Landlock) / root-owned (DAC) Plugin runtime dirs; parent and blueprints are root-owned
/tmp read-write Temp files, logs, tool caches, proxy-env.sh

Related Issue

Closes #804

Changes

File Change
nemoclaw-blueprint/policies/openclaw-sandbox.yaml include_workdir: false, /sandbox → read_only, /sandbox/.nemoclaw → read_write
Dockerfile DAC-protect .nemoclaw parent + blueprints (root ownership), pre-create state/migration/snapshots/staging dirs and config.json
Dockerfile.base Pre-create .bashrc/.profile sourcing /tmp/nemoclaw-proxy-env.sh
scripts/nemoclaw-start.sh Write proxy config to /tmp/nemoclaw-proxy-env.sh with symlink protection, redirect tool dotfiles to /tmp
docs/deployment/sandbox-hardening.md New "Read-Only Home Directory" section
test/service-env.test.js Updated proxy persistence tests for new path, added empty sed extraction guards

Testing

  • nemoclaw onboard completes successfully (sandbox creation with new policy)
  • openshell sandbox connect → interactive shell works, proxy env vars are set
  • Agent can write to workspace (/sandbox/.openclaw-data/workspace)
  • Agent cannot create files directly in /sandbox/ (e.g., touch /sandbox/test fails)
  • openclaw gateway run starts correctly (reads from read-only .openclaw/)
  • Plugin state persists across agent invocations (/sandbox/.nemoclaw/state/)
  • Snapshots and staging work (/sandbox/.nemoclaw/snapshots/, /sandbox/.nemoclaw/staging/)
  • Agent cannot rename or delete blueprints (/sandbox/.nemoclaw/blueprints/ is root-owned, parent is root-owned)

Signed-off-by: Prekshi Vyas prekshivyas@gmail.com

…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
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Build-time hardening pre-creates and DAC-protects /sandbox/.nemoclaw and shell init files; runtime entrypoint writes /tmp/nemoclaw-proxy-env.sh and redirects caches to /tmp; OpenShell/Landlock policy makes /sandbox read-only with only explicit subpaths writable. (48 words)

Changes

Cohort / File(s) Summary
Docker image build
Dockerfile, Dockerfile.base
Create /sandbox/.nemoclaw and subdirs; set ownership/perms (root:root 755 for blueprint dirs, sandbox:sandbox for runtime-writable subdirs); ensure /sandbox/.nemoclaw/config.json exists; pre-create /sandbox/.bashrc and /sandbox/.profile owned by sandbox:sandbox.
Sandbox policy
nemoclaw-blueprint/policies/openclaw-sandbox.yaml
Set include_workdir: false; mark /sandbox as read_only; remove broad /sandbox from read_write and add explicit /sandbox/.nemoclaw as read_write.
Entrypoint / startup script
scripts/nemoclaw-start.sh
Stop in-place edits to ~/.bashrc/~/.profile; emit /tmp/nemoclaw-proxy-env.sh containing proxy + cache/history/git/python env exports and chmod 644; make .env chmod failures non-fatal.
Documentation
docs/deployment/sandbox-hardening.md
Add "Read-Only Home Directory" section describing Landlock/OpenShell restrictions, allowed writable subpaths, prevented persistence vectors, pre-creation of shell init files; add reference to issue #804.
Tests
test/service-env.test.js
Switch tests to validate generated proxy-env.sh in a temp writable location (instead of modifying ~/.bashrc/~/.profile); update assertions for proxy values, cache redirect vars, idempotency, and gateway IP handling.

Sequence Diagram(s)

mermaid
sequenceDiagram
rect rgba(200,200,255,0.5)
participant Builder as Image Build
end
rect rgba(200,255,200,0.5)
participant Entrypoint as Container Entrypoint
end
rect rgba(255,200,200,0.5)
participant Policy as OpenShell/Landlock Policy
end
rect rgba(255,255,200,0.5)
participant Agent as Agent Process
end
Builder->>Filesystem: Create /sandbox/.bashrc, /sandbox/.profile, /sandbox/.nemoclaw/* and set owners/perms
Entrypoint->>Filesystem: Write /tmp/nemoclaw-proxy-env.sh (proxy + cache/history/git/python exports) and chmod 644
Entrypoint->>Env: Export cache/history/git/python paths pointing to /tmp
Policy->>Agent: Enforce /sandbox read-only, allow explicit writable subpaths
Agent->>Filesystem: Source /tmp/nemoclaw-proxy-env.sh and read allowed files
Agent->>Filesystem: Write only to declared writable subpaths (/sandbox/.nemoclaw, /sandbox/.openclaw-data, /tmp)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Poem

🐰
I nudged the burrow, locked the blueprint chest,
Pre-made my shells so temp-proxy can rest,
Read-only tunnels, a few doors left to peep,
Safe crumbs in /tmp where caches softly sleep,
Hop in — the sandbox guards dreams while you sleep.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: restricting /sandbox to read-only via Landlock, with clear reference to issue #804.
Linked Issues check ✅ Passed The PR addresses all coding objectives from issue #804: read-only root filesystem, restricted writable paths, blueprint DAC hardening, environment variable redirects, and comprehensive filesystem policy updates.
Out of Scope Changes check ✅ Passed All changes directly support the read-only sandbox objective: Landlock policy updates, Docker build hardening, entrypoint proxy/cache redirects, tests for proxy config, and documentation of the new security model.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@prekshivyas prekshivyas changed the title security(sandbox): restrict /sandbox to read-only via Landlock (#804) fix(sandbox): restrict /sandbox to read-only via Landlock (#804) Mar 30, 2026
…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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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-created is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 711b98e and ca44773.

📒 Files selected for processing (5)
  • Dockerfile
  • Dockerfile.base
  • docs/deployment/sandbox-hardening.md
  • nemoclaw-blueprint/policies/openclaw-sandbox.yaml
  • scripts/nemoclaw-start.sh

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/service-env.test.js (1)

187-225: Add guard for empty sed extraction to improve debuggability.

Unlike the extractProxyVars helper (lines 110-115) which throws a descriptive error when the sed extraction fails, this test would fail with a confusing ENOENT error at line 211 if the script structure changes and persistBlock is 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

📥 Commits

Reviewing files that changed from the base of the PR and between ca44773 and 7c42034.

📒 Files selected for processing (1)
  • test/service-env.test.js

prekshivyas and others added 6 commits March 30, 2026 16:36
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
@wscurran wscurran added security Something isn't secure priority: high Important issue that should be resolved in the next release fix labels Mar 31, 2026
@wscurran
Copy link
Copy Markdown
Contributor

✨ Thanks for submitting this PR with a detailed summary, it proposes restricting the sandbox environment to address a potential security issue.

prekshivyas and others added 6 commits March 31, 2026 11:58
…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>
@cv cv removed their assignment Mar 31, 2026
@ericksoa
Copy link
Copy Markdown
Contributor

ericksoa commented Apr 1, 2026

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_swapbin/lib/credentials.js writes ~/.nemoclaw/credentials.json and bin/lib/preflight.js writes ~/.nemoclaw/managed_swap, but the Dockerfile only pre-creates state/, migration/, snapshots/, staging/, and config.json as sandbox-owned. These two files will fail at the DAC layer (parent is root:root 755). They should either be pre-created in the Dockerfile or moved into state/.

Base image rebuild sequencing.bashrc/.profile are in Dockerfile.base. Until the base image is rebuilt on GHCR, deployed sandboxes will silently lose proxy config on openshell sandbox connect. What's the rebuild plan?

@prekshivyas
Copy link
Copy Markdown
Contributor Author

Re: DAC gap for credentials.json and managed_swap

Investigated — these don't need pre-creation because both files are written on the host, never inside the sandbox.

credentials.json — Only imported by host-side CLI code:

  • bin/nemoclaw.js:40require("./lib/credentials")
  • bin/lib/onboard.js:51require("./credentials")
  • saveCredential() is called from ensureApiKey() during nemoclaw onboard, which runs as the host user

Zero imports from sandbox code:

$ grep -rn 'require.*credentials\|from.*credentials' nemoclaw/src/ scripts/
(no matches)

The API key never enters the sandbox via credentials.json — it's proxied through the OpenShell gateway which injects credentials server-side. The key is even explicitly deleted from the sandbox env (bin/lib/onboard.js:2153-2161):

// Only pass non-sensitive env vars to the sandbox. NVIDIA_API_KEY is NOT
// needed inside the sandbox — inference is proxied through the OpenShell
// gateway which injects the stored credential server-side.
const sandboxEnv = { ...process.env };
delete sandboxEnv.NVIDIA_API_KEY;

managed_swap — Written by writeManagedSwapMarker() in src/lib/preflight.ts:302-313, called from ensureSwap() at bin/lib/onboard.js:1868 during host-side preflight. Only read by uninstall.sh:537 (also host-side). No sandbox code references it.

Summary: Both files live in the host user's ~/.nemoclaw/, not /sandbox/.nemoclaw/. The DAC layer inside the container doesn't apply.

@prekshivyas
Copy link
Copy Markdown
Contributor Author

Re: Base image rebuild sequencing

Existing 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 (nemoclaw onboard) to pick up any changes.

Agent processes are NOT affected even on a stale base image — the entrypoint (nemoclaw-start.sh:39-45) directly exports all proxy vars and cache redirects into its process:

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 .bashrc needed for the runtime path.

Only openshell sandbox connect is affected on a stale base — interactive bash sessions source ~/.bashrc to load proxy config from /tmp/nemoclaw-proxy-env.sh. On an old base image, .bashrc doesn't exist yet, so those sessions get OpenShell's narrow NO_PROXY and no cache redirects.

Rebuild is automated. The base-image.yaml workflow auto-triggers on push to main when Dockerfile.base changes. Once that CI run completes and pushes to GHCR, all subsequent builds pull the new base with .bashrc/.profile.

CI race on merge: main.yaml (sandbox build + E2E) and base-image.yaml both trigger on the same push. The sandbox build uses resolve-sandbox-base-image which pulls :latest from GHCR — if the base image rebuild hasn't finished yet, it silently gets the old base (the pull succeeds, just stale). E2E tests 23-24 in test/e2e-gateway-isolation.sh handle this gracefully:

SKIP: .bashrc not present (base image needs rebuild for #804)

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 openshell sandbox connect sessions are degraded until the user rebuilds their sandbox with the updated base image.

prekshivyas and others added 3 commits April 1, 2026 20:35
…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>
@prekshivyas
Copy link
Copy Markdown
Contributor Author

Re: OpenClaw agent testing needed

Agreed — filed nvbug 6042599 as a QA test request covering this PR. It includes 24 test cases across 5 categories:

  1. Critical (TC-1–6): Gateway startup, agent prompt response, pairing, pip install, Python REPL, git clone — all verifying writes land in /tmp via XDG redirects
  2. Functional (TC-7–12): npm cache, Node REPL history, XDG dir ownership, GPG home, bash history, proxy-env sourcing in interactive shells
  3. Security (TC-13–17): Cannot write to /sandbox, openclaw.json, .openclaw symlinks, .openclaw dir, or blueprints
  4. Regression (TC-18–22): Re-run existing SWQA tests T120–T124 (OpenClaw Capabilities) and T51–T53 (Sandbox Security)
  5. Interactive (TC-23–24): .bashrc/.profile source proxy env correctly

On the dotfile coverage gap concern: We audited every binary installed in the sandbox image (apt dependency tree from node:22-slim + our apt-get install list). Tools like less, ssh, wget, vim are NOT installed — confirmed in a clean container. The 14 XDG/tool redirects in commits f31a60a and 6c61285 cover all installed tools that write to home:

npm_config_cache, XDG_CACHE_HOME, XDG_CONFIG_HOME, XDG_DATA_HOME,
XDG_STATE_HOME, XDG_RUNTIME_DIR, NODE_REPL_HISTORY, HISTFILE,
GIT_CONFIG_GLOBAL, GNUPGHOME, PYTHONUSERBASE, PYTHONHISTFILE,
CLAUDE_CONFIG_DIR, npm_config_prefix

Additionally, commit 6c61285 pre-creates all redirected directories as sandbox:sandbox via install -d to prevent an ownership race where the gateway (starting first as gateway user) could create dirs before the sandbox user.

prekshivyas and others added 5 commits April 2, 2026 09:24
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix priority: high Important issue that should be resolved in the next release security Something isn't secure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SECURITY] No read-only root filesystem — writable /sandbox increases attack surface

5 participants