Skip to content

Allow sandbox supervisor to skip inner network namespace creation on hardened / immutable hosts (rootless Podman, dm-verity, overlayroot) #1194

Closed as not planned

Description

@crdion

Problem Statement

On hardened and immutable Linux hosts — dm-verity-protected rootfs, overlayroot, rootless Podman, and similar configurations — the sandbox supervisor cannot create its inner network namespace because setns is rejected by the kernel. Everything else in the OpenShell stack works in these environments: the standalone gateway runs natively, the Docker driver talks to a Podman socket cleanly, the sandbox container starts, and the supervisor successfully calls back to the gateway over gRPC. The only step that fails is ip netns add, which is the supervisor's first action after policy validation.

Concretely, this blocks every host where the kernel mount/network namespace policy is more restrictive than a stock mutable Linux distribution — including ARM64 NVIDIA GB10 / DGX Spark systems running an immutable Host OS, Bottlerocket, Talos, MicroOS, CIS-hardened RHEL/Ubuntu images, and any rootless-only environment where unprivileged setns across the host's mount namespace is denied.

We're not asking OpenShell to weaken any isolation on platforms that can enforce it. We're asking for a way to opt sandboxes into a "host-network" execution mode so that on a host where the kernel itself prevents the supervisor from creating its private netns, the sandbox can still run with the rest of the policy engine intact (filesystem, process, seccomp, OPA L7 proxy in pass-through / observation mode).

This is a small surface change with high leverage: it unblocks every overlayroot/immutable/rootless-restricted platform.


Proposed Design

Add a sandbox-level network mode that the supervisor honors at startup, with three values:

Mode Behavior
isolated (current behavior) Supervisor creates inner netns + veth pair, runs OPA L7 proxy with full egress filtering.
host Supervisor skips ip netns add and the veth setup. Sandbox process runs in the container's existing network namespace (which is already host-mode in the Docker driver after #1080). OPA L7 proxy still runs but in pass-through / observation mode (logs verdicts, does not enforce egress). Filesystem, process, and seccomp policies are unchanged.
auto (suggested new default) Supervisor probes capability at startup: tries unshare(CLONE_NEWNET) or ip netns add once; on EPERM falls back to host mode and emits a degraded-mode telemetry event.

Surface changes

  1. CLI / spec — Add a network_mode field to the sandbox template (isolated | host | auto). Or a simpler --host-network flag on openshell sandbox create.
  2. Gateway — Plumb the value through to the driver as part of the existing DriverSandbox proto.
  3. Supervisor — Single decision branch in the netns-creation step. The ip netns add call (and the subsequent veth setup) is gated on network_mode != Host. If host (or auto after probe), skip namespace creation and skip the veth/proxy-redirect setup. The OPA proxy can still run as a sidecar process listening on the container's own loopback for observability.
  4. Telemetry — Emit an OCSF event such as CONFIG:DEGRADED when running in host mode so operators can see clearly that egress enforcement is observation-only on that sandbox.

Why this is the right shape

  • It does not weaken the default isolation — isolated remains available for platforms where it works.
  • It mirrors the design already taken for the Docker driver in feat(driver-docker): use host networking for sandboxes #1080 (host networking for the container itself). This change brings the supervisor's inner namespace in line with the container's outer namespace on platforms that need it.
  • It is platform-detection-friendly: auto lets the supervisor self-degrade rather than requiring operators to know in advance whether their kernel will permit setns.
  • It keeps every other layer of the policy engine intact (filesystem Landlock, seccomp, OPA, provider env injection). Users on these platforms still get most of OpenShell's value.

Alternatives Considered

  1. Make the gateway run in a kernel-permissive container. Tried — putting the entire k3s cluster image in a privileged Podman container also fails on overlayroot because k3s itself needs veth pairs for pod networking, and kube-proxy needs nft (also blocked). The standalone gateway binary sidesteps this entirely; this issue is the last mile.
  2. Run the sandbox container with broader capabilities. Already done — the supervisor runs with SYS_ADMIN, NET_ADMIN, SYS_PTRACE, SYSLOG, SETUID, SETGID, DAC_READ_SEARCH. The blocker is not capability-based; it is the kernel refusing setns() on the host's mount namespace regardless of the capability set.
  3. Patch the host kernel to permit netns operations on overlayroot. Possible but has security implications for the host, is not portable across distributions, and requires every operator to maintain a custom kernel. Pushing the workaround into the host is the wrong direction; a network_mode flag in OpenShell is portable and explicit.
  4. Use a different CNI plugin. Not applicable — this failure is at the supervisor level, after the container is already created. We confirmed bridge, ptp, host-local, and netavark all fail at the same setns boundary inside the container; CNI is not involved at this point.

Reproduction (against current dev build)

# Platform: ARM64 immutable host (dm-verity rootfs + overlayroot)
# Kernel:   6.11.x
# Podman:   5.4.1 (with netavark, passt, aardvark-dns, crun 1.20)
# OpenShell: 0.0.37-dev.79+g721c39f1 (standalone gateway binary)

# 1. Start the standalone gateway with the Docker driver pointed at the user Podman socket
DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock \
  openshell-gateway \
  --db-url 'sqlite:///path/to/gateway.db?mode=rwc' \
  --drivers docker \
  --disable-tls \
  --port 8080 \
  --sandbox-image ghcr.io/nvidia/openshell-community/sandboxes/base:latest \
  --docker-supervisor-image ghcr.io/nvidia/openshell/supervisor:latest \
  --grpc-endpoint http://<HOST_IP>:8080 \
  --ssh-gateway-host <HOST_IP> \
  --ssh-handshake-secret $(openssl rand -hex 32)

# 2. Register and create a sandbox
openshell gateway add http://127.0.0.1:8080 --local --name local
openshell sandbox create --name test-sandbox

What works (everything up to the netns step)

Using compute driver: docker
Extracting supervisor binary from image to host cache
Server listening: address=0.0.0.0:8080
gateway add: Status: Connected, Version: 0.0.37-dev.79+g721c39f1

Created sandbox: test-sandbox
  [0.0s] Requesting compute...

The container starts. The supervisor runs, calls back to the gateway, and the gateway logs show successful gRPC traffic for the entire bring-up sequence:

  • GetSandboxConfig → 200
  • GetSandboxProviderEnvironment → 200
  • PushSandboxLogs → 200 (repeatedly)

Where it fails (supervisor ip netns add)

Supervisor logs from podman logs <sandbox-container>:

2026-05-04T00:27:18.279Z INFO openshell_sandbox: Starting sandbox
2026-05-04T00:27:18.279Z INFO openshell_sandbox: Fetching sandbox policy via gRPC
2026-05-04T00:27:18.322Z INFO openshell_sandbox: Creating OPA engine from proto policy data
2026-05-04T00:27:18.325Z OCSF CONFIG:VALIDATED [INFO] Validated 'sandbox' user exists in image
2026-05-04T00:27:18.327Z OCSF CONFIG:LOADED [INFO] Fetched provider environment [env_count:0]
2026-05-04T00:27:18.331Z OCSF CONFIG:ENABLED [INFO] TLS termination enabled: ephemeral CA generated
2026-05-04T00:27:18.331Z OCSF CONFIG:CREATING [INFO] Creating network namespace
                          [ns:sandbox-b5266943
                           host_veth:veth-h-b5266943
                           sandbox_veth:veth-s-b5266943]

Error:   × Network namespace creation failed and proxy mode requires isolation.
  │ Ensure CAP_NET_ADMIN and CAP_SYS_ADMIN are available and iproute2 is
  │ installed. Error: ip netns add sandbox-b5266943 failed:
  │ setns: Operation not permitted

The sandbox container then exits 1 and the gateway eventually reports:

Error: × sandbox provisioning timed out after 300s. Last reported status:
  │ DependenciesNotReady: Container is running; waiting for supervisor relay

The supervisor has all the capabilities the error message asks for — they are set by the driver per crates/openshell-driver-podman/src/container.rs. The kernel is rejecting setns because the host's mount namespace policy does not permit it for unprivileged containers, regardless of capability set.


Agent Investigation

We investigated the codebase and the failure path with the help of a coding agent. Findings:

  • Standalone gateway is the right entry point on hardened hosts. Running openshell-gateway natively (rather than via openshell gateway start, which puts k3s + containerd inside a container) sidesteps the broader kernel-level problems with k3s on overlayroot (veth, iptables/nft, kubelet cgroup writes). The standalone path got us 90% to a working sandbox with no other workarounds beyond pointing DOCKER_HOST at the user Podman socket.
  • Docker driver + Podman socket is functional. With podman-docker and the user-level Podman socket, the Docker compute driver connects to Podman cleanly. No code changes were needed on either side. The connection uses bollard against the Docker-compat API exposed by Podman.
  • The Podman driver currently hardcodes bridge mode. crates/openshell-driver-podman/src/container.rs (around lines 110 and 164) sets netns: NetNS { nsmode: "bridge" } unconditionally. Even after the host-networking improvements in feat(driver-docker): use host networking for sandboxes #1080 for the Docker driver, the Podman driver's container spec still requests bridge networking, which trips netavark on overlayroot before the supervisor even starts. Routing via --drivers docker against the same Podman socket is the workaround that gets us past this layer; the real fix would be to bring the Podman driver in line with feat(driver-docker): use host networking for sandboxes #1080.
  • The supervisor's netns-creation is the actual deepest blocker. Once we routed around the driver-level bridge issue by using --drivers docker, the supervisor reached the point where it tries to create its own inner netns inside the (host-networked) container. That is the precise line where setns returns EPERM. This is the smallest, most surgical fix point for the project — it's the topic of this issue.
  • Filesystem / process / seccomp / OPA all work in this environment. We saw the supervisor successfully validate the policy, generate the ephemeral CA, fetch the provider environment, and emit OCSF events. None of those layers are blocked. Only the network-isolation layer is.

Why this matters

  • Unblocks immutable Host OS images (dm-verity / verified-boot images, A/B-slot edge OSes, etc.).
  • Unblocks rootless-only environments where unprivileged setns across the host mount namespace is denied by policy.
  • Unblocks hardened distributions in the same family — Bottlerocket, Talos, MicroOS, Fedora CoreOS, CIS-hardened images.
  • Aligns the supervisor's network model with the container-runtime-side decision already taken for Docker sandboxes in feat(driver-docker): use host networking for sandboxes #1080 (use host networking).
  • Provides a clean degradation path with explicit OCSF telemetry, so operators always know which sandboxes are running with reduced egress enforcement.

We're happy to test patches on real ARM64 hardware running an immutable Host OS as soon as they land in dev.


Environment

  • Hardware: NVIDIA GB10 / DGX Spark, ARM64 / aarch64
  • Host OS: Immutable Ubuntu 24.04 ARM64 base, dm-verity verified rootfs, overlayroot tmpfs overlay
  • Kernel: 6.11.0 (NVIDIA arm64 build)
  • Container runtime: Podman 5.4.1 + netavark 1.12.1 + passt + aardvark-dns + crun 1.20
  • OpenShell: 0.0.37-dev.79+g721c39f1 (standalone openshell-gateway-aarch64-unknown-linux-gnu)
  • NemoClaw: 0.0.17 (CLI, blocked downstream of this issue)
  • GPU: NVIDIA GB10, Driver 580.x, CUDA 13.0 (CDI working — nvidia.com/gpu=all passes through to Podman containers fine)

Checklist

Activity

  1. maxamillion commented on May 6, 2026

    @maxamillion
    Collaborator

    @crdion before we get into the details of the proposed change itself, I'm curious why you're using the docker driver with rootless podman instead of the podman driver?

  2. cgwalters commented on May 6, 2026

    @cgwalters
    Contributor

    I don't think there's any relationship between "immutable" and setns here. Some "mutable" systems may have extra LSM/SELinux/AppArmor/whatever applied too that could break things. I think you need to dig into precisely what that is.

  3. maxamillion commented on May 6, 2026

    @maxamillion
    Collaborator

    I looked into this a bit, my current theory is that the docker ComputeDriver container spec isn't configured correctly to work with rootless podman, nor does it handle the /run/netns as needed but the podman ComputeDriver handles these things and actively targets rootless podman. I don't know enough about docker to have an opinion of what is or isn't correct for the docker driver when used with docker though so I'm uncertain if any changes are actually necessary. Please test with the podman driver and report back.

  4. self-assigned this
    on May 6, 2026
  5. maxamillion commented on May 6, 2026

    @maxamillion
    Collaborator

    Also, if you could provide the following that would be helpful. Thank you!

    1. /proc/1/status inside the container (seccomp: 2 means a filter is active)
    2. ausearch -m seccomp on the host for seccomp audit events
    3. strace -f ip netns add test inside the container to see which exact syscall returns EPERM
  6. crdion commented on May 7, 2026

    @crdion
    Author

    Thank you @maxamillion and @cgwalters — you were both right, and the testing confirmed it clearly.

    TL;DR

    The Podman driver works correctly for rootless Podman. The actual blocker is that this platform's kernel (6.11.0-1016-nvidia, NVIDIA DGX Spark / GB10) does not ship the veth kernel module, so the Podman driver's custom bridge network fails at the netavark layer before the supervisor ever starts.

    The default podman network (pasta/slirp4netns) works end-to-end, including ip netns add inside the container.

    Addressing the review comments

    @maxamillion (comment #1): I used the Docker driver because the NemoClaw installer wired it that way. You're correct that the Podman driver is the right choice — it properly mounts a tmpfs at /run/netns and handles rootless Podman. Switching to --drivers podman immediately got further.

    @cgwalters (comment #2): You're correct — the relationship is not "immutable → setns fails." The actual mechanism is a missing kernel module (veth), not LSM/SELinux/AppArmor/seccomp. I should not have attributed it to overlayroot.

    @maxamillion (comment #3): Confirmed. The Docker ComputeDriver container spec does not mount /run/netns as tmpfs and does not target rootless Podman. The Podman driver handles both correctly.

    Test results with Podman driver (--drivers podman)

    Test Result
    Gateway startup (Podman driver, rootless socket) PASS
    Custom bridge network (openshell) container start FAIL: netavark: create veth pair: Netlink error: Operation not supported (os error 95)
    Default podman network container start PASS
    ip netns add inside container (default net + tmpfs /run/netns) PASS
    unshare(CLONE_NEWNET) inside container PASS
    veth pair creation inside container FAIL: EOPNOTSUPP (no veth module in kernel)
    Gateway reachable from container via host.containers.internal:8080 PASS

    Root cause

    $ modprobe veth
    modprobe: FATAL: Module veth not found in directory /lib/modules/6.11.0-1016-nvidia
    
    $ find /lib/modules/$(uname -r) -name 'veth*'
    (no output)
    

    The NVIDIA kernel for this platform does not include veth.ko. The netlink RTM_NEWLINK for device type veth returns EOPNOTSUPP at the kernel level. This blocks:

    1. netavark bridge networking — the Podman driver creates a custom openshell bridge network; netavark tries to create a veth pair to connect the container and fails
    2. Supervisor inner veth setup — even if the container started, the supervisor's veth pair creation for the inner namespace would also fail

    The default podman network avoids this because rootless Podman uses pasta (or slirp4netns) which operates via a userspace TAP device, not veth pairs.

    Requested diagnostics (@maxamillion comment #4)

    1. /proc/1/status inside container (seccomp=unconfined, caps matching Podman driver spec):

    CapInh: 0000000000000000
    CapPrm: 00000000802415fb
    CapEff: 00000000802415fb
    CapBnd: 00000000802415fb
    CapAmb: 0000000000000000
    Seccomp: 0
    Seccomp_filters: 0
    

    No seccomp filter active. Not the blocker.

    2. ausearch -m seccomp on host:

    <no matches>
    

    No seccomp audit events. Not the blocker.

    3. strace -f ip netns add test inside container (default network + tmpfs at /run/netns):

    unshare(CLONE_NEWNET)                   = 0
    mount("/proc/self/ns/net", "/run/netns/test", MS_BIND) = 0
    setns(5, CLONE_NEWNET)                  = 0
    exit_group(0)
    

    All syscalls succeed. The Podman driver's tmpfs mount at /run/netns is the correct fix for rootless environments.

    Bonus: strace of veth creation failure:

    sendmsg(3, RTM_NEWLINK {type=veth, name="veth0", peer="veth1"}) = 92
    recvmsg(3, NLMSG_ERROR {error=-EOPNOTSUPP, "Unknown device type"})
    

    Kernel rejects veth device creation outright.

    Revised understanding

    My original issue conflated two separate problems. The actual situation is:

    Layer What fails Why Fix
    Gateway (Podman driver) Custom bridge network netavark needs veth; kernel lacks veth.ko Use default network (pasta/slirp4netns) instead of custom bridge
    Supervisor (inner namespace) veth pair setup Same: no veth.ko Skip veth setup; use existing container network directly

    The Podman driver's rootless handling (tmpfs at /run/netns, proper caps, seccomp_profile_path: "unconfined") is correct and works. The gap is the hard requirement on a custom bridge network and veth pairs.

    What I think is needed

    1. Podman driver: Option to use the default podman network (pasta/slirp4netns) instead of creating a custom bridge network. The host.containers.internal alias works on both.
    2. Supervisor: Option to skip inner veth pair creation and use the container's existing network interface directly when veth is unavailable.

    Happy to test any changes on this hardware. Let me know if additional diagnostics would be helpful.

  7. crdion commented on May 8, 2026

    @crdion
    Author

    Update: resolved — this is a platform issue, not an OpenShell issue

    After further investigation, I believe this can be closed. The root cause is a missing kernel module on the platform, not a gap in OpenShell.

    What we found

    1. The veth.ko module is shipped in the linux-modules-6.11.0-1016-nvidia package (the exact kernel we're running) — it's just not installed on the device.
    2. After sudo insmod veth.ko + sudo modprobe ip_tables iptable_nat, OpenShell's Podman driver works perfectly: custom bridge network creates, container starts on the openshell bridge, supervisor connects via gRPC.
    3. Newer NVIDIA kernel series (6.14.x, 6.17.x) also include veth.ko.

    This is the same situation as #692 (DRIVE AGX Thor / CONFIG_VETH), which was also resolved by adding the module to the platform.

    The fix (for our platform)

    We need to ensure linux-modules-6.11.0-1016-nvidia (or equivalent) is installed in our Host OS image, which will place veth.ko in /lib/modules/ and allow depmod/modprobe to load it. This is a platform image build change, not an OpenShell change.

    Recommendation

    Feel free to close this issue. The original feature request (skip inner network namespace creation) is not needed — once veth is available, both the outer bridge networking and the supervisor's inner namespace work correctly.

    Thank you @maxamillion and @cgwalters for steering us in the right direction. The diagnostics you requested made the root cause obvious.

  8. maxamillion commented on May 8, 2026

    @maxamillion
    Collaborator

    Awesome, glad you were able to resolve it! 🎉

  9. removed
    state:triage-neededOpened without agent diagnostics and needs triage
    on Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

os:linuxIssue affects Linux hosts

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions