fix: lock gateway config via Landlock filesystem policy#555
Conversation
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
|
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 now emits a complete, immutable Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
nemoclaw-blueprint/policies/openclaw-sandbox.yaml (1)
39-40: Consider hardening:best_effortallows silent degradation.With
best_effort, if Landlock isn't supported on the host kernel, the filesystem policy silently degrades — the agent could modifyopenclaw.jsonwithout detection. Issue#514lists changing toenforceas 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 usingwithis 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
📒 Files selected for processing (3)
Dockerfilenemoclaw-blueprint/policies/openclaw-sandbox.yamlscripts/nemoclaw-start.sh
💤 Files with no reviewable changes (1)
- scripts/nemoclaw-start.sh
| 'allowInsecureAuth': True, \ | ||
| 'dangerouslyDisableDeviceAuth': True, \ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
Dockerfile (2)
96-96: Minor: Use context manager for file operations.
open(path, 'w')without awithstatement 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 thenchown -R sandbox:sandboxat line 42 makes them sandbox-owned. Later, line 109 only changes ownership of the directory andopenclaw.jsonto 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_effortmode 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.jsonNote:
-hensures 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.
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.
There was a problem hiding this comment.
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.
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.
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>
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>
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>
* 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.
* 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.
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.jsonis written at Docker build time with a per-build auth token (secrets.token_hex(32)). The runtimefix_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 assandboxuser) 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/.openclawis added to the Landlockread_onlylist for additional enforcement when available.Changes
Dockerfile.openclawwith symlinks to.openclaw-data; pre-bake complete config with gateway section + auth token;root:root 1777on directory,root:root 444on configscripts/nemoclaw-start.shfix_openclaw_config()— no runtime config writesnemoclaw-blueprint/policies/openclaw-sandbox.yaml/sandbox/.openclawtoread_only,/sandbox/.openclaw-datatoread_writeTest results
11/11 tests pass across both Docker and OpenShell sandbox environments:
Full test plan in
test/test-plan-514-config-lock.md.Defense-in-depth hardening
Additional hardening tracked in #516:
best_effort)allowInsecureAuthanddangerouslyDisableDeviceAuthdefaultsSummary by CodeRabbit
Chores