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.
| Runtime | Workload location | Network requirement | Task evaluations |
|---|---|---|---|
| OpenSandbox | Your cluster | Wildcard DNS to the cluster ingress | Not supported |
| E2B | E2B cloud | Outbound access and public callbacks | Supported |
Use OpenSandbox
Section titled “Use OpenSandbox”OpenSandbox is the default and runs through the bundled sandbox controller and server:
dreadnode-api: config: sandboxProvider: opensandboxOpen Config → Sandbox Runtime and set Sandbox Provider to OpenSandbox (on-cluster, default).
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: sandbox_provider: value: opensandboxNo 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-qemuOpen Config → Advanced Helm Values → Helm Values, paste the Helm block from the preceding tab, save the configuration, and deploy.
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: advanced_helm_values: value: | dreadnode-sandbox-server: secureRuntime: type: kata runtimeClassName: kata-qemutype | Isolation | Nested containers |
|---|---|---|
gvisor | User-space kernel; no direct host syscalls | No |
kata | Full guest kernel in a VM (QEMU) | Yes |
firecracker | Guest kernel in a Firecracker microVM | Yes |
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:
kubectl get runtimeclassThe 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.
Install an isolation runtime
Section titled “Install an isolation runtime”Managed Kubernetes can do the install for you, and each provider offers one mechanism:
| Platform | Mechanism | Enable it | Resulting class |
|---|---|---|---|
| GKE | gVisor | GKE Sandbox — --sandbox type=gvisor on a node pool using the cos_containerd image | gvisor |
| AKS | Kata | Pod Sandboxing — --workload-runtime KataVmIsolation | kata-vm-isolation |
| EKS, k0s, bare metal | either | Install it yourself, below | You 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 isrunsc.
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.
Check the hardware requirement first
Section titled “Check the hardware requirement first”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, andR8i-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 (m7idis not on it, for instance). Nested virtualization is off by default: setNestedVirtualization=enabledin 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:
kubectl debug node/<node> -it --image=busybox -- ls -l /host/dev/kvmTo 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.
Route model calls from OpenSandbox
Section titled “Route model calls from OpenSandbox”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/v1Open Config → Advanced Helm Values → Helm Values, paste the Helm block from the preceding tab, save the configuration, and deploy.
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: advanced_helm_values: value: | dreadnode-api: config: opensandbox: litellmUrl: https://litellm.sandbox.example/v1Use 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.
Route browsers to OpenSandbox runtimes
Section titled “Route browsers to OpenSandbox runtimes”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-tlsOpen Config → Sandbox Runtime. Set Sandbox Wildcard Domain to
sandbox.dreadnode.example.com. For HTTPS, set Sandbox TLS Certificate Secret Name to a
Secret whose certificate covers *.sandbox.dreadnode.example.com.
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: scheme: value: https tls_secret_name: value: dreadnode-tls sandbox_domain: value: sandbox.dreadnode.example.com sandbox_tls_secret_name: value: dreadnode-sandbox-tlsCreate 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.
Size OpenSandbox pods
Section titled “Size OpenSandbox pods”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:
| Node | Free after platform | Sandboxes by CPU | Sandboxes by memory | Effective 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: 512Open Config → Sandbox Runtime, enable Set OpenSandbox pod requests, then set the CPU and memory requests.
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: sandbox_requests_customize: value: "1" sandbox_cpu_request_cores: value: "0.25" sandbox_memory_request_mb: value: "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.
Trust a private CA in OpenSandbox
Section titled “Trust a private CA in OpenSandbox”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.
Read the runtime image from your cluster
Section titled “Read the runtime image from your cluster”The runtime image reference is per-install, so read the authoritative value out of the API ConfigMap rather than composing one by hand:
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.
Authenticate the build host
Section titled “Authenticate the build host”Your cluster already holds the credential the build host needs, in enterprise-pull-secret:
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-stdinAn 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.
Build the wrapper image
Section titled “Build the wrapper image”ARG RUNTIME_IMAGEFROM ${RUNTIME_IMAGE}
USER rootCOPY 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 userupdate-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:
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"Point the API at the published image
Section titled “Point the API at the published 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>Open Config → Advanced Helm Values → Helm Values and paste the Helm block from the preceding tab.
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: advanced_helm_values: value: | 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:
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-registryEnter the Embedded Cluster shell and create the credential in kotsadm:
sudo ./dreadnode shellexport NAMESPACE=kotsadmkubectl -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 -Open Config → Advanced Helm Values → Helm Values and paste:
global: imagePullSecrets: - acme-registrySelect Save config, run the preflight checks, then select Deploy.
A ConfigValues file cannot create the referenced Kubernetes Secret. Complete the installation,
enter sudo ./dreadnode shell, create acme-registry in kotsadm, then apply the Helm block
from the Admin Console tab.
If separate automation creates the Secret before the application chart is deployed, put the
same block in advanced_helm_values:
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: advanced_helm_values: value: | global: imagePullSecrets: - acme-registrySkip 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:
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>"Know what the wrapper covers
Section titled “Know what the wrapper covers”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.
Use E2B
Section titled “Use E2B”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:
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: falsedreadnode-sandbox-server: enabled: falseOpen Config → Sandbox Runtime. Set Sandbox Provider to E2B cloud sandboxes and enter E2B API Key. The Admin Console disables the OpenSandbox components.
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: sandbox_provider: value: e2b e2b_api_key: valuePlaintext: '<your-e2b-key>'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:
kubectl -n "$NAMESPACE" logs deploy/dreadnode-apiConfigure public callbacks
Section titled “Configure public callbacks”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/v1Set Config → Sandbox Runtime → Sandbox Callback URL. When the bundled inference proxy is enabled, also set Config → Inference Proxy → LiteLLM Public URL.
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: e2b_server_url: value: https://dreadnode-bastion.example.com litellm_public_url: value: https://dreadnode-bastion.example.com/llm/v1Leave 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.
Runtime limits
Section titled “Runtime limits”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: 28800Open Config → Sandbox Runtime, enable Set custom session time limits, then set Default Session Length (seconds) and Maximum Session Length (seconds).
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: sandbox_limits_customize: value: "1" sandbox_default_runtime_seconds: value: "3600" sandbox_max_runtime_seconds: value: "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.
Template aliases
Section titled “Template aliases”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-prodOpen Config → Sandbox Runtime and set E2B Task Template and E2B Runtime Template.
apiVersion: kots.io/v1beta1kind: ConfigValuesspec: values: e2b_task_template_alias: value: brian-3740/dn-task-prod e2b_runtime_template_alias: value: brian-3740/dn-dreadnode-prodOverride 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.