Rune Box docs

Isolation and runtimes

Each sandbox reports the isolation tier it actually runs behind, proven by evidence from inside it, and can add in-sandbox confinement, network controls and an optional browser IDE.

Requiring a tier

A policy (Charter) can set a minimum tier. A sandbox (Citadel) that falls short is refused before anything runs in it.

[host]
type          = "container"   # or "k8s", or "microvm" (below)
runtime_class = "io.containerd.kata-qemu.v2"  # the OCI runtime (Docker/Podman --runtime, k8s RuntimeClass)

[isolation]
minimum = "vm"                # process < container < kernel < vm
Tier Boundary
process A host process with in-process confinement only.
container Namespaces and cgroups on the host kernel (runc, crun).
kernel A kernel that is not the host's: gVisor, or a VM guest kernel whose hardware virtualisation could not be confirmed.
vm A hardware-virtualised guest: Kata on QEMU, Cloud Hypervisor or Firecracker, or a native Firecracker or libkrun microVM.

Runtimes

runtime_class What it is At most
empty, runc, crun Engine default, shared kernel container
runsc (gvisor) gVisor kernel
kata, kata-qemu, kata-clh, io.containerd.kata[-<h>].v2 Kata Containers, QEMU or Cloud Hypervisor vm
kata-fc (Kubernetes only) Kata with Firecracker vm
krun crun with libkrun refused: no exec into a running guest; use the native libkrun driver

With host.type = "microvm" there is no OCI runtime: host.runtime names the monitor Rune Box boots itself, firecracker or libkrun (see Native microVMs). Both claim vm, confirmed from guest evidence like every other runtime.

runtime_class becomes docker run --runtime <name> / podman run --runtime <name>, or the Pod's runtimeClassName on Kubernetes. When it is empty, an OCI runtime in host.runtime is used. An unregistered runtime fails the create; Rune Box never falls back to the default runtime.

How the tier is decided

A runtime name is only a claim. Right after start, the backend runs a one-line POSIX probe (/bin/sh and cat) in the sandbox and compares it with the engine host's or node's kernel.

Evidence Tier
gVisor signatures kernel
Guest kernel differs from the host's and a hypervisor marker (CPU flag, DMI vendor, /sys/hypervisor/type, arm64 linux,dummy-virt, virtio-mmio) vm
Different kernel, no hypervisor marker kernel
Host's own kernel, or anything unconfirmed container
  • The result is capped at the runtime's "At most" tier. If the name claims more than the evidence shows, the weaker tier is reported with an ISOLATION DOWNGRADE warning.
  • The name does not identify the hypervisor: Kata 4's runtime-rs shim loads the first runtime-rs/configuration.toml it finds, so kata-clh or kata-fc can silently run QEMU. On a local Docker/Podman engine the backend finds the real hypervisor process and records host:hypervisor=... (plus host:hypervisor-mismatch=... and a warning). Remote engines and Kubernetes report unknown.
  • Evidence is stored on the sandbox (IsolationEvidence) and written to the signed audit trail on sandbox create, including refused creates:
isolation_signals = probe:ok,kernel:separate,hypervisor:cpu-flag,claim:vm,host:hypervisor=qemu,verdict:vm

Warning: The probe detects misconfiguration, not a hostile image: it trusts the image's /bin/sh. RUNEWARD_ISOLATION_PROBE=off skips it and trusts the runtime name (evidence records probe:disabled).

On Kubernetes the server and controller need get on nodes and runtimeclasses (granted by the Helm chart). Without it, VM runtimes report container.

Sizing VM sandboxes

Kata adds its own default_memory/default_vcpus to the container limits, so the defaults (RUNEWARD_SANDBOX_MEMORY=4g, RUNEWARD_SANDBOX_CPUS=2) become a 6144 MiB, 3 vCPU guest. If that does not fit, the engine only reports a shim timeout. On Docker or Podman the backend checks first:

guest memory = sandbox memory limit + RUNEWARD_VM_OVERHEAD_MEMORY  (default 2048m)
guest vCPUs  = ceil(sandbox CPU limit) + ceil(RUNEWARD_VM_OVERHEAD_CPUS)  (default 1)
  • Limits come from budgets ([rationing] memory / cpus), else the RUNEWARD_SANDBOX_* variables.
  • A guest larger than the engine host's total memory or CPUs is refused, naming what to lower. Set RUNEWARD_VM_OVERHEAD_* to match your runtime (0/off for none).
  • If the engine does not report capacity, nothing is refused. Passing the check does not guarantee a boot.
  • Kubernetes uses the RuntimeClass's overhead.podFixed and the scheduler instead.

Kata with Firecracker sizes the guest once from the Pod's limits plus overhead_memory / overhead_vcpus, and the guest kernel shares that memory, so the sandbox gets less than its limit. Set those overheads (for example overhead_memory = 256) in Kata's Firecracker configuration and match overhead.podFixed.

A known VM start failure (timed out waiting for QMP ready, start vm, failed to create shim task) gets an error naming likely causes: guest size, no usable /dev/kvm, or a broken /etc/kata-containers/runtime-rs/configuration.toml. A failed create leaves nothing behind.

Installing a runtime

runeward runtime install <gvisor|kata> [--engine docker|k8s] [--runtime-class NAME] [--version V] [--sha256 HEX] [--apply]

It prints the exact plan; nothing changes without --apply. Without root it verifies downloads into a temporary directory and prints the sudo commands. It does not check the host for VM support, so confirm the requirements yourself. VM runtimes need Linux, an openable /dev/kvm, nested virtualisation if the host is a VM, and no desktop engine (Docker Desktop, OrbStack and Podman machine cannot run kata*; gVisor works there).

Runtime Setup
gVisor Docker/Podman: installs runsc and containerd-shim-runsc-v1 into /usr/local/bin (verified against --sha256 or the release .sha512, fail closed) and, for Docker, adds runsc to /etc/docker/daemon.json; restart Docker. No KVM needed. Kubernetes: install on nodes, register the runsc handler in containerd, add a gvisor RuntimeClass with handler: runsc.
Kata Install Kata yourself (not automated). Docker: link the shim onto dockerd's PATH, as below, or let runtime install kata --apply register an existing containerd-shim-kata-v2 (or kata-runtime) in /etc/docker/daemon.json as runtime kata. Either way, point /etc/kata-containers/runtime-rs/configuration.toml at the hypervisor you want. Kubernetes: kata-deploy and a RuntimeClass with handler kata, kata-qemu or kata-clh.
kata-fc Kubernetes only, with containerd's devmapper snapshotter. Kata 4.2.0's configuration-rs-fc.toml is invalid: set dial_timeout_ms = 10 and reconnect_timeout_ms = 45000.
ln -sf /opt/kata/runtime-rs/bin/containerd-shim-kata-v2 /usr/local/bin/containerd-shim-kata-qemu-v2
docker run --runtime io.containerd.kata-qemu.v2 ...   # host.runtime_class = "io.containerd.kata-qemu.v2"

The Kata names differ because they name different things: containerd-shim-kata-v2 and kata-runtime are Kata binaries, kata is a Docker runtime name in daemon.json, and io.containerd.kata-qemu.v2 is a containerd runtime handler that resolves to the shim containerd-shim-kata-qemu-v2.

Firecracker limits

  • Workspaces. Firecracker cannot share a filesystem volume, so a normal workspace is a guest tmpfs lost on sleep, wake or restart, and [workspace] persist = true is refused. For persistent workspaces, enable the chart's direct-volume driver, which gives each one a real ext4 disk pinned to its node:
helm upgrade --install runeward deploy/helm/runeward \
  --set sandbox.directVolumeDriver.enabled=true \
  --set-json 'sandbox.directVolumeDriver.nodeSelector={"katacontainers.io/kata-runtime":"true"}'
  • To bring your own driver, set sandbox.directVolumeStorageClass (or RUNEWARD_K8S_DIRECT_VOLUME_CLASS). Kata's stock driver deletes the disk on sleep and leaves it unwritable to the sandbox user; yours must not.
  • Five drive slots per Pod, and exited init containers' slots are not reused. The supervisor's init container takes one, except under strict egress. Strict egress plus a broker plus a persistent workspace needs six and is refused up front (runeward validate flags it). A cooperative broker with a persistent workspace and a best-effort [filesystem] starts without the supervisor, runs the agent unconfined and records the supervisor as absent; a policy that requires the supervisor is refused in that shape. Use kata-qemu or kata-clh for the refused shapes.

Both limits are tracked in #79. The alternative is the native Firecracker driver (Native microVMs): no Kubernetes or Kata, and the workspace is a per-sandbox disk image that persists.

Native microVMs

Rune Box can also run each sandbox in its own Firecracker or libkrun microVM (host.type = "microvm"). See Native microVMs.

In-sandbox confinement

A policy with [filesystem] or [process] runs the agent under runeward-supervisor as PID 1:

  • Landlock limits the agent to the listed paths.
  • A seccomp filter blocks ptrace, namespace creation and similar escapes.
  • The agent runs as the policy's non-root identity with every capability cleared, and governed execs get the same confinement.
  • The enforcement report goes to the signed audit trail.

The supervisor confines the agent with whatever kernel the sandbox sees: the host's under runc, the guest kernel under Kata, gVisor's own kernel under runsc. It probes Landlock at start: best-effort uses the best ABI available and reports unsupported when there is none; required refuses below ABI 3 and the create fails with the supervisor's reason; landlock = "required" or [process] refuse to start on a kernel that cannot enforce it. The report records the Landlock ABI and seccomp filter actually installed, so a kernel without Landlock shows up as evidence, not a silent gap. The sandbox's root holds only SETUID, SETGID and CHOWN until the supervisor drops them.

Backend Where the supervisor comes from
Docker/Podman The sandbox image (deploy/Dockerfile.sandbox). A best-effort [filesystem] on an image without it runs unconfined, and that is recorded.
Kubernetes Never the sandbox image: an init container from the egress image copies it into a memory-backed, read-only /run/runeward/bin (under strict egress, the iptables init container does this). The agent identity must come from the policy ([process] run_as_user or host.user). Pod exec cannot pick a user, so the backend's own operations (workspace tar streams, file projection, the probe) run through the supervisor as the agent identity without Landlock or seccomp, and the control plane runs the image's own sh, chown and cat as the container's root.

Both backends give the same guarantees. On kind (runc) the opt-in k8s_governance test showed confined execs as uid 1500 with no capabilities, Landlock refusing paths outside the policy, and ptrace and unshare -U refused. The Kata and gVisor paths have not been run against a cluster yet.

Network controls

Network controls (Perimeter) deny egress unless a host is allowlisted.

Mode Enforcement
Cooperative The sandbox uses the proxy via HTTP(S)_PROXY, with a per-sandbox proxy credential.
Strict (network.enforce = "strict") All TCP is redirected through the proxy at the kernel, whatever the proxy env, by an egress sidecar sharing the network namespace. Non-DNS UDP (QUIC/HTTP3) and IPv6 egress are dropped.

RUNEWARD_DNS_RESOLVERS (comma-separated IPs) also confines DNS to those resolvers.

Credential broker. A policy's [[credential]]s never enter the sandbox. The agent gets an opaque rwph_… placeholder, and the proxy swaps in the real secret only on requests to the owning [[endpoint]] (elsewhere it is refused as placeholder-misuse). Inspected endpoints are TLS-terminated with a per-sandbox CA the sandbox trusts. require-approval requests wait for approvals (Conclave), and every decision is audited as tool egress. In strict mode and on Kubernetes the secrets sit in the sidecar: see security.md.

Browser IDE (experimental)

An opt-in code-server against the sandbox's /workspace, reverse-proxied by runeward serve behind short-lived tickets. It is off by default: enable it with RUNEWARD_ENABLE_EXPERIMENTAL_IDE=1 and policy [ide] enabled. It does not replace host-IDE MCP (runeward mcp) or in-sandbox CLI agents.

Warning: IDE keystrokes and commands run in the IDE terminal are not individually policy-gated. Isolation, egress allowlists, budgets, ownership and the audit events ide.open / ide.close still apply, the same boundary as the interactive terminal.

docker build --target ide -f deploy/Dockerfile.ide -t runeward-ide:latest .
RUNEWARD_ENABLE_EXPERIMENTAL_IDE=1 ./bin/runeward --config-dir examples serve
# Create ide-demo, then use the dashboard's Open IDE.
  • [ide] fields: enabled, port (default 8080), optional path, optional agents (dashboard hints only).
  • Target ide-agents builds runeward-ide-agents:latest with Claude Code and Codex CLIs; Cursor CLI needs --build-arg INSTALL_CURSOR=true --build-arg CURSOR_INSTALLER_SHA256=<digest>. Example policies: ide-demo, ide-claude, ide-codex, ide-cursor.
  • Routes: POST /v1/tickets with kind=ide or POST /v1/citadels/{id}/ide-ticket mint a ticket; /v1/citadels/{id}/ide is the proxy. See rest-api.md.

Limits:

  • ide-demo denies all egress, so extensions and cloud assistants fail until you allowlist hosts.
  • code-server runs with --auth none on the container network only. Do not publish the IDE port on the host.
  • Session cookies are process-local, so multi-replica serve is not supported.
  • Not supported: desktop GUIs (Cursor, Claude, Codex) in the sandbox, first-class GitHub Copilot (code-server uses Open VSX), or per-keystroke audit or IDE recording.

Written from docs/isolation.md, docs/browser-ide.md, docs/security-model.md at commit 1ee29ff4ab36. Rune Box is open source under Apache-2.0.