Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
36ee0c5
security(sandbox): restrict /sandbox to read-only via Landlock filesy…
prekshivyas Mar 30, 2026
ca44773
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Mar 30, 2026
8fce1bb
test(service-env): update proxy persistence tests for proxy-env.sh ap…
prekshivyas Mar 30, 2026
7c42034
fix(test): use replaceAll for proxy-env path substitution in tests
prekshivyas Mar 30, 2026
ace3484
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Mar 30, 2026
1d0e381
fix(security): harden .nemoclaw DAC and move proxy-env.sh to /tmp
prekshivyas Mar 30, 2026
99d7c1a
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Mar 31, 2026
9f44941
style: fix shfmt formatting in nemoclaw-start.sh
prekshivyas Mar 31, 2026
5cc63bc
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Mar 31, 2026
2231170
fix(sandbox): pre-create .nemoclaw/staging for non-persistent snapshots
prekshivyas Mar 31, 2026
a24b07f
merge: resolve conflict with upstream main in nemoclaw-start.sh
prekshivyas Mar 31, 2026
fba507c
test(security): add e2e tests for read-only /sandbox enforcement (#804)
prekshivyas Mar 31, 2026
3dc4eb4
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Mar 31, 2026
ef3e4eb
fix(test): remove /sandbox write test — Landlock-only protection (#804)
prekshivyas Mar 31, 2026
ba4a947
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Mar 31, 2026
c1097f8
fix(test): skip .bashrc/.profile tests when base image lacks them (#804)
prekshivyas Mar 31, 2026
65daaf8
merge: resolve conflict with upstream formatting in service-env.test.js
prekshivyas Mar 31, 2026
169b293
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Mar 31, 2026
db14b8b
Merge branch 'main' into security/read-only-sandbox-filesystem
cv Mar 31, 2026
20d5a37
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Mar 31, 2026
64332b2
style: fix prettier formatting in service-env.test.js
prekshivyas Mar 31, 2026
232643c
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Mar 31, 2026
7a0129b
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Mar 31, 2026
402006d
Merge branch 'main' into security/read-only-sandbox-filesystem
cv Apr 1, 2026
b5854ec
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Apr 2, 2026
2f40fad
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Apr 2, 2026
f31a60a
fix(entrypoint): complete XDG and tool dotfile redirects to /tmp (#804)
prekshivyas Apr 2, 2026
6c61285
fix(sandbox): add missing openclaw symlinks and pre-create XDG dirs (…
prekshivyas Apr 2, 2026
8989f81
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Apr 2, 2026
058bf91
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Apr 2, 2026
e08c571
fix(docker): ensure openclaw symlinks exist without base image rebuil…
prekshivyas Apr 2, 2026
34d417c
fix(docker): replace real dirs with symlinks after openclaw doctor (#…
prekshivyas Apr 2, 2026
cf5de1b
Merge branch 'main' into security/read-only-sandbox-filesystem
cv Apr 2, 2026
e08b32e
Merge branch 'main' into security/read-only-sandbox-filesystem
cv Apr 2, 2026
00b1190
fix(security): address review concerns for read-only sandbox (#804)
prekshivyas Apr 7, 2026
f8fd565
merge: resolve conflicts with upstream main and address review concerns
prekshivyas Apr 7, 2026
0d85225
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Apr 7, 2026
09b1661
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Apr 8, 2026
24a8b5a
fix(sandbox): define _SANDBOX_HOME before use in entrypoint (#1609)
Apr 8, 2026
2845415
fix(sandbox): guard tmp dir creation for non-root mode
Apr 8, 2026
5e5db86
Merge branch 'main' into security/read-only-sandbox-filesystem
Apr 8, 2026
29f44ca
fix(sandbox): add sticky bit to .nemoclaw to survive OpenShell chown …
Apr 8, 2026
c9db74e
Merge remote-tracking branch 'origin/main' into security/read-only-sa…
Apr 8, 2026
c5b66a6
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Apr 8, 2026
5f491ea
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Apr 8, 2026
3f55ee8
Merge branch 'main' into security/read-only-sandbox-filesystem
prekshivyas Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,49 @@ services:
> capability dropping in your `docker run` flags, Compose file, or Kubernetes
> `securityContext`.

## Read-Only Home Directory

The sandbox Landlock policy restricts `/sandbox` (the agent's home directory) to read-only access.
Only explicitly declared directories are writable:

| Path | Access | Purpose |
|------|--------|---------|
| `/sandbox` | read-only | Home directory — agents cannot create arbitrary files |
| `/sandbox/.openclaw` | read-only | Immutable gateway config (auth tokens, CORS) |
| `/sandbox/.openclaw-data` | read-write | Agent state, workspace, plugins (via symlinks) |
| `/sandbox/.nemoclaw` | read-write | Plugin state and config; blueprints within are DAC-protected (root-owned) |
| `/tmp` | read-write | Temporary files and logs |

This prevents agents from:

- Writing scripts and executing them later
- Modifying their own runtime environment
- Creating hidden files that persist across invocations
- Using writable space for data staging before exfiltration

The image build pre-creates shell init files `.bashrc` and `.profile`.
These files source runtime proxy configuration from `/tmp/nemoclaw-proxy-env.sh`.

### Landlock Kernel Requirements

Landlock LSM requires Linux kernel 5.13 or later with `CONFIG_SECURITY_LANDLOCK=y`.
The NemoClaw sandbox policy uses `compatibility: best_effort`, which means Landlock enforcement is silently skipped on kernels that do not support it.

On such kernels, protection falls back to DAC (file ownership and permissions) only.
Files owned by the sandbox user (e.g., `.bashrc`, `.profile`) would be writable by the agent despite the Landlock read-only policy.

Operators should verify Landlock availability:

```console
$ ls /sys/kernel/security/landlock
```

For production deployments, kernel 5.13+ with Landlock enabled is strongly recommended.
The `test/e2e/e2e-cloud-experimental/checks/04-landlock-readonly.sh` script validates enforcement at runtime.

## References

- [#804](https://github.com/NVIDIA/NemoClaw/issues/804): Read-only home directory
- [#807](https://github.com/NVIDIA/NemoClaw/issues/807): gcc in sandbox image
- [#808](https://github.com/NVIDIA/NemoClaw/issues/808): netcat in sandbox image
- [#809](https://github.com/NVIDIA/NemoClaw/issues/809): No process limit
Expand Down
46 changes: 45 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ COPY nemoclaw-blueprint/ /opt/nemoclaw-blueprint/
WORKDIR /opt/nemoclaw
RUN npm ci --omit=dev

# Set up blueprint for local resolution
# Set up blueprint for local resolution.
# Blueprints are immutable at runtime; DAC protection (root ownership) is applied
# later since /sandbox/.nemoclaw is Landlock read_write for plugin state (#804).
RUN mkdir -p /sandbox/.nemoclaw/blueprints/0.1.0 \
&& cp -r /opt/nemoclaw-blueprint/* /sandbox/.nemoclaw/blueprints/0.1.0/

Expand Down Expand Up @@ -176,6 +178,28 @@ RUN openclaw doctor --fix > /dev/null 2>&1 || true \
# The writable state lives in .openclaw-data, reached via the symlinks.
# hadolint ignore=DL3002
USER root

# Ensure .openclaw-data subdirs and symlinks exist for logs, credentials, and
# sandbox. These are defined in Dockerfile.base but the GHCR base image may
# not have been rebuilt yet. Idempotent — harmless once the base catches up.
# Ref: https://github.com/NVIDIA/NemoClaw/issues/804
RUN mkdir -p /sandbox/.openclaw-data/logs \
/sandbox/.openclaw-data/credentials \
/sandbox/.openclaw-data/sandbox \
&& chown sandbox:sandbox /sandbox/.openclaw-data/logs \
/sandbox/.openclaw-data/credentials \
/sandbox/.openclaw-data/sandbox \
&& for dir in logs credentials sandbox; do \
if [ -L "/sandbox/.openclaw/$dir" ]; then true; \
elif [ -e "/sandbox/.openclaw/$dir" ]; then \
cp -a "/sandbox/.openclaw/$dir/." "/sandbox/.openclaw-data/$dir/" 2>/dev/null || true; \
rm -rf "/sandbox/.openclaw/$dir"; \
ln -s "/sandbox/.openclaw-data/$dir" "/sandbox/.openclaw/$dir"; \
else \
ln -s "/sandbox/.openclaw-data/$dir" "/sandbox/.openclaw/$dir"; \
fi; \
done

RUN chown root:root /sandbox/.openclaw \
&& rm -rf /root/.npm /sandbox/.npm \
&& find /sandbox/.openclaw -mindepth 1 -maxdepth 1 -exec chown -h root:root {} + \
Expand All @@ -189,6 +213,26 @@ RUN sha256sum /sandbox/.openclaw/openclaw.json > /sandbox/.openclaw/.config-hash
&& chmod 444 /sandbox/.openclaw/.config-hash \
&& chown root:root /sandbox/.openclaw/.config-hash

# DAC-protect .nemoclaw directory: /sandbox/.nemoclaw is Landlock read_write
# (for plugin state/config), but the parent and blueprints are immutable at
# runtime. Root ownership on the parent prevents the agent from renaming or
# replacing the root-owned blueprints directory. Only state/, migration/,
# snapshots/, and config.json are sandbox-owned for runtime writes.
# Sticky bit (1755): OpenShell's prepare_filesystem() chowns read_write paths
# to run_as_user at sandbox start, flipping this dir to sandbox:sandbox.
# The sticky bit survives the chown and prevents the sandbox user from
# renaming or deleting root-owned entries (blueprints/).
# Ref: https://github.com/NVIDIA/NemoClaw/issues/804
# Ref: https://github.com/NVIDIA/NemoClaw/issues/1607
RUN chown root:root /sandbox/.nemoclaw \
&& chmod 1755 /sandbox/.nemoclaw \
&& chown -R root:root /sandbox/.nemoclaw/blueprints \
&& chmod -R 755 /sandbox/.nemoclaw/blueprints \
&& mkdir -p /sandbox/.nemoclaw/state /sandbox/.nemoclaw/migration /sandbox/.nemoclaw/snapshots /sandbox/.nemoclaw/staging \
&& chown sandbox:sandbox /sandbox/.nemoclaw/state /sandbox/.nemoclaw/migration /sandbox/.nemoclaw/snapshots /sandbox/.nemoclaw/staging \
&& touch /sandbox/.nemoclaw/config.json \
&& chown sandbox:sandbox /sandbox/.nemoclaw/config.json

# Entrypoint runs as root to start the gateway as the gateway user,
# then drops to sandbox for agent commands. See nemoclaw-start.sh.
ENTRYPOINT ["/usr/local/bin/nemoclaw-start"]
Expand Down
24 changes: 22 additions & 2 deletions Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,10 @@ RUN mkdir -p /sandbox/.openclaw-data/agents/main/agent \
/sandbox/.openclaw-data/canvas \
/sandbox/.openclaw-data/cron \
/sandbox/.openclaw-data/memory \
/sandbox/.openclaw-data/telegram \
/sandbox/.openclaw-data/logs \
/sandbox/.openclaw-data/credentials \
/sandbox/.openclaw-data/sandbox \
/sandbox/.openclaw-data/telegram \
&& mkdir -p /sandbox/.openclaw \
&& ln -s /sandbox/.openclaw-data/agents /sandbox/.openclaw/agents \
&& ln -s /sandbox/.openclaw-data/extensions /sandbox/.openclaw/extensions \
Expand All @@ -115,14 +117,32 @@ RUN mkdir -p /sandbox/.openclaw-data/agents/main/agent \
&& ln -s /sandbox/.openclaw-data/canvas /sandbox/.openclaw/canvas \
&& ln -s /sandbox/.openclaw-data/cron /sandbox/.openclaw/cron \
&& ln -s /sandbox/.openclaw-data/memory /sandbox/.openclaw/memory \
&& ln -s /sandbox/.openclaw-data/logs /sandbox/.openclaw/logs \
&& ln -s /sandbox/.openclaw-data/credentials /sandbox/.openclaw/credentials \
&& ln -s /sandbox/.openclaw-data/sandbox /sandbox/.openclaw/sandbox \
&& touch /sandbox/.openclaw-data/update-check.json \
&& ln -s /sandbox/.openclaw-data/update-check.json /sandbox/.openclaw/update-check.json \
&& touch /sandbox/.openclaw-data/exec-approvals.json \
&& ln -s /sandbox/.openclaw-data/exec-approvals.json /sandbox/.openclaw/exec-approvals.json \
&& ln -s /sandbox/.openclaw-data/telegram /sandbox/.openclaw/telegram \
&& ln -s /sandbox/.openclaw-data/credentials /sandbox/.openclaw/credentials \
&& chown -R sandbox:sandbox /sandbox/.openclaw /sandbox/.openclaw-data

# Pre-create shell init files for the sandbox user.
# The /sandbox home directory is Landlock read-only at runtime (#804), so these
# files must exist at build time. Runtime proxy config is written by the
# entrypoint to /tmp/nemoclaw-proxy-env.sh (root-owned, sticky-bit protected)
# and sourced from here on every interactive session.
# hadolint ignore=SC2028
RUN printf '%s\n' \
'# Source runtime proxy config (Landlock read-only home, #804)' \
'[ -f /tmp/nemoclaw-proxy-env.sh ] && . /tmp/nemoclaw-proxy-env.sh' \
> /sandbox/.bashrc \
&& printf '%s\n' \
'# Source runtime proxy config (Landlock read-only home, #804)' \
'[ -f /tmp/nemoclaw-proxy-env.sh ] && . /tmp/nemoclaw-proxy-env.sh' \
> /sandbox/.profile \
&& chown sandbox:sandbox /sandbox/.bashrc /sandbox/.profile

# Install OpenClaw CLI + PyYAML for inline Python scripts in e2e tests.
# When bumping the openclaw version, rebuild this base image.
RUN npm install -g openclaw@2026.3.11 \
Expand Down
41 changes: 41 additions & 0 deletions docs/deployment/sandbox-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,49 @@ services:
> capability dropping in your `docker run` flags, Compose file, or Kubernetes
> `securityContext`.

## Read-Only Home Directory

The sandbox Landlock policy restricts `/sandbox` (the agent's home directory) to read-only access.
Only explicitly declared directories are writable:

| Path | Access | Purpose |
|------|--------|---------|
| `/sandbox` | read-only | Home directory — agents cannot create arbitrary files |
| `/sandbox/.openclaw` | read-only | Immutable gateway config (auth tokens, CORS) |
| `/sandbox/.openclaw-data` | read-write | Agent state, workspace, plugins (via symlinks) |
| `/sandbox/.nemoclaw` | read-write | Plugin state and config; blueprints within are DAC-protected (root-owned) |
| `/tmp` | read-write | Temporary files and logs |

This prevents agents from:

- Writing scripts and executing them later
- Modifying their own runtime environment
- Creating hidden files that persist across invocations
- Using writable space for data staging before exfiltration

The image build pre-creates shell init files `.bashrc` and `.profile`.
These files source runtime proxy configuration from `/tmp/nemoclaw-proxy-env.sh`.

### Landlock Kernel Requirements

Landlock LSM requires Linux kernel 5.13 or later with `CONFIG_SECURITY_LANDLOCK=y`.
The NemoClaw sandbox policy uses `compatibility: best_effort`, which means Landlock enforcement is silently skipped on kernels that do not support it.

On such kernels, protection falls back to DAC (file ownership and permissions) only.
Files owned by the sandbox user (e.g., `.bashrc`, `.profile`) would be writable by the agent despite the Landlock read-only policy.

Operators should verify Landlock availability:

```console
$ ls /sys/kernel/security/landlock
```

For production deployments, kernel 5.13+ with Landlock enabled is strongly recommended.
The `test/e2e/e2e-cloud-experimental/checks/04-landlock-readonly.sh` script validates enforcement at runtime.

## References

- [#804](https://github.com/NVIDIA/NemoClaw/issues/804): Read-only home directory
- [#807](https://github.com/NVIDIA/NemoClaw/issues/807): gcc in sandbox image
- [#808](https://github.com/NVIDIA/NemoClaw/issues/808): netcat in sandbox image
- [#809](https://github.com/NVIDIA/NemoClaw/issues/809): No process limit
Expand Down
16 changes: 14 additions & 2 deletions nemoclaw-blueprint/policies/openclaw-sandbox.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
version: 1

filesystem_policy:
include_workdir: true
# SECURITY: must be false. When true, OpenShell adds WORKDIR (/sandbox) to
# read_write automatically, which overrides our read_only entry below because
# Landlock grants the union of all matching rules. All needed writable paths
# are declared explicitly in read_write.
# Ref: https://github.com/NVIDIA/NemoClaw/issues/804
include_workdir: false
read_only:
- /usr
- /lib
Expand All @@ -25,16 +30,23 @@ filesystem_policy:
- /app
- /etc
- /var/log
- /sandbox # Home directory — read-only to prevent agents
# from creating arbitrary files or modifying
# their own runtime environment. Writable state
# is restricted to /sandbox/.openclaw-data.
# Ref: https://github.com/NVIDIA/NemoClaw/issues/804
- /sandbox/.openclaw # Immutable gateway config — prevents agent
# from tampering with auth tokens or CORS.
# Writable state (agents, plugins) lives in
# /sandbox/.openclaw-data via symlinks.
# Ref: https://github.com/NVIDIA/NemoClaw/issues/514
read_write:
- /sandbox
- /tmp
- /dev/null
- /sandbox/.openclaw-data # Writable agent/plugin state (symlinked from .openclaw)
- /sandbox/.nemoclaw # Plugin state and config (state.ts, config.ts).
# Blueprints are root-owned; sticky bit (1755)
# on parent prevents rename/delete (#1607).

landlock:
compatibility: best_effort
Expand Down
Loading
Loading