Skip to content
Open
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
66 changes: 29 additions & 37 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,17 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_INFERENCE_API=${NEMOCLAW_INFERENCE_API} \
NEMOCLAW_INFERENCE_COMPAT_B64=${NEMOCLAW_INFERENCE_COMPAT_B64} \
NEMOCLAW_WEB_CONFIG_B64=${NEMOCLAW_WEB_CONFIG_B64} \
NEMOCLAW_DISABLE_DEVICE_AUTH=${NEMOCLAW_DISABLE_DEVICE_AUTH}
NEMOCLAW_DISABLE_DEVICE_AUTH=${NEMOCLAW_DISABLE_DEVICE_AUTH} \
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 @@ -123,51 +126,40 @@ config = { \
'auth': {'token': secrets.token_hex(32)} \
} \
}; \
config.update({ \
'tools': { \
'web': { \
'search': { \
'enabled': True, \
'provider': 'brave', \
**({'apiKey': web_config.get('apiKey', '')} if web_config.get('apiKey', '') else {}) \
}, \
'fetch': { \
'enabled': bool(web_config.get('fetchEnabled', True)) \
} \
} \
} \
} if web_config.get('provider') == 'brave' else {}); \
path = os.path.expanduser('~/.openclaw/openclaw.json'); \
json.dump(config, open(path, 'w'), indent=2); \
os.chmod(path, 0o600)"
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); \
if os.path.lexists(wrapper_path) and not os.path.islink(wrapper_path): \
os.remove(wrapper_path); \
if os.path.islink(wrapper_path) and os.path.realpath(wrapper_path) != config_path: \
os.remove(wrapper_path); \
if not os.path.islink(wrapper_path): \
os.symlink(config_path, wrapper_path); \
with open(config_path, 'w', encoding='utf-8') as fh: \
json.dump(config, fh, indent=2); \
fh.write('\n'); \
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
RUN chown root:root /sandbox/.openclaw \
&& 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

# Entrypoint runs as root to start the gateway as the gateway user,
# then drops to sandbox for agent commands. See nemoclaw-start.sh.
Expand Down
9 changes: 6 additions & 3 deletions Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,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 @@ -103,6 +105,7 @@ RUN mkdir -p /sandbox/.openclaw-data/agents/main/agent \
/sandbox/.openclaw-data/cron \
/sandbox/.openclaw-data/memory \
&& 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
Loading