Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -207,23 +207,24 @@ The container mounts system directories read-only to prevent the agent from modi
| Risk if relaxed | Making `/usr` or `/lib` writable lets the agent replace system binaries (such as `curl` or `node`) with trojanized versions. Making `/etc` writable lets the agent modify DNS resolution, TLS trust stores, or user accounts. |
| Recommendation | Never make system paths writable. If the agent needs a writable location for generated files, use a subdirectory of `/sandbox`. |

### Read-Only `.openclaw` Config
### Hardened `.openclaw` Wrapper

The `/sandbox/.openclaw` directory contains the OpenClaw gateway configuration, including auth tokens and CORS settings.
The container mounts it read-only while writable agent state (plugins, agent data) lives in `/sandbox/.openclaw-data` through symlinks.
The container mounts the wrapper path read-only while writable agent state and the live config file live in `/sandbox/.openclaw-data`.
`/sandbox/.openclaw/openclaw.json` is a fixed symlink to `/sandbox/.openclaw-data/config/openclaw.json`, so legit config updates still work without reopening the wrapper directory itself.

Multiple defense layers protect this directory:

- **DAC permissions.** Root owns the directory and `openclaw.json` with `chmod 444`, so the sandbox user cannot write to them.
- **Immutable flag.** The entrypoint applies `chattr +i` to the directory and all symlinks, preventing modification even if other controls fail.
- **Symlink validation.** At startup, the entrypoint verifies every symlink in `.openclaw` points to the expected `.openclaw-data` target. If any symlink points elsewhere, the container refuses to start.
- **Config integrity hash.** The build process pins a SHA256 hash of `openclaw.json`. The entrypoint verifies it at startup and refuses to start if the hash does not match.
- **DAC permissions.** Root owns the wrapper directory and the symlink entries under it, so the sandbox user cannot replace them.
- **Immutable flag.** The entrypoint applies `chattr +i` to the directory and all wrapper symlinks, preventing modification even if other controls fail.
- **Symlink validation.** At startup, the entrypoint verifies every symlink in `.openclaw` points to the expected target before the gateway launches.
- **Shared live-config path.** The live config file lives in `/sandbox/.openclaw-data/config/openclaw.json`, which is writable by both the sandbox user and the gateway process so `openclaw config`, Control UI edits, and direct file updates persist cleanly.

| Aspect | Detail |
|---|---|
| Default | The container mounts `/sandbox/.openclaw` as read-only, root-owned, immutable, and integrity-verified at startup. `/sandbox/.openclaw-data` remains writable. |
| Default | The container mounts `/sandbox/.openclaw` as a read-only, root-owned wrapper with immutable symlinks. The live config file lives in `/sandbox/.openclaw-data/config/openclaw.json`, exposed at `/sandbox/.openclaw/openclaw.json`, and `/sandbox/.openclaw-data` remains writable. |
| What you can change | Move `/sandbox/.openclaw` from `read_only` to `read_write` in the policy file. |
| Risk if relaxed | A writable `.openclaw` directory lets the agent modify its own gateway config: disabling CORS, changing auth tokens, or redirecting inference to an attacker-controlled endpoint. This is the single most dangerous filesystem change. |
| Risk if relaxed | A writable `.openclaw` directory lets the agent replace wrapper symlinks and redirect config-backed paths to attacker-controlled locations, which can enable arbitrary code execution, persistence, or privilege escalation. |
| Recommendation | Never make `/sandbox/.openclaw` writable. |

### Writable Paths
Expand Down Expand Up @@ -378,7 +379,7 @@ Device authentication requires each connecting device to go through a pairing fl
| Aspect | Detail |
|---|---|
| Default | Enabled. The gateway requires device pairing for all connections. |
| What you can change | Set `NEMOCLAW_DISABLE_DEVICE_AUTH=1` as a Docker build argument to disable device authentication. This is a build-time setting baked into `openclaw.json` and verified by hash at startup. |
| What you can change | Set `NEMOCLAW_DISABLE_DEVICE_AUTH=1` as a Docker build argument to change the initial value, or edit `gateway.controlUi.dangerouslyDisableDeviceAuth` in the sandbox's OpenClaw config afterward. |
| Risk if relaxed | Disabling device auth allows any device on the network to connect to the gateway without proving identity. This is dangerous when combined with LAN-bind changes or cloudflared tunnels in remote deployments, resulting in an unauthenticated, publicly reachable dashboard. |
| Recommendation | Keep device auth enabled (the default). Only disable it for headless or development environments where no untrusted devices can reach the gateway. |

Expand Down
15 changes: 9 additions & 6 deletions .agents/skills/nemoclaw-user-reference/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ $ NEMOCLAW_SINGLE_SESSION=1 curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash
The wizard prompts for a provider first, then collects the provider credential if needed.
Supported non-experimental choices include NVIDIA Endpoints, OpenAI, Anthropic, Google Gemini, and compatible OpenAI or Anthropic endpoints.
Credentials are stored in `~/.nemoclaw/credentials.json`. For file permissions, plaintext storage behavior, and hardening guidance, see Credential Storage (see the `nemoclaw-user-configure-security` skill).
The sandbox's live OpenClaw config is stored at `/sandbox/.openclaw-data/config/openclaw.json` and exposed at `/sandbox/.openclaw/openclaw.json`, so later `openclaw config set` and dashboard config edits persist after onboarding.
The legacy `nemoclaw setup` command is deprecated; use `nemoclaw onboard` instead.

After provider selection, the wizard prompts for a **policy tier** that controls the default set of network policy presets applied to the sandbox.
Expand All @@ -80,10 +81,9 @@ In non-interactive mode, set the tier with `NEMOCLAW_POLICY_TIER` (default: `bal
$ NEMOCLAW_POLICY_TIER=restricted nemoclaw onboard --non-interactive --yes-i-accept-third-party-software
```

If you enable Brave Search during onboarding, NemoClaw currently stores the Brave API key in the sandbox's OpenClaw configuration.
That means the OpenClaw agent can read the key.
NemoClaw explores an OpenShell-hosted credential path first, but the current OpenClaw Brave runtime does not consume that path end to end yet.
Treat Brave Search as an explicit opt-in and use a dedicated low-privilege Brave key.
If you enable web search during onboarding, NemoClaw stores an OpenShell environment resolver in the sandbox's OpenClaw configuration as that provider's `apiKey`.
That keeps the raw key out of `openclaw.json`, but the OpenClaw agent can still resolve and read the key at runtime.
Treat web search as an explicit opt-in and use a dedicated low-privilege provider key.

For non-interactive onboarding, you must explicitly accept the third-party software notice:

Expand All @@ -97,14 +97,17 @@ or:
$ NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 nemoclaw onboard --non-interactive
```

To enable Brave Search in non-interactive mode, set:
To enable web search in non-interactive mode, set a supported provider key:

```console
$ BRAVE_API_KEY=... \
nemoclaw onboard --non-interactive
```

`BRAVE_API_KEY` enables Brave Search in non-interactive mode and also enables `web_fetch`.
Supported keys are `BRAVE_API_KEY`, `GEMINI_API_KEY`, and `TAVILY_API_KEY`.
If more than one is set, NemoClaw prefers Brave, then Gemini, then Tavily unless `NEMOCLAW_WEB_SEARCH_PROVIDER` is set explicitly to `brave`, `gemini`, or `tavily`.
Whichever provider wins that selection has its key copied into the sandbox OpenClaw config, so agents can read the selected provider's `apiKey`.
Enabling web search also enables `web_fetch`.

The wizard prompts for a sandbox name.
Names must follow RFC 1123 subdomain rules: lowercase alphanumeric characters and hyphens only, and must start and end with an alphanumeric character.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,18 @@ The status command detects the sandbox context and reports "active (inside sandb

Run `openshell sandbox list` on the host to check the underlying sandbox state.

### `openclaw update` hangs or times out inside the sandbox

This is expected for the current NemoClaw deployment model.
NemoClaw installs `openclaw` into the sandbox image at build time, so the CLI is image-pinned rather than updated in place inside a running sandbox.

Do not run `openclaw update` inside the sandbox.
Instead:

1. Upgrade to a NemoClaw release that includes the newer `openclaw` version.
2. If you build NemoClaw from source, bump the pinned `openclaw` version in `Dockerfile.base` and rebuild the sandbox base image.
3. Back up any workspace files you need, then recreate the sandbox so it uses the rebuilt image.

### Inference requests time out

Verify that the inference provider endpoint is reachable from the host.
Expand Down
71 changes: 31 additions & 40 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,9 @@ ARG NEMOCLAW_BUILD_ID=default
# before running `nemoclaw onboard`. See #1409.
ARG NEMOCLAW_PROXY_HOST=10.200.0.1
ARG NEMOCLAW_PROXY_PORT=3128
# Non-secret flag: set to "1" when the user configured Brave Search during
# onboard. Controls whether the web search block is written to openclaw.json.
# The actual API key is injected at runtime via openshell:resolve:env, never
# baked into the image.
ARG NEMOCLAW_WEB_SEARCH_ENABLED=0
# Base64-encoded non-secret OpenClaw web-search config fragment. API keys are
# written as openshell:resolve:env references and injected at runtime.
ARG NEMOCLAW_WEB_CONFIG_B64=e30=

# SECURITY: Promote build-args to env vars so the Python script reads them
# via os.environ, never via string interpolation into Python source code.
Expand All @@ -215,14 +213,17 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_DISABLE_DEVICE_AUTH=${NEMOCLAW_DISABLE_DEVICE_AUTH} \
NEMOCLAW_PROXY_HOST=${NEMOCLAW_PROXY_HOST} \
NEMOCLAW_PROXY_PORT=${NEMOCLAW_PROXY_PORT} \
NEMOCLAW_WEB_SEARCH_ENABLED=${NEMOCLAW_WEB_SEARCH_ENABLED}
NEMOCLAW_WEB_CONFIG_B64=${NEMOCLAW_WEB_CONFIG_B64} \
OPENCLAW_STATE_DIR=/sandbox/.openclaw \
OPENCLAW_CONFIG_PATH=/sandbox/.openclaw-data/config/openclaw.json

WORKDIR /sandbox
USER sandbox

# Write the COMPLETE openclaw.json including gateway config and auth token.
# This file is immutable at runtime (Landlock read-only on /sandbox/.openclaw).
# No runtime writes to openclaw.json are needed or possible.
# The live config lives under /sandbox/.openclaw-data/config so OpenClaw CLI
# and Control UI edits can persist after onboarding. /sandbox/.openclaw stays
# as the immutable wrapper path and exposes the live config through a symlink.
# Build args (NEMOCLAW_MODEL, CHAT_UI_URL) customize per deployment.
# Auth token is generated per build so each image has a unique token.
RUN python3 -c "\
Expand Down Expand Up @@ -276,36 +277,31 @@ config = { \
'auth': {'token': secrets.token_hex(32)} \
} \
}; \
config.update({ \
'tools': { \
'web': { \
'search': { \
'enabled': True, \
'provider': 'brave', \
'apiKey': 'openshell:resolve:env:BRAVE_API_KEY' \
}, \
'fetch': {'enabled': True} \
} \
} \
}) if os.environ.get('NEMOCLAW_WEB_SEARCH_ENABLED', '') == '1' else None; \
path = os.path.expanduser('~/.openclaw/openclaw.json'); \
json.dump(config, open(path, 'w'), indent=2); \
os.chmod(path, 0o600)"
web_config = json.loads(base64.b64decode(os.environ.get('NEMOCLAW_WEB_CONFIG_B64', 'e30=') or 'e30=').decode('utf-8')); \
config.update(web_config if isinstance(web_config, dict) else {}); \
state_dir = os.environ.get('OPENCLAW_STATE_DIR', os.path.expanduser('~/.openclaw')); \
config_path = os.environ.get('OPENCLAW_CONFIG_PATH', os.path.join(state_dir, 'openclaw.json')); \
wrapper_path = os.path.join(state_dir, 'openclaw.json'); \
os.makedirs(os.path.dirname(config_path), exist_ok=True); \
os.makedirs(state_dir, exist_ok=True); \
os.remove(wrapper_path) if os.path.lexists(wrapper_path) and not os.path.islink(wrapper_path) else None; \
os.remove(wrapper_path) if os.path.islink(wrapper_path) and os.path.realpath(wrapper_path) != config_path else None; \
os.symlink(config_path, wrapper_path) if not os.path.islink(wrapper_path) else None; \
fh = open(config_path, 'w', encoding='utf-8'); \
json.dump(config, fh, indent=2); \
fh.write('\n'); \
fh.close(); \
os.chmod(config_path, 0o660)"

# Install NemoClaw plugin into OpenClaw
RUN openclaw doctor --fix > /dev/null 2>&1 || true \
&& openclaw plugins install /opt/nemoclaw > /dev/null 2>&1 || true

# Lock openclaw.json via DAC: chown to root so the sandbox user cannot modify
# it at runtime. This works regardless of Landlock enforcement status.
# The Landlock policy (/sandbox/.openclaw in read_only) provides defense-in-depth
# once OpenShell enables enforcement.
# Lock the .openclaw wrapper tree via DAC. The wrapper path stays read-only
# and root-owned so the sandbox user cannot replace symlinks or swap the live
# config path. The active config file itself lives in .openclaw-data/config and
# is shared between the sandbox user and the gateway process.
# Ref: https://github.com/NVIDIA/NemoClaw/issues/514
# Lock the entire .openclaw directory tree.
# SECURITY: chmod 755 (not 1777) — the sandbox user can READ but not WRITE
# to this directory. This prevents the agent from replacing symlinks
# (e.g., pointing /sandbox/.openclaw/hooks to an attacker-controlled path).
# The writable state lives in .openclaw-data, reached via the symlinks.
# hadolint ignore=DL3002
USER root

Expand Down Expand Up @@ -369,14 +365,9 @@ RUN chown root:root /sandbox/.openclaw \
&& rm -rf /root/.npm /sandbox/.npm \
&& find /sandbox/.openclaw -mindepth 1 -maxdepth 1 -exec chown -h root:root {} + \
&& chmod 755 /sandbox/.openclaw \
&& chmod 444 /sandbox/.openclaw/openclaw.json

# Pin config hash at build time so the entrypoint can verify integrity.
# Prevents the agent from creating a copy with a tampered config and
# restarting the gateway pointing at it.
RUN sha256sum /sandbox/.openclaw/openclaw.json > /sandbox/.openclaw/.config-hash \
&& chmod 444 /sandbox/.openclaw/.config-hash \
&& chown root:root /sandbox/.openclaw/.config-hash
&& chown sandbox:gateway /sandbox/.openclaw-data/config /sandbox/.openclaw-data/config/openclaw.json \
&& chmod 2775 /sandbox/.openclaw-data/config \
&& chmod 664 /sandbox/.openclaw-data/config/openclaw.json

# DAC-protect .nemoclaw directory: /sandbox/.nemoclaw is Landlock read_write
# (for plugin state/config), but the parent and blueprints are immutable at
Expand Down
9 changes: 6 additions & 3 deletions Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,14 @@ RUN groupadd -r gateway && useradd -r -g gateway -d /sandbox -s /usr/sbin/nologi
&& mkdir -p /sandbox/.nemoclaw \
&& chown -R sandbox:sandbox /sandbox

# Split .openclaw into immutable config dir + writable state dir.
# Split .openclaw into an immutable wrapper + writable state dir.
# The policy makes /sandbox/.openclaw read-only via Landlock, so the agent
# cannot modify openclaw.json, auth tokens, or CORS settings. Writable
# state (agents, plugins, etc.) lives in .openclaw-data, reached via symlinks.
# cannot replace symlinks or rewrite the wrapper layout. Writable state,
# including the active openclaw.json, lives in .openclaw-data and is exposed
# through fixed symlinks.
# Ref: https://github.com/NVIDIA/NemoClaw/issues/514
RUN mkdir -p /sandbox/.openclaw-data/agents/main/agent \
/sandbox/.openclaw-data/config \
/sandbox/.openclaw-data/extensions \
/sandbox/.openclaw-data/workspace \
/sandbox/.openclaw-data/skills \
Expand All @@ -109,6 +111,7 @@ RUN mkdir -p /sandbox/.openclaw-data/agents/main/agent \
/sandbox/.openclaw-data/sandbox \
/sandbox/.openclaw-data/telegram \
&& mkdir -p /sandbox/.openclaw \
&& ln -s /sandbox/.openclaw-data/config/openclaw.json /sandbox/.openclaw/openclaw.json \
&& ln -s /sandbox/.openclaw-data/agents /sandbox/.openclaw/agents \
&& ln -s /sandbox/.openclaw-data/extensions /sandbox/.openclaw/extensions \
&& ln -s /sandbox/.openclaw-data/workspace /sandbox/.openclaw/workspace \
Expand Down
15 changes: 9 additions & 6 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ $ NEMOCLAW_SINGLE_SESSION=1 curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash
The wizard prompts for a provider first, then collects the provider credential if needed.
Supported non-experimental choices include NVIDIA Endpoints, OpenAI, Anthropic, Google Gemini, and compatible OpenAI or Anthropic endpoints.
Credentials are stored in `~/.nemoclaw/credentials.json`. For file permissions, plaintext storage behavior, and hardening guidance, see [Credential Storage](../security/credential-storage.md).
The sandbox's live OpenClaw config is stored at `/sandbox/.openclaw-data/config/openclaw.json` and exposed at `/sandbox/.openclaw/openclaw.json`, so later `openclaw config set` and dashboard config edits persist after onboarding.
The legacy `nemoclaw setup` command is deprecated; use `nemoclaw onboard` instead.

After provider selection, the wizard prompts for a **policy tier** that controls the default set of network policy presets applied to the sandbox.
Expand All @@ -102,10 +103,9 @@ In non-interactive mode, set the tier with `NEMOCLAW_POLICY_TIER` (default: `bal
$ NEMOCLAW_POLICY_TIER=restricted nemoclaw onboard --non-interactive --yes-i-accept-third-party-software
```

If you enable Brave Search during onboarding, NemoClaw currently stores the Brave API key in the sandbox's OpenClaw configuration.
That means the OpenClaw agent can read the key.
NemoClaw explores an OpenShell-hosted credential path first, but the current OpenClaw Brave runtime does not consume that path end to end yet.
Treat Brave Search as an explicit opt-in and use a dedicated low-privilege Brave key.
If you enable web search during onboarding, NemoClaw stores an OpenShell environment resolver in the sandbox's OpenClaw configuration as that provider's `apiKey`.
That keeps the raw key out of `openclaw.json`, but the OpenClaw agent can still resolve and read the key at runtime.
Treat web search as an explicit opt-in and use a dedicated low-privilege provider key.

For non-interactive onboarding, you must explicitly accept the third-party software notice:

Expand All @@ -119,14 +119,17 @@ or:
$ NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 nemoclaw onboard --non-interactive
```

To enable Brave Search in non-interactive mode, set:
To enable web search in non-interactive mode, set a supported provider key:

```console
$ BRAVE_API_KEY=... \
nemoclaw onboard --non-interactive
```

`BRAVE_API_KEY` enables Brave Search in non-interactive mode and also enables `web_fetch`.
Supported keys are `BRAVE_API_KEY`, `GEMINI_API_KEY`, and `TAVILY_API_KEY`.
If more than one is set, NemoClaw prefers Brave, then Gemini, then Tavily unless `NEMOCLAW_WEB_SEARCH_PROVIDER` is set explicitly to `brave`, `gemini`, or `tavily`.
Whichever provider wins that selection has its key copied into the sandbox OpenClaw config, so agents can read the selected provider's `apiKey`.
Enabling web search also enables `web_fetch`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

The wizard prompts for a sandbox name.
Names must follow RFC 1123 subdomain rules: lowercase alphanumeric characters and hyphens only, and must start and end with an alphanumeric character.
Expand Down
Loading