Configure remote sandboxes
For: cluster operators.
Review remote sandbox egress and lifetime limits before enabling a service.
Remote sandboxes
Some nodes have no local tier 2: Windows nodes, Intel Macs, hosts with rootless containers only, VMs without KVM. A tool with isolation: remote runs in a remote sandbox instead: a sandbox of an E2B-compatible service, which runs each one as a microVM with a kernel of its own, far from the node. One plugin, ai.oz0/sandbox-e2b, reaches E2B Cloud, a self-hosted E2B and CubeSandbox.
A remote sandbox is not a local one. Know what changes before you allow it:
| A container on the node | A remote sandbox | |
|---|---|---|
| Where the code and data are | On the node | At the service: the tool's image is a template there, and every call's arguments and results go there |
| Files | The tool's own container | No mount of the node's: files go in and out through the protocol, at most 4 MiB each; calls that hand the tool files are refused for now |
| Network | None, or the node's egress proxy with the tenant's and the plugin's list | The service's own rules, which are weaker (below) |
| Secrets | By name, from the tenant | None: a plugin that declares secrets or connections runs no tool remotely |
| Who decides | The plugin's install | The node's operator configures the service; the tenant's admins allow it |
Set up a node
The node's operator configures the service, never a tenant. Install the plugin on the node (it needs Python 3.13 and nothing else):
uv tool install ./plugins/sandbox-e2b # from a checkout of the repository
Put the service's API key in a file in the node's data directory (here the default on Linux, --data-dir changes it), which no sandboxed process can read, readable by the node's user only:
mkdir -p ~/.local/share/orchestrator-zero-node/secrets && install -m 600 /dev/null ~/.local/share/orchestrator-zero-node/secrets/e2b-key
$EDITOR ~/.local/share/orchestrator-zero-node/secrets/e2b-key
Build a template of the tool's image at the service, from the image pinned by digest (for E2B, a template whose Dockerfile is FROM <image>@sha256:...), and map the image to it:
{"ghcr.io/acme/fetch@sha256:4c2a...": "acme-fetch"}
Then start the node with the backend:
orchestrator-zero-node run --remote-sandbox \
"e2b=oz0-sandbox-e2b --api-key-file $HOME/.local/share/orchestrator-zero-node/secrets/e2b-key --templates /etc/oz0/e2b-templates.json"
For a self-hosted E2B or CubeSandbox, add --domain, --api-url (and --sandbox-url when one address routes to every sandbox) and --ca-file for a private CA. oz0-sandbox-e2b doctor with the same options checks the key. The node checks the backend when it starts, destroys the sandboxes it left behind, and reports it; oz0 node show <node> says:
Remote sandbox e2b (e2b 0.1.0), reaching api.e2b.app
Allow it for a tenant
Remote sandboxes are off for every tenant until one of its admins allows them, because the tools' code and data leave the node:
oz0 tenant remote-sandbox default allow
The audit log keeps each change (sandbox.remote_allowed, sandbox.remote_denied). oz0 tenant remote-sandbox default deny stops them: the tenant's nodes restart their runtimes, and every remote sandbox they had is destroyed.
A remote sandbox does not meet a tenant's gVisor requirement: in a job an outside event started, a tenant that requires gVisor gets a refusal for a remote tool's call, which says why, never the remote sandbox in gVisor's place. The service's egress rules are weaker than the node's proxy (below), and nothing of the node's sees the tool's traffic. Jobs a person started still run the tool remotely.
In a plugin
tools:
- id: fetch
package: oci:ghcr.io/acme/fetch@sha256:4c2a...
isolation: remote
plugin lint refuses isolation: remote for a tool that is not from an OCI image, and in a plugin that declares requires.secrets, requires.optional_secrets or a connection that hands the tool a token: no secret leaves the node. The agents that call the tool require isolation.remote=ready, which only nodes with a backend that answered get, and only for tenants that allowed remote sandboxes. Each job's trace says which steps ran remotely, in oz0 job trace and, as "Ran in: a remote sandbox of e2b", in the console's step inspector:
+380ms 31ms tool fetch.get node-5fb2c18ef4d5 (remote sandbox: e2b) ok
Egress is the service's, and weaker
The node never sends the service more than its own list allows; what the service cannot hold as the node would, it leaves out:
| The node's entry | In the remote sandbox |
|---|---|
| No entries | No network at all |
api.example.com, *.example.com | Allowed by name on ports 80 and 443, as on the node |
api.example.com:443 | Allowed by name on both 80 and 443 |
db.example.com:5432, host:*, host:22/tcp | Left out: E2B filters names only on ports 80 and 443 |
| An IP address | Left out: E2B would allow every port of it |
!uploads.example.com inside an allowed *.example.com | Refused: E2B's denies take addresses only |
Two things are weaker than the node's egress proxy, and you cannot change them: names are filtered only on ports 80 (the Host header) and 443 (the TLS server name), and when any name is allowed E2B allows its DNS resolver, 8.8.8.8, too, so DNS queries, and anything tunnelled through them, are not filtered. The plugin never adds a resolver of its own.
Do not treat these rules as an allowlist. A filter on the Host header and the TLS server name judges what the client itself says, and the sandbox has a resolver and its own route out, with no proxy of the node's in between: what a client names and where its traffic goes can differ, and DNS is open besides.
Assume a tool in a remote sandbox can reach hosts its list leaves out, and send it nothing it could not be allowed to pass on: no secret leaves the node, but call arguments and results do.
Remote sandboxes have still not run against the real service, so nothing here has been measured against E2B itself (integration maturity). A tenant that needs the node's rules keeps such tools on nodes with a local tier 2.
When sandboxes end
The node destroys a remote sandbox when its tool's server stops, when its runtime restarts and when the backend breaks the protocol. A cancelled call of a job started from outside (a webhook, a schedule) stops that job's server, so it destroys the server's sandbox too; a long-lived server keeps its sandbox through a cancelled call, as a local tool's server keeps its container, and the sandbox ends when the server or the runtime stops.
If the node itself dies, the sandbox ends after its timeout (10 minutes after the node was last heard from), and the node destroys what it left when it starts again. Every remote sandbox the node made carries the label oz0.node=<node ID>.
Earlier section link