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.
Restrict sandbox syscalls with seccomp
Section titled “Restrict sandbox syscalls with seccomp”Sandbox containers run under the container runtime’s default seccomp profile. It blocks the syscalls
a container has no ordinary reason to make, including clone with CLONE_NEWUSER — the step that
opens a user namespace, and the start of most kernel privilege-escalation chains.
Kubernetes applies no profile of its own unless the kubelet is started with seccompDefault, which
managed distributions generally do not. Dreadnode sets it on every sandbox pod instead of relying on
the cluster to do it.
The filter covers the sandbox container. The init container that installs the sandbox agent is unaffected, and this setting is independent of a RuntimeClass — a profile restricts which syscalls reach the kernel, while a RuntimeClass changes which kernel serves them.
Agent tooling that needs a blocked syscall can turn the filter off. There is no dedicated Admin Console field, so both Embedded Cluster paths use Advanced Helm Values, which overrides the dedicated fields:
dreadnode-sandbox-server: sandboxSeccompProfile: UnconfinedOpen 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: sandboxSeccompProfile: UnconfinedThis applies to every sandbox the deployment creates, not to one runtime, so it is better used to identify a syscall a workload needs than left in place.
A RuntimeClass and a seccomp profile both constrain a sandbox from the inside. To constrain what it can reach on the network, see Restrict sandbox egress.
Restrict sandbox egress
Section titled “Restrict sandbox egress”Sandbox pods reach whatever the cluster network allows. Turn on egress enforcement and the sandbox server adds a filtering sidecar to every sandbox pod it creates:
global: sandbox: egressEnabled: trueOpen 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: | global: sandbox: egressEnabled: trueOne value switches both halves. The sandbox server injects the sidecar from it, and the API reads the same value to learn whether a policy it composes will actually be enforced. They cannot be set independently, because a server that filters while the API sends no policy fails every sandbox at admission, and an API that assumes enforcement the server is not doing reports containment that does not exist.
This is the OpenSandbox path. On E2B, enforcement rides the sandbox create call and there is nothing to enable — the switch above has no effect, and the rest of this section’s chart values do not apply.
The sidecar shares the sandbox pod’s network namespace and filters with nftables inside it, so
enforcement does not depend on your CNI implementing NetworkPolicy. It runs in a different container
from the one agent code runs in, and the sandbox container is provisioned without NET_ADMIN or
NET_RAW — a process that escalates to root inside the sandbox can neither flush the rules nor
mark its own packets to skip them.
Raw sockets in the sandbox
Section titled “Raw sockets in the sandbox”Dropping NET_RAW costs the sandbox its raw-socket tooling. ping stops working, nmap’s SYN and
UDP scans are unavailable, and an nmap run that does not name a scan type falls back to a TCP
connect scan rather than failing — slower, and visible to the target as a completed connection
rather than a half-open one.
The capability is dropped because on its own it defeats the filter. The sidecar exempts its own
forwarded DNS by marking those packets, and from Linux 5.17 NET_RAW is enough to set that mark —
so a sandbox that keeps it marks its own traffic and skips every rule, not one destination. That is
a wider hole than any single allowed host, which is why the trade lands this way by default.
Restore raw sockets where the tooling matters more than the filter. There is no dedicated Admin Console field, so both Embedded Cluster paths use Advanced Helm Values:
dreadnode-sandbox-server: sandboxDropCapabilities: []Open 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: sandboxDropCapabilities: []NET_ADMIN is dropped whatever you set here, so the rules stay out of reach either way. The setting
applies to every sandbox the deployment creates, and it reopens the bypass for all of them — an
air-gapped deployment is the worst place to leave it cleared.
The floor you set
Section titled “The floor you set”Every sandbox gets a floor, and global.airgapped alone decides which one. The floor has no setting
of its own: global.airgapped already states whether the deployment has public egress, and a second
switch could only disagree with the first.
global.airgapped | Default action | What the floor denies |
|---|---|---|
true | Deny | Everything not named — a literal-address connection included |
false | Allow | The cloud metadata address 169.254.169.254 |
The two are not symmetric, and the asymmetry is worth understanding before you rely on the connected floor. Deny-by-default refuses a connection to a literal address for free, because such a destination is in no allow list — an air-gapped floor blocks your data stores, the Kubernetes API server and every other pod without the platform knowing a single address. Allow-by-default refuses nothing unless a rule matches it, so each protected destination has to be denied by address, and today only the metadata address is. A connected deployment’s floor is not a substitute for NetworkPolicy on your data stores — see Network trust boundaries for the inbound half.
Nothing above the floor can widen it. A task or a runtime can declare a narrower scope for itself, and a target your floor denies fails that run with the target named, before the sandbox starts — never dropped from the policy and run without. See Scoping what a sandbox can reach.
Add your own destinations
Section titled “Add your own destinations”Most enclave hosts are already reachable, because the platform reads the URL-valued entries you set
in SANDBOX_EXTRA_ENV — UV_INDEX_URL, GOPROXY, HF_ENDPOINT, proxies — and allows those hosts.
Add anything they do not name:
global: sandbox: egressExtraAllow: - artifacts.corp.internal - '*.mirror.corp.internal' - registry.corp.internal # a private image registry compose tasks pull fromEach entry is a fully-qualified name, a *.suffix wildcard, an IP, or a CIDR. Name an in-cluster
service as <service>.<namespace>.svc.cluster.local: a single-label name cannot be matched by the
sidecar and is ignored, and the same rule applies to the hosts read from SANDBOX_EXTRA_ENV. The
platform derives the public container registries for a compose task but not a private one, so a
registry your mirror serves belongs here. egressExtraAllow is additive: it cannot override a floor
deny, and an entry containing a protected destination is rejected rather than quietly widening the
floor.
The platform’s own in-cluster URLs are held to the same rule. If you override the LiteLLM or MinIO endpoint with a single-label service name, an air-gapped deployment fails preflight naming the setting, and a connected one logs a warning at boot, because every sandbox that declared a scope would otherwise lose the gateway.
The sidecar is also unsupported inside a pod that already carries a transparent service-mesh sidecar, since both rewrite outbound traffic in the same namespace. Nothing detects that at render, so it is a prerequisite you own.
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”Set global.caBundle. Sandbox runtimes receive the same bundle as the API and the bundled LiteLLM
proxy, so a private CA needs no custom image:
global: caBundle: enabled: true source: secret name: corporate-ca key: ca.crtOpen Config → Outbound TLS trust and supply the same bundle. See Outbound TLS trust for creating the secret.
A sandbox cannot mount a cluster secret — no sandbox provider exposes a secret volume backend — so the API passes the bundle in bounded, numbered environment chunks. This avoids Linux’s per-value process environment limit for combined public/private bundles. A CA certificate is a public key, so this discloses nothing a mounted file would not.
The runtime rejoins the chunks, merges your bundle with the image’s own system roots, and writes the
result to /tmp/dreadnode-ca-bundle.pem, so public TLS keeps working.
Every trust mechanism in the image is pointed at that file from the sandbox’s own environment, not
by the runtime process: SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, GIT_SSL_CAINFO,
NODE_EXTRA_CA_CERTS, AWS_CA_BUNDLE and GRPC_DEFAULT_SSL_ROOTS_FILE_PATH, plus UV_NATIVE_TLS
— uv uses its own vendored roots unless told otherwise, and so would keep failing behind an
intercepting proxy with everything else configured correctly. Declaring them in the container is
what makes curl, wget and an agent’s own scripts inherit your CA, rather than only the processes
the runtime happens to start. The image ships that path pre-populated with its stock public roots,
so the variables point at a usable bundle from the moment the sandbox starts.
Verify from a running sandbox:
Start a runtime, then find its pod — the OpenSandbox controller names sandbox pods for the sandbox ID, so they are the ones that carry no release name — and check trust from inside it:
kubectl get pods -n $NAMESPACEkubectl exec -n $NAMESPACE <sandbox-pod> -c sandbox -- \ python3 -c "import ssl; print(len(ssl.create_default_context().get_ca_certs()))"kubectl exec -n $NAMESPACE <sandbox-pod> -c sandbox -- \ curl -sSf https://your-internal-service.acme.internal >/dev/null && echo OKThe count should exceed the image’s stock root count by the number of certificates in your bundle.
Run the same two commands with bash -lc in front of them and the result must not change: the
trust comes from the container environment, so it does not depend on which shell reaches it.
Build a wrapper image
Section titled “Build a wrapper image”Only needed when the runtime needs more than trust — extra tooling, a preinstalled dataset, an
internal package your capabilities import. global.caBundle covers certificates on its own, and a
wrapper image means you own upgrades: the runtime image otherwise tracks the chart’s appVersion
automatically, and pinning it stops that, so the runtime SDK will drift from the API unless you
rebuild on each release.
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 rootRUN apt-get update && apt-get install -y --no-install-recommends your-tooling && \ rm -rf /var/lib/apt/lists/*USER userDo not install certificates here. global.caBundle reaches the runtime at boot and applies to
every sandbox without rebuilding anything, and a wrapper that also carries a CA gives you two
sources of trust to keep in step.
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.