Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7dc6559
fix(image): default OpenClaw sandbox to non-root
deepujain Jul 30, 2026
2a14846
fix(image): expose root entrypoint mode
deepujain Jul 31, 2026
2f0e87c
test(image): cover non-root and root startup paths
deepujain Jul 30, 2026
8091faf
docs(security): clarify root supervisor guidance
deepujain Jul 30, 2026
c29eda4
Merge branch 'main' into fix/7882-default-sandbox-user
cv Aug 4, 2026
95f5cf7
Merge branch 'main' into fix/7882-default-sandbox-user
deepujain Aug 4, 2026
6945ee1
Merge branch 'main' into fix/7882-default-sandbox-user
deepujain Aug 4, 2026
0f8467f
Merge branch 'main' into fix/7882-default-sandbox-user
deepujain Aug 4, 2026
7460b1a
Merge branch 'main' into fix/7882-default-sandbox-user
deepujain Aug 5, 2026
64511cc
Merge branch 'main' into fix/7882-default-sandbox-user
deepujain Aug 5, 2026
957b7ae
merge(main): refresh non-root sandbox user fix
prekshivyas Aug 5, 2026
574cbc0
fix(image): preserve managed root runtime contract
prekshivyas Aug 5, 2026
7ae5223
ci(e2e): retrigger after hosted inference recovery
prekshivyas Aug 5, 2026
91b36d5
Merge branch 'main' into fix/7882-default-sandbox-user
deepujain Aug 6, 2026
ca55fc7
Merge branch 'main' into fix/7882-default-sandbox-user
deepujain Aug 7, 2026
d41976f
merge(main): refresh non-root sandbox image fix
cv Aug 7, 2026
f7ecc70
merge(main): refresh non-root sandbox image fix
cv Aug 7, 2026
a6a82aa
docs(security): clarify non-root image terminology
cv Aug 7, 2026
8996e4e
docs(security): separate runtime user guidance
cv Aug 7, 2026
00ed92e
docs(security): sync process controls with main
prekshivyas Aug 8, 2026
cbf623a
test(shields): allow policy transition setup time
prekshivyas Aug 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
3 changes: 3 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5080,9 +5080,11 @@ jobs:
matrix:
include:
- agent: openclaw
expect_non_root_entrypoint: "1"
sandbox_name: e2e-oc-security
test_file: test/e2e/live/full-e2e.test.ts
- agent: hermes
expect_non_root_entrypoint: "0"
sandbox_name: e2e-hm-security
test_file: test/e2e/live/hermes-e2e.test.ts
env:
Expand All @@ -5094,6 +5096,7 @@ jobs:
NEMOCLAW_E2E_USE_HOSTED_INFERENCE: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_AGENT: ${{ matrix.agent }}
NEMOCLAW_E2E_EXPECT_NON_ROOT_ENTRYPOINT: ${{ matrix.expect_non_root_entrypoint }}
# Legacy-parity contract: enforce a non-root host user. PID 1 must stay
# root long enough to step down child processes, so its uid, bounding
# set, and NoNewPrivs remain evidence unless an opt-in expectation is set.
Expand Down
18 changes: 12 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -1203,9 +1203,9 @@
# union. It is inert by default and must never be enabled for a deployment-
# specific Dockerfile build carrying an active messaging plan.
ARG NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=0
# OpenClaw already uses a root supervisor; the explicit value keeps the managed
# image entry-user contract uniform with Hermes and DCode publication.
ARG NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root
# OpenShell requires USER sandbox as the image default. The managed-image
# publication workflow selects root to preserve gateway and agent UID isolation.
ARG NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=sandbox
# Base64-encoded JSON array of secondary OpenClaw agent config entries
# (e.g. [{"id":"research","workspace":"/sandbox/.openclaw/workspace-research",
# "agentDir":"/sandbox/.openclaw/agents/research", ...}]).
Expand All @@ -1217,10 +1217,10 @@
# CHAT_UI_URL is a non-loopback address (Brev Launchable, remote deployments)
# since terminal-based pairing is impossible in those contexts.
# Default: "0" (device auth enabled for local deployments — secure by default).
ARG NEMOCLAW_DISABLE_DEVICE_AUTH=0

Check warning on line 1220 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
# Internal audit provenance for the opt-out above. Standard onboarding rewrites
# this to managed-onboard; direct image builders retain operator provenance.
ARG NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE=operator

Check warning on line 1223 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
# Compatibility build arg for older custom Dockerfiles and rebuild tooling.
# NemoClaw-managed images intentionally do not consume it; gateway auth tokens
# are generated at container startup and are never baked into image layers.
Expand Down Expand Up @@ -1254,7 +1254,7 @@
# NEMOCLAW_MESSAGING_PLAN_B64 intentionally remains ARG-only: Docker exposes it
# to build RUN processes without retaining the full plan in the final image env.
# Direct ARG interpolation into inline source is a code injection vector (C-2).
ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \

Check warning on line 1257 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1257 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
NEMOCLAW_INFERENCE_PROVIDER_ID=${NEMOCLAW_INFERENCE_PROVIDER_ID} \
NEMOCLAW_UPSTREAM_PROVIDER=${NEMOCLAW_UPSTREAM_PROVIDER} \
NEMOCLAW_PRIMARY_MODEL_REF=${NEMOCLAW_PRIMARY_MODEL_REF} \
Expand Down Expand Up @@ -1298,7 +1298,11 @@
;; \
*) echo "ERROR: NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION must be 0 or 1" >&2; exit 1 ;; \
esac \
&& test "$NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER" = "root"
&& case "$NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER" in \
root|sandbox) ;; \
*) echo "ERROR: NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER must be root or sandbox" >&2; exit 1 ;; \
esac \
&& command -v setpriv >/dev/null 2>&1

# Bake reduced messaging runtime metadata for the entrypoint. The full
# NEMOCLAW_MESSAGING_PLAN_B64 is a build input; OpenShell sandbox create only
Expand Down Expand Up @@ -1976,7 +1980,9 @@
test -z "$(dpkg --audit)"
# End completed-image security package verification.

# Entrypoint runs as root to start the gateway as the gateway user,
# then drops to sandbox for agent commands. See nemoclaw-start.sh.
# Stock builds use a non-root OCI default for OpenShell compatibility.
# Deployments that require gateway and agent UID isolation can override
# the runtime user to root.
USER ${NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER}
ENTRYPOINT ["/usr/local/bin/nemoclaw-start"]
CMD ["/bin/bash"]
3 changes: 3 additions & 0 deletions docs/deployment/install-openclaw-plugins.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,12 @@ RUN chown sandbox:sandbox /sandbox/.openclaw/openclaw.json \
&& sha256sum /sandbox/.openclaw/openclaw.json > /sandbox/.openclaw/.config-hash \
&& chown sandbox:sandbox /sandbox/.openclaw/.config-hash \
&& chmod 660 /sandbox/.openclaw/.config-hash

USER sandbox
```

The final stage inherits the stock runtime entrypoint, command, gateway health check, generated configuration, and file permissions.
The final `USER sandbox` sets the non-root default user that OpenShell requires.
It redeclares and promotes the tool-disclosure build argument because Docker build arguments are scoped to a stage and the appended plugin stage becomes the final image stage.

The local install copies the staged plugin into OpenClaw's extensions tree, records the install, links it to the image's OpenClaw runtime, and leaves existing managed plugin load paths intact before the explicit enable and inspect steps.
Expand Down
98 changes: 87 additions & 11 deletions docs/security/process-controls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,58 @@ Refer to [Review Sandbox Hardening](../../manage-sandboxes/configure-sandboxes/r
| Risk if relaxed | `CAP_SYS_ADMIN` and `CAP_SYS_PTRACE` expand kernel and process attack surface. `CAP_NET_RAW` allows raw socket access for network sniffing. `CAP_DAC_OVERRIDE` bypasses filesystem permission checks. If `capsh` cannot run or `CAP_SETPCAP` is unavailable, the container retains more of the runtime-provided capability set. |
| Recommendation | Run on an image that includes `capsh` and `setpriv` (NemoClaw-managed images include them). For defense-in-depth, also pass `--cap-drop=ALL` at the container runtime level. |

<AgentOnly variant="openclaw">

## Gateway Process Isolation

Gateway and agent UID isolation depends on the container process topology.
A direct root-entrypoint container runs the gateway as the separate `gateway` user and the agent as the `sandbox` user.
An OpenShell-managed container has OpenShell as PID 1 and launches nonroot `nemoclaw-start`, so the supervisor, gateway, and agent all use the `sandbox` UID.
The stock OpenClaw image defaults to the `sandbox` user for OpenShell compatibility.
An OpenShell-managed container has OpenShell as PID 1 and launches `nemoclaw-start` as a non-root process, so the supervisor, gateway, and agent all use the `sandbox` UID.
The managed-image publication workflow explicitly sets `NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root` for its reviewed release images.
That build-time setting lets the entrypoint run the gateway as the separate `gateway` user and agent commands as the `sandbox` user.
A direct container runtime can override the image user to `root`.
That root-entrypoint topology runs the gateway as the separate `gateway` user and agent commands as the `sandbox` user.

| Aspect | Detail |
|---|---|
| Default | Direct root entrypoints use `setpriv` for gateway UID isolation. The OpenShell-managed topology runs the gateway and agent under the same sandbox UID because `no-new-privileges` prevents the nonroot entrypoint from changing users. |
| What you can change | This is not a user-facing knob. The selected container topology determines whether the entrypoint can step down from root to the gateway UID. |
| Default | A stock Dockerfile build declares `USER sandbox`. The OpenShell-managed topology runs the gateway and agent under the same sandbox UID because `no-new-privileges` prevents the non-root entrypoint from changing users. |
| What you can change | The managed-image publication workflow can set `NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root` at build time. A direct container runtime can also override the image user to `root`. OpenClaw custom images must keep `USER sandbox` as the default. |
| Risk if relaxed | A same-UID agent can signal peer processes and can attempt to imitate the expected gateway process shape. The root managed controller prevents PID-reuse mistakes, but it cannot prove provenance against a malicious same-UID process or provide the direct root-entrypoint restart seal for mutable config. |
| Recommendation | Use a direct root-entrypoint deployment when separate gateway and agent UIDs are required. Treat the managed controller as authenticated lifecycle and exact-target safety, not as a same-UID provenance boundary. |
| Recommendation | Keep the stock OpenClaw image default user as `sandbox`. Use the managed-image publication setting or a direct root-entrypoint deployment when separate gateway and agent UIDs are required. Treat an OpenShell-managed controller that shares the `sandbox` UID as an authenticated lifecycle control, not as proof that the target process is trustworthy. |

</AgentOnly>
<AgentOnly variant="hermes">

## Gateway Process Isolation

The stock Hermes image and the managed-image publication workflow set the OCI default user to `root`.
The entrypoint runs the Hermes agent gateway as the `gateway` user and agent commands as the `sandbox` user.
If a runtime starts the entrypoint as a non-root user, the supervisor, agent gateway, and agent commands share that UID.

| Aspect | Detail |
|---|---|
| Default | The OCI default user is `root`. The entrypoint runs the agent gateway as `gateway` and agent commands as `sandbox`. |
| What you can change | A direct container runtime can override the image user. A non-root override removes separate gateway and agent UIDs. |
| Risk if relaxed | A same-UID agent can signal the agent gateway and attempt to imitate its process shape. Running agent commands as `root` also expands filesystem access and container escape impact. |
| Recommendation | Keep the root entrypoint when the deployment requires separate gateway and agent UIDs. Keep agent commands under the `sandbox` user. |

</AgentOnly>
<AgentOnly variant="deepagents">

## Gateway Process Isolation

The stock Deep Agents image defaults to the `sandbox` user.
The managed-image publication workflow sets the OCI default user to `root`, but the entrypoint immediately switches to `sandbox`.
Deep Agents does not use a separate agent gateway UID in this topology.

| Aspect | Detail |
|---|---|
| Default | Stock builds declare `USER sandbox`. Images from the managed-image publication workflow use `root` only for the startup handoff to `sandbox`. |
| What you can change | A direct container runtime can override the image user, but the entrypoint switches a root process to `sandbox` before it starts Deep Agents. |
| Risk if relaxed | Running the terminal agent as `root` expands filesystem access and increases the impact of container escape vulnerabilities. |
| Recommendation | Keep the terminal runtime under the `sandbox` user. Use `root` only for the managed startup handoff. |

</AgentOnly>

## No New Privileges

Expand All @@ -76,7 +116,7 @@ The `no-new-privileges` flag prevents processes from gaining additional privileg
| Default | OpenShell sets `PR_SET_NO_NEW_PRIVS` using `prctl()` inside the sandbox process as part of the seccomp filter setup. The NemoClaw Compose example also shows the equivalent `security_opt: no-new-privileges:true` setting. |
| What you can change | OpenShell's seccomp path enforces this inside the sandbox. It is not a user-facing knob. |
| Risk if relaxed | Without this flag, a compromised process could execute a setuid binary to escalate to root inside the container, then attempt container escape techniques. |
| Recommendation | No action needed. OpenShell enforces this automatically when the sandbox network policy is active. In the OpenShell-managed topology, this flag also means the nonroot entrypoint and gateway remain under the sandbox UID. |
| Recommendation | No action needed. OpenShell enforces this automatically when the sandbox network policy is active. When an OpenShell-managed topology starts an entrypoint as a non-root user, this flag prevents that process from changing users. |

## Process Limit

Expand Down Expand Up @@ -174,17 +214,53 @@ The managed headless path automatically approves non-shell tool requests such as

</AgentOnly>

<AgentOnly variant="openclaw">

## Non-Root User

The sandbox runs agent processes as a dedicated `sandbox` user and group.
The entrypoint starts as root for privilege separation, then drops to the `sandbox` user for all agent commands.
The stock OpenClaw image starts the entrypoint as `sandbox` for OpenShell compatibility.
A build from the managed-image publication workflow can select `root` so the entrypoint starts the gateway and agent commands under separate UIDs.
A direct runtime can override the image user to `root`, which lets the entrypoint separate the `gateway` and `sandbox` UIDs before it runs agent commands.

| Aspect | Detail |
|---|---|
| Default | `run_as_user: sandbox`, `run_as_group: sandbox`. A separate `gateway` user runs the gateway process. |
| What you can change | Change the `process` section in the policy file to run as a different user. |
| Risk if relaxed | Running as `root` inside the container gives the agent access to modify any file in the container filesystem and increases the impact of container escape vulnerabilities. |
| Recommendation | Never run as root. Keep the `sandbox` user. |
| Default | `run_as_user: sandbox`, `run_as_group: sandbox`. The stock OpenClaw image runs the entrypoint, gateway, and agent under the sandbox UID. |
| What you can change | Change the `process` section in the policy file to run as a different user. The managed-image publication workflow can select the root entrypoint at build time, and direct container runtimes can override the image user to `root`. |
| Risk if relaxed | Running agent commands as `root` gives the agent access to modify any file in the container filesystem and increases the impact of container escape vulnerabilities. |
| Recommendation | Keep agent commands under the `sandbox` user. Use `root` only for the entrypoint supervisor when the managed-image publication workflow or a direct container runtime selects it. Do not run agent commands as `root` or make `root` the OpenClaw image default. |

</AgentOnly>
<AgentOnly variant="hermes">

## Non-Root User

The Hermes entrypoint starts as `root` and runs the agent gateway as `gateway`.
It runs the dashboard and agent commands as `sandbox`.

| Aspect | Detail |
|---|---|
| Default | The image entrypoint uses `root`. The agent gateway uses `gateway`, and the dashboard and agent commands use `sandbox`. |
| What you can change | A direct container runtime can override the image user, but a non-root override removes the separate gateway UID. |
| Risk if relaxed | Running dashboard or agent commands as `root` expands filesystem access and increases the impact of container escape vulnerabilities. |
| Recommendation | Keep dashboard and agent commands under the `sandbox` user. Use `root` only for the entrypoint supervisor. |

</AgentOnly>
<AgentOnly variant="deepagents">

## Non-Root User

The stock Deep Agents image starts as `sandbox`.
An image from the managed-image publication workflow starts as `root` only long enough to switch immediately to `sandbox`.

| Aspect | Detail |
|---|---|
| Default | The terminal runtime and agent commands use the `sandbox` user. |
| What you can change | A direct container runtime can override the image user, but the entrypoint switches a root process to `sandbox` before it starts Deep Agents. |
| Risk if relaxed | Running the terminal agent as `root` expands filesystem access and increases the impact of container escape vulnerabilities. |
| Recommendation | Keep the terminal runtime and agent commands under the `sandbox` user. Use `root` only for the managed startup handoff. |

</AgentOnly>

## PATH Hardening

Expand Down
2 changes: 2 additions & 0 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5788,6 +5788,8 @@ fi

# ── Root path (full privilege separation via setpriv) ──────────

echo "[gateway] NEMOCLAW_ENTRYPOINT_MODE=root" >&2

# Empty-config recovery runs before integrity check so a #3118 truncation
# (openshell inference set inside the sandbox) is restored from baseline
# rather than failing the integrity hash for the empty file.
Expand Down
2 changes: 1 addition & 1 deletion src/lib/shields/policy-transition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe("shields policy transition", () => {
vi.spyOn(console, "error").mockImplementation(() => undefined);
vi.spyOn(console, "log").mockImplementation(() => undefined);
shields = requireSource(SHIELDS_MODULE);
});
}, 30_000);

afterEach(() => {
vi.restoreAllMocks();
Expand Down
Loading
Loading