---
title: "Configure remote sandboxes"
description: "Review remote sandbox egress and lifetime limits before enabling a service."
canonical_url: "https://docs.oz0.ai/fleet/sandbox-tier-2/remote"
---
# Configure remote sandboxes

> Review remote sandbox egress and lifetime limits before enabling a service.

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.

<warning>

Remote sandboxes have automated coverage against an API stand-in. E2B Cloud and self-hosted E2B have not been validated; see [integration maturity](https://docs.oz0.ai/reference/compatibility#integrations-awaiting-live-validation).

</warning>

A remote sandbox is not a local one. Know what changes before you allow it:

<table>
<thead>
  <tr>
    <th>
      
    </th>
    
    <th>
      A container on the node
    </th>
    
    <th>
      A remote sandbox
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Where the code and data are
    </td>
    
    <td>
      On the node
    </td>
    
    <td>
      At the service: the tool's image is a template there, and every call's arguments and results go there
    </td>
  </tr>
  
  <tr>
    <td>
      Files
    </td>
    
    <td>
      The tool's own container
    </td>
    
    <td>
      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
    </td>
  </tr>
  
  <tr>
    <td>
      Network
    </td>
    
    <td>
      None, or the node's egress proxy with the tenant's and the plugin's list
    </td>
    
    <td>
      The service's own rules, which are weaker (below)
    </td>
  </tr>
  
  <tr>
    <td>
      Secrets
    </td>
    
    <td>
      By name, from the tenant
    </td>
    
    <td>
      None: a plugin that declares secrets or connections runs no tool remotely
    </td>
  </tr>
  
  <tr>
    <td>
      Who decides
    </td>
    
    <td>
      The plugin's install
    </td>
    
    <td>
      The node's operator configures the service; the tenant's admins allow it
    </td>
  </tr>
</tbody>
</table>

### 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):

```bash
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:

```bash
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:

```json
{"ghcr.io/acme/fetch@sha256:4c2a...": "acme-fetch"}
```

Then start the node with the backend:

```bash
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:

```text
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:

```bash
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](https://docs.oz0.ai/fleet/sandbox-tier-2/gvisor#a-tenant-that-requires-it): 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

```yaml
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:

```text
+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:

<table>
<thead>
  <tr>
    <th>
      The node's entry
    </th>
    
    <th>
      In the remote sandbox
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      No entries
    </td>
    
    <td>
      No network at all
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        api.example.com
      </code>
      
      , <code>
        *.example.com
      </code>
    </td>
    
    <td>
      Allowed by name on ports 80 and 443, as on the node
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        api.example.com:443
      </code>
    </td>
    
    <td>
      Allowed by name on both 80 and 443
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        db.example.com:5432
      </code>
      
      , <code>
        host:*
      </code>
      
      , <code>
        host:22/tcp
      </code>
    </td>
    
    <td>
      Left out: E2B filters names only on ports 80 and 443
    </td>
  </tr>
  
  <tr>
    <td>
      An IP address
    </td>
    
    <td>
      Left out: E2B would allow every port of it
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        !uploads.example.com
      </code>
      
       inside an allowed <code>
        *.example.com
      </code>
    </td>
    
    <td>
      Refused: E2B's denies take addresses only
    </td>
  </tr>
</tbody>
</table>

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](https://docs.oz0.ai/reference/compatibility#integrations-awaiting-live-validation)). 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>`.

<span id="set-up-a-node-1">

Earlier section link

</span>


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
