Skip to content

Sandbox runtime

Choose between on-cluster OpenSandbox and E2B cloud sandboxes, route browsers to runtimes, and size sandbox pods.

The sandbox runtime is chart configuration, applied by a redeploy. Configure it in a Helm overlay, under Sandbox Runtime in the Admin Console, or with matching KOTS ConfigValues items.

Commands on this page use $NAMESPACE. For Helm, set it to your release namespace. For Embedded Cluster, enter sudo ./dreadnode shell, then set it to kotsadm.

Dreadnode runs agent code in isolated sandboxes. Choose the runtime based on where workloads may execute and whether you need task-based evaluations.

RuntimeWorkload locationNetwork requirementTask evaluations
OpenSandboxYour clusterWildcard DNS to the cluster ingressNot supported
E2BE2B cloudOutbound access and public callbacksSupported

OpenSandbox is the default and runs through the bundled sandbox controller and server:

dreadnode-api:
config:
sandboxProvider: opensandbox

No manually supplied credential or callback configuration is required. The chart generates a shared API key for the platform API and OpenSandbox server; rotate it later from Secret rotation. OpenSandbox is the appropriate choice for air-gapped installs and environments where workload data cannot leave the cluster — the wildcard DNS record below resolves inside your network, so nothing about it requires public DNS.

Sandbox pods run in the release namespace. The chart can pin them elsewhere with dreadnode-sandbox-server.kubernetes.namespace, but that is unsupported on Embedded Cluster and KOTS installs: the license-bound image pull secret exists only in the release namespace, so pods scheduled outside it cannot pull their runtime image.

The first runtime on a node can take longer while Kubernetes fills its image cache. Dreadnode waits up to three minutes for the sandbox pod to become ready by default, which accommodates a cold pull of the runtime image through the Replicated proxy. Later runtimes normally start much faster.

Isolate OpenSandbox pods with a RuntimeClass

Section titled “Isolate OpenSandbox pods with a RuntimeClass”

By default, sandbox pods run on the same container runtime as everything else on the node and share the host kernel with it. Agent code that escapes its container reaches the node. To put a stronger boundary around it, name a Kubernetes RuntimeClass backed by an isolation runtime, and Dreadnode applies it to every sandbox pod it creates.

type names the isolation mechanism and runtimeClassName is the exact class installed on your cluster. Both are required together. Dreadnode never derives the class name from the type, because a RuntimeClass can be called anything. There is no dedicated Admin Console field, so both Embedded Cluster paths use Advanced Helm Values.

dreadnode-sandbox-server:
secureRuntime:
type: kata # gvisor, kata, or firecracker
runtimeClassName: kata-qemu
typeIsolationNested containers
gvisorUser-space kernel; no direct host syscallsNo
kataFull guest kernel in a VM (QEMU)Yes
firecrackerGuest kernel in a Firecracker microVMYes

The chart does not install the isolation runtime. A cluster administrator installs gVisor or Kata on the nodes and creates the RuntimeClass first. Confirm the name before you deploy:

Terminal window
kubectl get runtimeclass

The OpenSandbox server checks the RuntimeClass exists when it starts and refuses to start when it does not, so a typo fails the deployment instead of quietly running every sandbox on the host kernel.

Managed Kubernetes can do the install for you, and each provider offers one mechanism:

PlatformMechanismEnable itResulting class
GKEgVisorGKE Sandbox--sandbox type=gvisor on a node pool using the cos_containerd imagegvisor
AKSKataPod Sandboxing--workload-runtime KataVmIsolationkata-vm-isolation
EKS, k0s, bare metaleitherInstall it yourself, belowYou choose the name

GKE Sandbox applies to a whole node pool, so every pod on those nodes is sandboxed, and it cannot be enabled on a Standard cluster’s default node pool. AKS Pod Sandboxing needs Kubernetes 1.27 or later, the AzureLinux OS SKU, and a generation 2 VM size that supports nested virtualization.

Everywhere else, install the runtime on the nodes yourself:

  • Kata Containers — the kata-deploy Helm chart lays the binaries down on each node and creates the RuntimeClass objects for you.
  • gVisor — follow the containerd quick start to install runsc, then create a RuntimeClass whose handler is runsc.

Both are node-level installs that need root on every node that will run sandboxes, and both must be reapplied when you replace or upgrade node images.

gVisor runs on any node. Kata and Firecracker need hardware virtualization/dev/kvm — on every node that runs sandboxes, and this is where most Kata deployments stall:

  • Bare metal gives you KVM natively. It is also what AWS recommends when the workload is performance-sensitive or latency-sensitive.
  • AWS EC2 supports nested virtualization on virtual instances, currently C7i, M7i, R7i, I7i, C7i-flex, M7i-flex, C8i, M8i, R8i, X8i, C8id, M8id, R8id, C8i-flex, M8i-flex, and R8i-flex. Check that list against AWS’s own page before you size a node group — it grows, and sibling types you might expect are absent (m7id is not on it, for instance). Nested virtualization is off by default: set NestedVirtualization=enabled in the instance CPU options, which for EKS means the managed node group’s launch template. An existing instance must be stopped to change it.
  • Azure requires a generation 2 VM size that supports nested virtualization, such as Dsv3.

Confirm a node can actually do it before you commit to Kata:

Terminal window
kubectl debug node/<node> -it --image=busybox -- ls -l /host/dev/kvm

To keep the isolation runtime on dedicated nodes, put scheduling.nodeSelector and scheduling.tolerations on the RuntimeClass itself. Kubernetes applies them to every pod that selects the class. Dreadnode does not set pod placement for sandboxes.

With the bundled inference proxy enabled, OpenSandbox runtimes receive DREADNODE_LLM_BASE=http://<release>-litellm:4000/v1. This in-cluster route does not depend on the platform’s public ingress accepting traffic from its own pods. E2B runtimes and external clients continue to use the configured LiteLLM public URL.

Leave the OpenSandbox URL empty for a bundled, on-cluster deployment. Set it when OpenSandbox runs in another cluster or your runtime pods need a custom LiteLLM route. The value may include /v1; the API normalizes the injected base to one /v1 suffix. There is no dedicated Admin Console field, so both Embedded Cluster paths use Advanced Helm Values.

dreadnode-api:
config:
opensandbox:
litellmUrl: https://litellm.sandbox.example/v1

Use a URL reachable from the runtime pods. Pointing a remote OpenSandbox runtime at an in-cluster Service name from the Dreadnode cluster will fail DNS resolution.

Point *.sandbox.dreadnode.example.com at the same ingress endpoint as the platform, then configure the base authority without *.. If you leave it empty, Dreadnode derives sandbox.<platform-domain>.

global:
domain: dreadnode.example.com
scheme: https
tls:
secretName: dreadnode-tls
sandbox:
domain: sandbox.dreadnode.example.com
tls:
secretName: dreadnode-sandbox-tls

Create the TLS Secret in the Dreadnode namespace before deployment. Its certificate needs the single wildcard SAN *.sandbox.dreadnode.example.com; each runtime uses one label beneath that authority, such as https://<sandbox-id>-8787.sandbox.dreadnode.example.com. You may reuse the platform TLS Secret when its certificate already includes this SAN.

The gateway routes stable Host names without signing them. Dreadnode protects runtime HTTP endpoints with the runtime bearer token and browser WebSocket handshakes with short-lived, single-use tickets. The OpenSandbox lifecycle API stays on its authenticated ClusterIP Service and is not exposed by the wildcard Ingress.

Each sandbox pod requests 0.25 CPU and 512 MiB by default. The runtime’s configured 2 CPU and 2 GiB remain pod limits, so idle interactive sessions reserve modest capacity but can burst when they are active. These dynamic pod requests are separate from the Resource Preset, which sizes the always-on platform components.

Requests decide how many sandboxes fit on a node. The small preset reserves about 1.76 CPU and 5.1 GiB for always-on components, so subtract that and the kubelet’s own reservation from node allocatable, then divide by the per-sandbox request:

NodeFree after platformSandboxes by CPUSandboxes by memoryEffective limit
8 vCPU / 16 GiB~5.7 CPU, ~8.9 GiB~22~17~17 (memory)
4 vCPU / 32 GiB~1.8 CPU, ~25 GiB~7~50~7 (CPU)

Which resource binds depends on the node’s CPU-to-memory ratio, so check both before assuming a larger box helps.

CPU and memory requests are not equally safe to lower. CPU is compressible: a pod above its request is throttled in proportion to that request, and nothing is killed. Memory is not. A pod above its memory request is a candidate for eviction under node memory pressure, and a pod at its memory limit is OOM-killed. Lower the CPU request to raise density; leave the memory request alone unless you have measured the workload.

dreadnode-api:
config:
opensandbox:
resources:
requests:
cpuCores: 0.25
memoryMb: 512

Raise requests when measurements show a sandbox needs guaranteed capacity. Lower them only when the node is constrained and the workload can tolerate contention. If provisioning times out, see Runtime provisioning returns a scheduling timeout.

Build a wrapper image on the runtime image your cluster already pulls, install the organization’s root certificate into it, and point the API at the result.

The runtime image reference is per-install, so read the authoritative value out of the API ConfigMap rather than composing one by hand:

Terminal window
export NAMESPACE=dreadnode # kotsadm for Embedded Cluster
RUNTIME_IMAGE=$(kubectl -n "$NAMESPACE" get cm \
-l app.kubernetes.io/name=dreadnode-api \
-o jsonpath='{.items[0].data.DOCKER_RUNTIME_IMAGE}')
echo "$RUNTIME_IMAGE"
# proxy.enterprise.dreadnode.io/proxy/dreadnode/index.docker.io/dreadnode/dreadnode:<version>

Self-hosted installs pull that image through proxy.enterprise.dreadnode.io, authenticated by your license. docker.io/dreadnode/dreadnode is not anonymously pullable — a build host that reaches for it gets a 401. Channels apply to charts and releases, not to images: there is no beta or unstable path for an image reference the way there is for oci://registry.replicated.com/dreadnode/beta/dreadnode.

Your cluster already holds the credential the build host needs, in enterprise-pull-secret:

Terminal window
REGISTRY="${RUNTIME_IMAGE%%/*}"
AUTH=$(kubectl -n "$NAMESPACE" get secret enterprise-pull-secret \
-o jsonpath='{.data.\.dockerconfigjson}' | base64 -d |
jq -r --arg registry "$REGISTRY" '.auths[$registry].auth' | base64 -d)
printf '%s' "${AUTH#*:}" |
docker login "$REGISTRY" --username "${AUTH%%:*}" --password-stdin

An air-gapped install that mirrors images into its own registry has no enterprise-pull-secret. Build from your mirror’s copy of the runtime image and authenticate to that registry instead.

ARG RUNTIME_IMAGE
FROM ${RUNTIME_IMAGE}
USER root
COPY organization-root.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates
# Node, Bun, and the Claude Code CLI carry their own CA list and ignore the OS store.
ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/organization-root.crt
USER user

update-ca-certificates adds the organization root to the image’s combined system bundle while retaining normal public roots. Copy one certificate per .crt file.

NODE_EXTRA_CA_CERTS appends to the CA list the JavaScript runtimes ship with rather than replacing it, so a single-root file is what you want here — unlike the combined bundle SSL_CERT_FILE needs in Outbound TLS trust. The image contains no private key.

Build and publish to a registry the OpenSandbox nodes can pull. Carry the Dreadnode version through to the wrapper’s tag so the two never drift:

Terminal window
VERSION="${RUNTIME_IMAGE##*:}"
WRAPPER_IMAGE="registry.acme.internal/dreadnode/runtime-private-ca:$VERSION"
docker build \
--build-arg RUNTIME_IMAGE="$RUNTIME_IMAGE" \
-t "$WRAPPER_IMAGE" \
.
docker push "$WRAPPER_IMAGE"

Substitute the tag you just pushed for <version>. No dedicated Admin Console field sets dockerRuntimeImage, so both Embedded Cluster paths route through Advanced Helm Values, which Dreadnode merges after the dedicated fields and which therefore overrides them.

dreadnode-api:
config:
dockerRuntimeImage: registry.acme.internal/dreadnode/runtime-private-ca:<version>

Pinning dockerRuntimeImage opts the runtime out of automatic version tracking. Without an override, each release ships the matching runtime image and an upgrade moves it for you; with one, the runtime stays where you put it. Rebuild and repoint the wrapper on every upgrade so the API and runtime SDK stay on the same release commit.

Give sandbox pods a credential for your registry

Section titled “Give sandbox pods a credential for your registry”

OpenSandbox creates sandbox pods dynamically, and they carry only the pull secrets the BatchSandbox template injects — by default just enterprise-pull-secret, which authenticates to the Dreadnode proxy and nothing else. A dockerRuntimeImage on your own registry needs your own credential in global.imagePullSecrets. No dedicated Admin Console field sets it either, so both Embedded Cluster paths route through Advanced Helm Values again.

Create the registry credential in the Dreadnode namespace:

Terminal window
kubectl -n "$NAMESPACE" create secret docker-registry acme-registry \
--docker-server=registry.acme.internal \
--docker-username=<username> \
--docker-password=<password> \
--dry-run=client -o yaml | kubectl apply -f -

Then reference it in your values overlay:

global:
imagePullSecrets:
- acme-registry

Skip this and sandbox pods land in ImagePullBackOff while the rest of the platform stays healthy. See Pods in ImagePullBackOff.

Every sandbox pod also runs a second image, opensandbox-execd, which the sandbox server injects and which the wrapper does not replace. A mirroring install needs both:

Terminal window
kubectl -n "$NAMESPACE" get cm \
-l app.kubernetes.io/name=dreadnode-sandbox-server \
-o jsonpath='{.items[0].data.config\.toml}' | grep execd_image
# execd_image = "proxy.enterprise.dreadnode.io/proxy/dreadnode/index.docker.io/dreadnode/opensandbox-execd:<version>"

The Dreadnode runtime’s Python paths — platform API, model gateway, and trace exporter — read the image’s OS trust store, which update-ca-certificates updates. The runtime image also ships Node 22, Bun, and the Claude Code CLI for JavaScript MCP servers and the claude-code agent engine. Those maintain their own CA list and ignore the OS store entirely, which is why the recipe sets NODE_EXTRA_CA_CERTS. Behind a TLS-inspecting proxy, an image built without it passes Python traffic and fails every JavaScript path.

CA rotation requires a new image build and tag, a chart upgrade, and recreation of affected runtime sandboxes. This recipe does not configure arbitrary task images, E2B templates, dynamic CA injection, or provider-mounted CA files. See Trust an internal certificate for the host TUI, CLI, and SDK setup. Use Outbound TLS trust separately for the platform API and bundled LiteLLM pods.

E2B requires outbound access to api.e2b.dev and *.e2b.app. Create an API key in your E2B team and enable custom template builds for the account.

Configure E2B and its API key:

Store the API key in the Dreadnode namespace:

Terminal window
kubectl -n "$NAMESPACE" create secret generic e2b-credentials \
--from-literal=E2B_API_KEY='<your-e2b-key>'

Then select E2B and reference the Secret:

dreadnode-api:
config:
sandboxProvider: e2b
e2b:
apiKey:
existingSecret: e2b-credentials
secretKey: E2B_API_KEY
dreadnode-sandbox-controller:
enabled: false
dreadnode-sandbox-server:
enabled: false

The Embedded Cluster paths protect the key in stored configuration, then inject it as a plain environment variable on the API Deployment. Anyone who can read that Deployment can read the key. Use the Helm path with a pre-created Secret when that exposure is unacceptable. Disabling the on-cluster subcharts in Helm is optional, but reclaims their resources.

The API validates the E2B configuration at startup: it authenticates the key, checks the template aliases, logs any configuration errors, and gives up after five seconds so the API can finish starting. Check the API logs after switching providers:

Terminal window
kubectl -n "$NAMESPACE" logs deploy/dreadnode-api

E2B sandboxes and the E2B control plane call back into Dreadnode for:

  • agent runtime reporting
  • model inference through LiteLLM, when enabled
  • lifecycle webhooks at /api/v1/webhooks/e2b

These URLs must be reachable from E2B’s cloud. The defaults use the configured Dreadnode domain and work only when that domain is publicly reachable. For an internal-only deployment, expose a restricted tunnel, bastion, or reverse proxy and configure both callback URLs:

dreadnode-api:
config:
e2b:
serverUrl: https://dreadnode-bastion.example.com
litellm:
publicUrl: https://dreadnode-bastion.example.com/llm/v1

Leave serverUrl empty when the configured Dreadnode domain is already reachable from E2B. The API registers the lifecycle webhook beneath the same URL; it has no separate setting.

Evaluations without an explicit task timeout run for up to one hour by default. Explicit timeouts can run for up to the configured maximum, which defaults to eight hours:

dreadnode-api:
config:
sandboxRuntime:
defaultRuntimeSeconds: 3600
maxRuntimeSeconds: 28800

Set the default to at least 60 seconds and no higher than the maximum. The API rejects an invalid pair at startup. These values apply to both providers. E2B also enforces your account’s session limit. Raise the default if long unattended evaluations are being cut short.

Dreadnode extends published base templates for task and interactive runtime sandboxes:

dreadnode-api:
config:
e2b:
taskTemplateAlias: brian-3740/dn-task-prod
runtimeTemplateAlias: brian-3740/dn-dreadnode-prod

Override these aliases only when you maintain compatible forks of the base templates. The first evaluation for a task builds a team-local template and is slower than later runs, which reuse it. E2B bills runtime and template builds to your account, and Dreadnode does not automatically prune accumulated templates.