Security model¶
Interactive sessions are not per-command policy gates
Per-action policy and approval decisions apply to calls routed through the control plane's governed tool APIs (REST, MCP, dashboard shell/file/code controls, and SDK adapters). The interactive terminal, optional browser IDE (code-server), and commands started directly inside a sandbox receive sandbox, network, and resource controls plus terminal recording / Chronicle open-close for IDE sessions, but Runeward does not intercept each shell command or IDE keystroke for a policy verdict. Use governed tool calls when individual command approval and signed verdict evidence are required.
runeward's job is to reduce the blast radius of an autonomous agent. Knowing what it does — and does not — protect against is essential to using it safely.
For reporting vulnerabilities, see SECURITY.md. Please disclose privately; do not open a public issue.
What runeward provides¶
- Isolation. Each cell runs in a container (Docker/Podman) or Pod (Kubernetes)
with its own workspace. Docker cells are hardened by default: all Linux
capabilities dropped,
no-new-privileges, a--pids-limit, and default memory/CPU ceilings (overridable viaRUNEWARD_SANDBOX_MEMORY,RUNEWARD_SANDBOX_CPUS,RUNEWARD_SANDBOX_PIDS; set to0to disable). Settinghost.read_only = truemounts the root filesystem read-only (with a writable/tmpand the writable workspace) on both Docker and Kubernetes.host.seccomp/host.apparmorpin a seccomp/AppArmor profile (Docker--security-opt; Kubernetes Localhost profiles), and Kubernetes pods default to the runtime's seccomp profile rather than Unconfined. A Charter can explicitly override an application image's entrypoint withhost.command; Docker/Podman creation requires the process to remain alive across post-start checks before the Citadel is reported as running. - Control-plane authentication.
runeward servebinds127.0.0.1by default and refuses any non-loopback--bindunless authentication is set (an API token via--token/RUNEWARD_API_TOKEN, or an RBAC store). When set it is required on every request — REST,/mcp, the terminal WebSocket, and the dashboard. A non-loopback listener also refuses plaintext HTTP unless TLS is configured with--tls-cert/--tls-keyor the operator explicitly passes--allow-insecure-httpbehind a trusted TLS-terminating proxy. Request bodies are size-capped to bound memory use. - RBAC / multi-principal auth. Setting
RUNEWARD_AUTHZ_FILEto a JSON store of principals (each with its own token, an allowed-profile glob list, and launch, approval-profile, and admin scopes) upgrades the single shared token to per-principal access: the server enforces which profiles a caller may launch and whether it may resolve approvals, and records the principal name as the audit actor./v1/whoamireports honestcan_launch/can_approveflags;/v1/chartersand policy simulation are scoped to launchable Charters for non-admins. A principal has a resource-owningtenantand an independently attributed actorname; several agents can therefore collaborate without sharing one identity. Each Citadel records its owning tenant; a non-admin can see and act on only its tenant's Citadels (an ownership guard enforces this on every/v1/citadels/{id}route), while admins see all. The dashboard has an interactive token login (backed by/v1/whoami) that gates create/approve controls to what the caller is permitted; the static dashboard shell loads without a token so the login screen can render, but the API always requires one. Recovery snapshots are tenant-scoped and persisted. HTTP MCP uses the same per-request principal and ownership rules as REST; stdio MCP binds one process to the principal selected byRUNEWARD_MCP_DEFAULT_TOKENor the credential saved byruneward auth login. - HTTP hardening. Responses set
nosniff, frame denial, no-referrer, and a restrictive Permissions Policy. API/MCP responses are no-store and use a deny-all content policy; dashboard assets use the explicit dashboard CSP. - OIDC authentication.
RUNEWARD_OIDC_ISSUERplusRUNEWARD_OIDC_AUDIENCEenables RS256 JWT verification from the provider's JWKS. Runeward maps only signed claims:runeward_tenant,runeward_profiles,runeward_approval_profiles,runeward_can_approve, andruneward_admin. Issuer, audience, expiry, not-before, algorithm, key id, and signature are checked. Non-loopback issuer and JWKS endpoints must use HTTPS. - Signed Cohort leases. Claim returns a short-lived HMAC-signed
lease_tokenbound to Cohort, task, actor, and expiry. Heartbeat returns a refreshed token; complete/fail require the current token. Listing a task never exposes its lease capability. - Durable state. Cohort boards, snapshots, and provider-neutral Run records
use private atomic state-file replacement. A run still marked active after a
control-plane restart is closed as
interruptedrather than appearing live. - Cost / token budgets. Agents or Cohort workers report model usage to
POST /v1/citadels/{id}/usage; usage accrues per Citadel and per Charter (surfaced in Prometheus and the Citadel view). A Charter'srationing.max_tokens/rationing.max_cost_usdcaps are enforced fail-closed — once exceeded, further governed tool calls are denied. - Attributed approvals. Resolving an approval records who decided it (the
RBAC principal name, else
X-Runeward-Actor, else the peer address) in the audit ledger. - Deny-by-default egress, enforced at L3 on both backends. Network access is
denied unless explicitly allowlisted. Cooperative mode points the sandbox at
the proxy via
HTTP(S)_PROXY(the host proxy requires a per-cell credential); strict mode (network.enforce = "strict") enforces transparently at the kernel: on Kubernetes via an iptables init container + sidecar sharing the pod netns, and on Docker via aNET_ADMINegress sidecar that owns the netns (the sandbox joins it with--network container:…). In strict mode all TCP is redirected through the proxy regardless of proxy env, so code that ignores it can't bypass the allowlist. The strict path also drops non-DNS UDP (blocking QUIC/HTTP3 bypass) and IPv6 egress; settingRUNEWARD_DNS_RESOLVERS(comma-separated IPs) additionally confines DNS (UDP+TCP :53) to those resolvers, closing DNS as a covert exfil channel. - Per-action policy and approvals.
allow/deny/require-approvalverdicts, with human-in-the-loop gates for risky operations. - Guardrails. Hard caps on wall-clock, exec count, egress requests, and token/spend budgets, plus retry-loop detection.
- Tamper-evident audit. An append-only, hash-chained, ed25519-signed ledger,
independently verifiable offline. Events can also stream in real time to a
webhook or file sink (
RUNEWARD_AUDIT_WEBHOOK_URL/RUNEWARD_AUDIT_FILE) for SIEM ingestion, over a non-blocking queue that never stalls the ledger. A built-in anomaly detector flags novel egress targets, exec bursts, and denial spikes (RUNEWARD_ANOMALY_*). - Terminal session recording. With
RUNEWARD_RECORD_TERMINALS=1, governed terminal sessions are captured as asciinema v2 casts under the state dir and can be replayed withruneward replayas part of the audit trail. - Read-only Live chat. Conversation turns are opt-in messages published by an agent harness, not automatic capture of private agent UI text. Runeward bounds each Citadel's in-memory history, applies declared-secret and credential-pattern redaction, strips terminal controls, and streams through an ownership-checked, same-origin WebSocket using a short-lived scoped ticket. The observer socket accepts no application input. Live chat is operational visibility; the signed Chronicle remains the authoritative action evidence.
- No host mounts.
copy_fromcopies into the sandbox; the host tree is never mounted. Request-time overrides are administrator-only. SetRUNEWARD_COPY_FROM_ROOTS(a colon-separated allowlist) to confine which host directoriescopy_frommay read; sources outside the roots fail creation. - Kubernetes multi-tenancy. The managed namespace carries Pod Security
Admission labels (
RUNEWARD_K8S_PSA_ENFORCE, or the chart'spodSecurityStandard), Citadel containers always dropALLcapabilities and disable privilege escalation, and an optional default-deny NetworkPolicy (DNS-only egress) isolates Citadel pods (RUNEWARD_K8S_NETWORK_POLICY, or the chart'snetworkPolicy.enabled) so cells can't reach each other or the control plane laterally. - Admission enforcement defaults. The validating ClusterPolicy webhook is
fail-closed (
failurePolicy: Fail) so webhook outages block admission for governed resources. The mutating default-profile webhook is best-effort (failurePolicy: Ignore) and only fills missingspec.profile. - Supply-chain assurance. Releases are cosign-signed (keyless) with SBOMs,
and CI runs SAST (gosec, CodeQL), dependency/vuln scanning (govulncheck, Trivy),
per-image CVE scans, and a DAST baseline, with Dependabot keeping dependencies
current. Gosec uploads the complete SARIF report for triage and blocks releases
on every unsuppressed HIGH-severity result; accepted architecture findings use
narrow, source-local
#noseccomments with justification.
In scope (please report)¶
- Citadel escape from a cell to the host or another cell.
- Bypass of the egress allowlist, policy engine, or approval gates.
- Audit-ledger forgery or silent tampering that verification would miss.
- Path traversal / writes outside the intended workspace (e.g. tar-slip).
- Auth/authorization flaws in the REST API, WebSocket terminal, browser IDE proxy, or admission webhook.
- Secret leakage in logs, the ledger, or the dashboard.
Operator responsibility (out of scope)¶
- Security of the container runtime, host kernel, and Kubernetes cluster — keep them patched.
- Trustworthiness of images referenced by Charters and of the agents/CLIs you run inside a cell.
- Secrets you place in Charters; runeward redacts declared secret values from
the ledger and additionally masks common credential shapes (API keys, bearer
tokens, PEM keys,
password=/token=pairs) wherever they appear, but pattern matching is best-effort and can't catch every custom format. - Network exposure of
runeward serve. It binds127.0.0.1and requires an API token before any non-loopback bind, but you still choose the token strength, terminate TLS appropriately, and configure static RBAC or OIDC rather than a shared token for team deployments. - Denial of service from workloads you explicitly grant large resource limits.
Operational notes¶
One writer per ledger
The audit ledger is single-writer, protected by a file lock. Give each running
instance its own RUNEWARD_STATE_DIR. Two processes sharing one ledger produce
out-of-order/duplicate records, permanently breaking the hash chain so
verification reports tampering.
Same-origin WebSocket
The dashboard terminal WebSocket enforces a same-origin check to prevent
cross-site hijacking, and state-changing REST requests reject mismatched
browser Origins. Per-IP rate limiting defaults to 50 requests/sec; tune
RUNEWARD_RATE_LIMIT or explicitly set it to off for trusted benchmarks.
Front the control plane with TLS in production.
Experimental browser IDE
With RUNEWARD_ENABLE_EXPERIMENTAL_IDE=1 and Charter [ide] enabled, serve
reverse-proxies in-cell code-server. That session is workspace-equivalent
interactive access (not per-keystroke policy). Cursor/Claude Desktop/Codex
GUIs are not embedded; GitHub Copilot is not first-class on code-server
(Open VSX). Details and limits: Browser IDE.
runeward is defense-in-depth, not a hard isolation boundary. Its default
container backend shares the host kernel, so a determined escape via a kernel or
runtime vulnerability is possible. For untrusted or adversarial workloads, add
VM-grade isolation by setting host.runtime_class in the profile to a
sandboxed runtime. On Kubernetes this maps to runtimeClassName (e.g. gvisor
or kata); on Docker it maps to docker run --runtime (e.g. runsc for
gVisor, or kata-runtime). The runtime must first be installed and registered
with your engine — runeward does not install it, and a name the engine doesn't
recognize fails cell creation rather than silently falling back to the
shared-kernel runtime. runeward runtime check probes Docker and Kubernetes for
registered runsc/kata runtimes and runeward runtime guide prints the setup
steps. For the strictest cases, also use a disposable host.
runeward's sweet spot is governing a cooperative-but-fallible agent, not caging
code whose goal is to break out.