Skip to content

fix: lock gateway config via Landlock filesystem policy#555

Merged
ericksoa merged 7 commits into
mainfrom
fix/policy-lockdown-514
Mar 21, 2026
Merged

fix: lock gateway config via Landlock filesystem policy#555
ericksoa merged 7 commits into
mainfrom
fix/policy-lockdown-514

Conversation

@ericksoa

@ericksoa ericksoa commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Prevents the sandboxed AI agent from modifying gateway security settings (openclaw.json) at runtime. All configuration — including gateway mode, CORS origins, trusted proxies, and auth token — is now pre-baked at Docker build time and made immutable via filesystem permissions.

Closes #514. Supersedes the approach in #515.

How it works

Pre-baked config: The complete openclaw.json is written at Docker build time with a per-build auth token (secrets.token_hex(32)). The runtime fix_openclaw_config() function is removed — no runtime writes to the config are needed.

Immutable config file: After all build-time writes complete, the config file is set to root:root 444. The sandbox process (running as sandbox user) cannot modify it.

Protected directory: The parent directory uses sticky bit permissions (root:root 1777, same semantics as /tmp). The sandbox user can create and manage their own files but cannot delete root-owned files.

Writable agent state: Runtime state (plugins, agent profiles, etc.) is relocated to /sandbox/.openclaw-data/ via symlinks baked into the image. Writes through symlinks resolve to the writable target directory.

Filesystem policy (defense-in-depth): /sandbox/.openclaw is added to the Landlock read_only list for additional enforcement when available.

Changes

File Change
Dockerfile Restructure .openclaw with symlinks to .openclaw-data; pre-bake complete config with gateway section + auth token; root:root 1777 on directory, root:root 444 on config
scripts/nemoclaw-start.sh Remove fix_openclaw_config() — no runtime config writes
nemoclaw-blueprint/policies/openclaw-sandbox.yaml Add /sandbox/.openclaw to read_only, /sandbox/.openclaw-data to read_write

Test results

11/11 tests pass across both Docker and OpenShell sandbox environments:

Category Tests Result
Build and structure Image builds, permissions correct, symlinks resolve PASS
Config immutability Write, chmod, chown, delete all blocked PASS
Runtime functionality Gateway starts (HTTP 200), symlink writes work, auth profiles writable PASS
Sandbox lifecycle Re-creation preserves protections PASS

Full test plan in test/test-plan-514-config-lock.md.

Defense-in-depth hardening

Additional hardening tracked in #516:

  • Landlock enforcement (currently best_effort)
  • Tool policy deny lists for write/edit/exec
  • Re-evaluate allowInsecureAuth and dangerouslyDisableDeviceAuth defaults

Summary by CodeRabbit

Chores

  • Transitioned configuration initialization from runtime startup to build phase, enabling faster container startup cycles.
  • Enhanced security posture through immutable configuration files and refined filesystem permission controls.
  • Streamlined startup sequence by eliminating redundant runtime configuration modification steps.
  • Improved sandbox isolation with reorganized filesystem layout separating immutable configuration from writable state directories.

Use OpenShell's filesystem_policy to make /sandbox/.openclaw read-only
via Landlock LSM.  The agent cannot modify, delete, or replace
openclaw.json — including auth tokens, CORS origins, and gateway
settings.

The approach:
- Add /sandbox/.openclaw to the read_only policy list (Landlock
  most-specific-path-wins overrides /sandbox read_write)
- Pre-bake the complete openclaw.json at Docker build time, including
  gateway config and a per-build auth token
- Relocate writable agent state (plugins, profiles, etc.) to
  /sandbox/.openclaw-data/ via symlinks from .openclaw subdirectories
- Remove fix_openclaw_config() — no runtime writes to the config file

Symlinks in the read-only directory cannot be deleted or replaced by
the agent.  Writes through symlinks resolve to the writable target
(/sandbox/.openclaw-data/), so agent/plugin state still works.

Closes #514
@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

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 now emits a complete, immutable /sandbox/.openclaw/openclaw.json (including a generated gateway.auth.token) and reorganizes writable state under /sandbox/.openclaw-data/* with /sandbox/.openclaw/* symlinks; startup no longer mutates gateway config and the config file is root-owned and read-only.

Changes

Cohort / File(s) Summary
Docker build & config generation
Dockerfile
Create /sandbox/.openclaw-data/* for writable state and symlink /sandbox/.openclaw/* into it. Generate a COMPLETE openclaw.json at build time (adds gateway block and a build-time gateway.auth.token via secrets.token_hex(32)), chown/chmod the config (root-owned, file readonly, dir sticky), then continue as USER sandbox.
Sandbox policy
nemoclaw-blueprint/policies/openclaw-sandbox.yaml
Add /sandbox/.openclaw to the read-only allowlist and /sandbox/.openclaw-data to the read-write allowlist to reflect the immutable config path backed by writable data.
Startup script
scripts/nemoclaw-start.sh
Remove the fix_openclaw_config function and its invocation; startup no longer edits openclaw.json (gateway/controlUi/allowedOrigins/trustedProxies/auth token). Startup now runs openclaw doctor --fix, write_auth_profile, installs plugins, then launches openclaw gateway.

Sequence Diagram(s)

sequenceDiagram
  participant Builder as Builder (Dockerfile)
  participant FS as Filesystem (/sandbox)
  participant Gateway as Gateway (openclaw)
  participant Agent as Agent (sandbox user)

  Builder->>FS: create /sandbox/.openclaw-data/*\nwrite complete openclaw.json (secrets.token_hex)
  Builder->>FS: create symlinks /sandbox/.openclaw/* -> .openclaw-data/*
  Builder->>FS: chown root:root openclaw.json\nchmod 444 openclaw.json\nchmod 1777 /sandbox/.openclaw
  Gateway->>FS: read openclaw.json at startup
  Gateway->>Agent: run as sandbox user (runtime)
  Agent->>FS: attempt to modify /sandbox/.openclaw/openclaw.json
  FS-->>Agent: EPERM / denied (root-owned, readonly)
  Agent->>Gateway: attempt to restart/alter gateway (no config change)
  Gateway-->>Agent: config unchanged / continues using immutable config
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I stitched a token bright as dawn,
Hid the burrow where the bytes live on,
Root guards the door, symlinks mark the trail,
Agents may nudge, but the lock will not fail,
Safe hops and tidy tails prevail!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix: lock gateway config via Landlock filesystem policy' accurately summarizes the main objective: preventing gateway config modification through filesystem-level protections.
Linked Issues check ✅ Passed The PR fully implements the minimum mitigation from issue #514: making openclaw.json root-owned and read-only (chown root:root; chmod 444) and adding defense-in-depth with Landlock policy allowlists.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #514: Dockerfile restructures .openclaw paths and locks permissions, scripts removes runtime config modification, and YAML adds Landlock allowlist entries.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/policy-lockdown-514

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
nemoclaw-blueprint/policies/openclaw-sandbox.yaml (1)

39-40: Consider hardening: best_effort allows silent degradation.

With best_effort, if Landlock isn't supported on the host kernel, the filesystem policy silently degrades — the agent could modify openclaw.json without detection. Issue #514 lists changing to enforce as a follow-up; recommend tracking this explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw-blueprint/policies/openclaw-sandbox.yaml` around lines 39 - 40, The
policy sets landlock.compatibility to "best_effort", which allows silent
degradation if Landlock isn't available; change the value to "enforce" to ensure
the filesystem policy is mandatory (so modifications to openclaw.json by the
agent are prevented/detected), and add a short comment or TODO referencing issue
`#514` to track any follow-up if hosts lack Landlock support; locate the
"landlock" block and update the "compatibility" field from best_effort to
enforce and add the issue-tracking note.
Dockerfile (1)

96-96: Nitpick: use context manager for file write.

open(path, 'w') without a context manager leaves the file handle unclosed until GC. In a one-shot build script this is harmless, but using with is cleaner.

♻️ Suggested fix
-json.dump(config, open(path, 'w'), indent=2); \
-os.chmod(path, 0o600)"
+with open(path, 'w') as f: json.dump(config, f, indent=2); \
+os.chmod(path, 0o600)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 96, Replace the direct open(...) call inside json.dump
with a context manager: use a with open(path, 'w') as f: block and pass that
file handle to json.dump(config, f, indent=2) so the file is closed promptly;
locate the json.dump(...) expression (and the surrounding shell line
continuation in the Dockerfile) and convert it to use the with-block, preserving
any required line continuation/backslash semantics in the Dockerfile.
🤖 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 87-88: The config currently sets 'allowInsecureAuth': True and
'dangerouslyDisableDeviceAuth': True which weakens authentication; change these
to be false by default and only enable them when explicitly requested for
local/dev use (e.g. behind a guarded isLocalMode or an explicit env flag like
ENABLE_INSECURE_AUTH=true), add an inline comment documenting why the override
is allowed and a TODO linking issue `#514` to harden defaults later, and ensure
any path that enables these flags is audited/logged so the exception is
deliberate.

---

Nitpick comments:
In `@Dockerfile`:
- Line 96: Replace the direct open(...) call inside json.dump with a context
manager: use a with open(path, 'w') as f: block and pass that file handle to
json.dump(config, f, indent=2) so the file is closed promptly; locate the
json.dump(...) expression (and the surrounding shell line continuation in the
Dockerfile) and convert it to use the with-block, preserving any required line
continuation/backslash semantics in the Dockerfile.

In `@nemoclaw-blueprint/policies/openclaw-sandbox.yaml`:
- Around line 39-40: The policy sets landlock.compatibility to "best_effort",
which allows silent degradation if Landlock isn't available; change the value to
"enforce" to ensure the filesystem policy is mandatory (so modifications to
openclaw.json by the agent are prevented/detected), and add a short comment or
TODO referencing issue `#514` to track any follow-up if hosts lack Landlock
support; locate the "landlock" block and update the "compatibility" field from
best_effort to enforce and add the issue-tracking note.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5a7cdb22-2701-49e7-9f11-2a9dc6c0867a

📥 Commits

Reviewing files that changed from the base of the PR and between 20d0c95 and 2250988.

📒 Files selected for processing (3)
  • Dockerfile
  • nemoclaw-blueprint/policies/openclaw-sandbox.yaml
  • scripts/nemoclaw-start.sh
💤 Files with no reviewable changes (1)
  • scripts/nemoclaw-start.sh

Comment thread Dockerfile
Comment on lines +87 to +88
'allowInsecureAuth': True, \
'dangerouslyDisableDeviceAuth': True, \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Security concern: insecure auth settings require justification.

allowInsecureAuth: True and dangerouslyDisableDeviceAuth: True explicitly weaken the gateway's authentication. While these may be necessary for local mode, they could undermine the security gains from making the config immutable — an attacker who can reach the gateway endpoint may not need to modify the config if auth is already disabled.

Confirm these are required for the intended use case, and track hardening these defaults per issue #514's follow-up items.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 87 - 88, The config currently sets
'allowInsecureAuth': True and 'dangerouslyDisableDeviceAuth': True which weakens
authentication; change these to be false by default and only enable them when
explicitly requested for local/dev use (e.g. behind a guarded isLocalMode or an
explicit env flag like ENABLE_INSECURE_AUTH=true), add an inline comment
documenting why the override is allowed and a TODO linking issue `#514` to harden
defaults later, and ensure any path that enables these flags is audited/logged
so the exception is deliberate.

Landlock enforcement is not yet active in the current OpenShell
runtime, so the filesystem_policy read_only list is defense-in-depth
only.  Add a concrete DAC lock: after all build-time writes are done,
switch to USER root and chown openclaw.json to root:root 444.

The sandbox process runs as the sandbox user and cannot modify a
root-owned read-only file — no Landlock, no sudo, no entrypoint
tricks needed.  This works because fix_openclaw_config() was removed
in the previous commit and all config is pre-baked at build time.
chown root:root on /sandbox/.openclaw/ (755) so the sandbox user
cannot unlink openclaw.json and replace it with a malicious copy.
The directory is still world-readable/traversable so symlinks resolve.
root:root ownership alone broke openclaw runtime operations. Sticky
bit (chmod 1777) gives /tmp-like semantics: sandbox user can create
and delete their own files, but cannot delete root-owned openclaw.json.

Combined with root:root 444 on the config file itself, the agent
cannot modify, delete, or replace the gateway security settings.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
Dockerfile (2)

96-96: Minor: Use context manager for file operations.

open(path, 'w') without a with statement may leave the file handle open if an exception occurs during JSON serialization. While unlikely to cause issues in a Dockerfile build context, using proper resource management is a good practice.

🧹 Proposed fix
-json.dump(config, open(path, 'w'), indent=2); \
+with open(path, 'w') as f: json.dump(config, f, indent=2); \
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 96, Replace the direct open(path, 'w') call used with
json.dump(config, open(path, 'w'), indent=2) by using a Python context manager
to ensure the file handle is closed on exceptions; locate the statement that
calls json.dump with open(path, 'w') (references: json.dump, open, path, config)
and refactor it to use a with open(path, 'w') as f: block and pass f into
json.dump.

36-42: Consider making symlinks root-owned for defense-in-depth.

The symlinks in /sandbox/.openclaw/ are created here but then chown -R sandbox:sandbox at line 42 makes them sandbox-owned. Later, line 109 only changes ownership of the directory and openclaw.json to root — the symlinks remain sandbox-owned.

With the sticky bit (1777), the sandbox user can still delete/rename files they own (the symlinks). If Landlock operates in best_effort mode and fails to enforce read-only, the agent could remove these symlinks and recreate them pointing elsewhere.

If you want the symlinks protected by DAC as well, change line 109 to recursively chown the directory or explicitly include the symlinks:

🔒 Proposed fix to protect symlinks
 USER root
-RUN chown root:root /sandbox/.openclaw /sandbox/.openclaw/openclaw.json \
+RUN chown -h root:root /sandbox/.openclaw /sandbox/.openclaw/* \
     && chmod 1777 /sandbox/.openclaw \
     && chmod 444 /sandbox/.openclaw/openclaw.json

Note: -h ensures symlinks themselves are chowned rather than their targets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 36 - 42, The symlinks created by the mkdir/ln -s
block are later made sandbox-owned by the chown -R sandbox:sandbox command; to
protect the symlinks themselves you should ensure they are owned by root (not
the sandbox user) after creation — update the final ownership step (the chown
invocation that changes ownership of the directory and openclaw.json) to also
change ownership of the symlinks (use a recursive/h option or explicitly chown
the symlink names) so the symlink entries created by the ln -s lines are
root-owned rather than sandbox-owned.
🤖 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`:
- Line 92: The Dockerfile currently embeds a build-time auth token via the
`'auth': {'token': secrets.token_hex(32)}` expression which bakes a secret into
the image; confirm this behavior matches your threat model and either (A) avoid
baking the token by moving token generation to container runtime (generate
inside an entrypoint or app startup) or (B) inject it at deploy time using a
safer mechanism (Docker secrets, environment variables supplied by orchestrator,
or build ARGs with proper registry controls), and update documentation to state
why this is acceptable for `'mode': 'local'` and the restrictive flags
(`allowInsecureAuth`, `dangerouslyDisableDeviceAuth`) if you keep it.

---

Nitpick comments:
In `@Dockerfile`:
- Line 96: Replace the direct open(path, 'w') call used with json.dump(config,
open(path, 'w'), indent=2) by using a Python context manager to ensure the file
handle is closed on exceptions; locate the statement that calls json.dump with
open(path, 'w') (references: json.dump, open, path, config) and refactor it to
use a with open(path, 'w') as f: block and pass f into json.dump.
- Around line 36-42: The symlinks created by the mkdir/ln -s block are later
made sandbox-owned by the chown -R sandbox:sandbox command; to protect the
symlinks themselves you should ensure they are owned by root (not the sandbox
user) after creation — update the final ownership step (the chown invocation
that changes ownership of the directory and openclaw.json) to also change
ownership of the symlinks (use a recursive/h option or explicitly chown the
symlink names) so the symlink entries created by the ln -s lines are root-owned
rather than sandbox-owned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9c420d29-572a-4bd5-990c-d88a0f193af9

📥 Commits

Reviewing files that changed from the base of the PR and between 4a3fd95 and 03dcc9b.

📒 Files selected for processing (1)
  • Dockerfile

Comment thread Dockerfile
chown -h root:root on all entries in /sandbox/.openclaw/ so the
sandbox user cannot delete symlinks and recreate them pointing to
malicious targets.  Writes through symlinks still resolve to the
writable .openclaw-data/ directory.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 109-111: The chown invocation uses a glob that skips dotfiles;
update the Dockerfile RUN that currently references chown -h root:root
/sandbox/.openclaw /sandbox/.openclaw/* so hidden files are included—replace the
glob with a dotfile-safe traversal (for example use find -mindepth 1 -maxdepth 1
-exec chown -h root:root {} + or an explicit /sandbox/.openclaw/.* pattern
handled carefully) so all entries in /sandbox/.openclaw (including dotfiles) are
chowned to root before you apply chmod 1777 and chmod 444 to
/sandbox/.openclaw/openclaw.json.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1dd9622e-687b-488d-9567-3ca54f80d309

📥 Commits

Reviewing files that changed from the base of the PR and between 03dcc9b and 3dc7cd4.

📒 Files selected for processing (1)
  • Dockerfile

Comment thread Dockerfile Outdated
Restores customization that was lost when fix_openclaw_config() was
removed:

- NEMOCLAW_MODEL: sets the default model (was NEMOCLAW_MODEL env var)
- CHAT_UI_URL: adds remote origin to CORS allowedOrigins (was runtime)
- NEMOCLAW_BUILD_ID: busts Docker layer cache so each build gets a
  unique auth token (pass --build-arg NEMOCLAW_BUILD_ID=$(date +%s))

nemoclaw onboard passes these at image build time.  Defaults preserve
current behavior (nemotron-3-super, localhost origins).
Shell glob * doesn't match dotfiles.  Use find -mindepth 1 -maxdepth 1
to ensure all entries (including any hidden files created at build time)
are chowned to root.
@ericksoa ericksoa merged commit 2d3f84e into main Mar 21, 2026
3 checks passed
gagandaroach added a commit that referenced this pull request Mar 21, 2026
openclaw.json is locked to root:444 at runtime (PR #555 security
hardening). Move model selection before sandbox creation and patch
NEMOCLAW_MODEL build arg in the Dockerfile so the correct model is
baked into the image. Remove the Python block that wrote openclaw.json
at runtime — it's no longer needed or possible.

Step reorder: preflight → gateway → select model → create sandbox →
setup inference → sync nemoclaw config → policies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gagandaroach added a commit that referenced this pull request Mar 21, 2026
openclaw.json is locked to root:444 at runtime (PR #555 security
hardening). Move model selection before sandbox creation and patch
NEMOCLAW_MODEL build arg in the Dockerfile so the correct model is
baked into the image. Remove the Python block that wrote openclaw.json
at runtime — it's no longer needed or possible.

Step reorder: preflight → gateway → select model → create sandbox →
setup inference → sync nemoclaw config → policies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gagandaroach added a commit that referenced this pull request Mar 21, 2026
openclaw.json is locked to root:444 at runtime (PR #555 security
hardening). Move model selection before sandbox creation and patch
NEMOCLAW_MODEL build arg in the Dockerfile so the correct model is
baked into the image. Remove the Python block that wrote openclaw.json
at runtime — it's no longer needed or possible.

Step reorder: preflight → gateway → select model → create sandbox →
setup inference → sync nemoclaw config → policies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ryuketsukami pushed a commit to Ryuketsukami/NemoClaw that referenced this pull request Mar 24, 2026
* fix: lock gateway config via Landlock filesystem policy (NVIDIA#514)

Use OpenShell's filesystem_policy to make /sandbox/.openclaw read-only
via Landlock LSM.  The agent cannot modify, delete, or replace
openclaw.json — including auth tokens, CORS origins, and gateway
settings.

The approach:
- Add /sandbox/.openclaw to the read_only policy list (Landlock
  most-specific-path-wins overrides /sandbox read_write)
- Pre-bake the complete openclaw.json at Docker build time, including
  gateway config and a per-build auth token
- Relocate writable agent state (plugins, profiles, etc.) to
  /sandbox/.openclaw-data/ via symlinks from .openclaw subdirectories
- Remove fix_openclaw_config() — no runtime writes to the config file

Symlinks in the read-only directory cannot be deleted or replaced by
the agent.  Writes through symlinks resolve to the writable target
(/sandbox/.openclaw-data/), so agent/plugin state still works.

Closes NVIDIA#514

* fix: lock openclaw.json via root ownership at build time (DAC)

Landlock enforcement is not yet active in the current OpenShell
runtime, so the filesystem_policy read_only list is defense-in-depth
only.  Add a concrete DAC lock: after all build-time writes are done,
switch to USER root and chown openclaw.json to root:root 444.

The sandbox process runs as the sandbox user and cannot modify a
root-owned read-only file — no Landlock, no sudo, no entrypoint
tricks needed.  This works because fix_openclaw_config() was removed
in the previous commit and all config is pre-baked at build time.

* fix: lock parent directory to root to block delete+recreate attack

chown root:root on /sandbox/.openclaw/ (755) so the sandbox user
cannot unlink openclaw.json and replace it with a malicious copy.
The directory is still world-readable/traversable so symlinks resolve.

* fix: use sticky bit (1777) on .openclaw dir to block delete+recreate

root:root ownership alone broke openclaw runtime operations. Sticky
bit (chmod 1777) gives /tmp-like semantics: sandbox user can create
and delete their own files, but cannot delete root-owned openclaw.json.

Combined with root:root 444 on the config file itself, the agent
cannot modify, delete, or replace the gateway security settings.

* fix: make symlinks root-owned to prevent redirection attack

chown -h root:root on all entries in /sandbox/.openclaw/ so the
sandbox user cannot delete symlinks and recreate them pointing to
malicious targets.  Writes through symlinks still resolve to the
writable .openclaw-data/ directory.

* fix: add build args for model, CORS origins, and unique auth token

Restores customization that was lost when fix_openclaw_config() was
removed:

- NEMOCLAW_MODEL: sets the default model (was NEMOCLAW_MODEL env var)
- CHAT_UI_URL: adds remote origin to CORS allowedOrigins (was runtime)
- NEMOCLAW_BUILD_ID: busts Docker layer cache so each build gets a
  unique auth token (pass --build-arg NEMOCLAW_BUILD_ID=$(date +%s))

nemoclaw onboard passes these at image build time.  Defaults preserve
current behavior (nemotron-3-super, localhost origins).

* fix: use find instead of glob to chown dotfiles in .openclaw

Shell glob * doesn't match dotfiles.  Use find -mindepth 1 -maxdepth 1
to ensure all entries (including any hidden files created at build time)
are chowned to root.
jessesanford pushed a commit to jessesanford/NemoClaw that referenced this pull request Mar 24, 2026
* fix: lock gateway config via Landlock filesystem policy (NVIDIA#514)

Use OpenShell's filesystem_policy to make /sandbox/.openclaw read-only
via Landlock LSM.  The agent cannot modify, delete, or replace
openclaw.json — including auth tokens, CORS origins, and gateway
settings.

The approach:
- Add /sandbox/.openclaw to the read_only policy list (Landlock
  most-specific-path-wins overrides /sandbox read_write)
- Pre-bake the complete openclaw.json at Docker build time, including
  gateway config and a per-build auth token
- Relocate writable agent state (plugins, profiles, etc.) to
  /sandbox/.openclaw-data/ via symlinks from .openclaw subdirectories
- Remove fix_openclaw_config() — no runtime writes to the config file

Symlinks in the read-only directory cannot be deleted or replaced by
the agent.  Writes through symlinks resolve to the writable target
(/sandbox/.openclaw-data/), so agent/plugin state still works.

Closes NVIDIA#514

* fix: lock openclaw.json via root ownership at build time (DAC)

Landlock enforcement is not yet active in the current OpenShell
runtime, so the filesystem_policy read_only list is defense-in-depth
only.  Add a concrete DAC lock: after all build-time writes are done,
switch to USER root and chown openclaw.json to root:root 444.

The sandbox process runs as the sandbox user and cannot modify a
root-owned read-only file — no Landlock, no sudo, no entrypoint
tricks needed.  This works because fix_openclaw_config() was removed
in the previous commit and all config is pre-baked at build time.

* fix: lock parent directory to root to block delete+recreate attack

chown root:root on /sandbox/.openclaw/ (755) so the sandbox user
cannot unlink openclaw.json and replace it with a malicious copy.
The directory is still world-readable/traversable so symlinks resolve.

* fix: use sticky bit (1777) on .openclaw dir to block delete+recreate

root:root ownership alone broke openclaw runtime operations. Sticky
bit (chmod 1777) gives /tmp-like semantics: sandbox user can create
and delete their own files, but cannot delete root-owned openclaw.json.

Combined with root:root 444 on the config file itself, the agent
cannot modify, delete, or replace the gateway security settings.

* fix: make symlinks root-owned to prevent redirection attack

chown -h root:root on all entries in /sandbox/.openclaw/ so the
sandbox user cannot delete symlinks and recreate them pointing to
malicious targets.  Writes through symlinks still resolve to the
writable .openclaw-data/ directory.

* fix: add build args for model, CORS origins, and unique auth token

Restores customization that was lost when fix_openclaw_config() was
removed:

- NEMOCLAW_MODEL: sets the default model (was NEMOCLAW_MODEL env var)
- CHAT_UI_URL: adds remote origin to CORS allowedOrigins (was runtime)
- NEMOCLAW_BUILD_ID: busts Docker layer cache so each build gets a
  unique auth token (pass --build-arg NEMOCLAW_BUILD_ID=$(date +%s))

nemoclaw onboard passes these at image build time.  Defaults preserve
current behavior (nemotron-3-super, localhost origins).

* fix: use find instead of glob to chown dotfiles in .openclaw

Shell glob * doesn't match dotfiles.  Use find -mindepth 1 -maxdepth 1
to ensure all entries (including any hidden files created at build time)
are chowned to root.
@wscurran wscurran added the bug-fix PR fixes a bug or regression label Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug-fix PR fixes a bug or regression

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent can self-modify openclaw.json to bypass auth and CORS controls

3 participants