Skip to content

P1: Enforce cross-VM host isolation, extract the sandboxed proxy, add capsem proxy, and document the architecture #206

Description

@ebursztein

Status and scheduling

P1 — unified sandboxing work for the 0.7 stabilization milestone. Ledger confinement, VM proxy/DNS extraction, standalone capsem proxy, and architecture documentation remain together in this issue. The steps below are internal implementation milestones, not separate product projects.

Roadmap: landed #199 → finish #227 → snapshot removal and correctness/counter stabilization → this unified #206 effort. #207/#209 describe control and codec work already carried by #199, not new migrations to schedule here.

#201 supplies the original MITM privilege finding; its proposed owner-side evaluator is superseded by the proxy ownership specified below. Preserve the authenticated control, browser isolation, policy and lifecycle behavior already landed with #199.

Internal implementation order

  1. Define and enforce worker authority, trusted session/generation grants and broker authorization (section 1).
  2. Confine all session-ledger reads and writes; settle stopped-session access, worker supervision and recovery (section 5).
  3. Extract the existing VM proxy/DNS into the confined per-session engine (section 2).
  4. Expose that same engine through standalone capsem proxy, without a VM (section 3).
  5. Complete integration/platform proof and the architecture documentation (section 4); update affected contracts and security claims as each boundary changes.

Keep these as focused, reviewable commits and Sprinty items within one effort. The standalone endpoint depends on the confined engine and ledger; it is not a second proxy implementation. Keep the #227 implementation sprint separate.

Goal

Complete Capsem's host-side isolation so a compromised VM, or a host worker processing its traffic, cannot access another VM's files, credentials, control sockets, or unrelated networks.

Extract the existing MITM, security enforcement, logging, and telemetry pipeline into sandboxed proxy workers. Reuse that engine for both VM interception and a new capsem proxy command exposing an OpenAI-compatible API endpoint.

Deliver a clean architecture page for the rebuilt documentation site, describing the implemented system rather than the historical design. The host MCP package is already an SDK client following #199; preserve that ownership.

Problem and security contract

The source audit found explicit OS confinement around capsem-router switches and published-port relays, but incomplete host boundaries elsewhere:

  • HTTP gateway: authenticated separate process, without application-installed OS confinement.
  • VM owners: cleared inherited environments, but still the launching user's filesystem/socket authority.
  • MITM and DNS: inside the process holding VM control and hypervisor resources.
  • Documentation: 0700 directories and 0600 sockets incorrectly described as preventing sibling same-UID access.

Library separation, parent watching, authentication, and environment clearing are useful controls, not substitutes for an OS sandbox.

Assume malicious guest traffic and potential code execution inside a guest-facing host worker. Its resulting authority must be limited to its assigned VM, proxy session, or network. The coordinator and host kernel remain trusted. Intentional communication between members of the same network follows that network's contract. Standalone proxy clients are not themselves sandboxed.

A compromised worker must also be unable to trick the coordinator into acting for another session: broker IPC authorization is part of the containment boundary, not merely input validation.

Required implementation

1. Enforce process authority

Define and implement each process's permitted files, descriptors, listeners, upstream connections, IPC operations, credentials, and lifetime owner.

Process Required authority
capsem-service Trusted lifecycle coordinator and capability broker
HTTP gateway API listener, necessary control/terminal channels, narrow runtime/log access
VM owner One VM's virtualization resources, session, read-only assets, required IPC
Proxy worker One VM or standalone proxy session's traffic, policy, credentials, telemetry
Session ledger process One session's directory (session.db, its WAL/SHM sidecars, archive generation files and locks defined by the landed #227 protocol); its socket to granted producers and readers. No network, no exec, no entitlement, no other path
Network switch One network's attachments and forwarding control
Published-port relay Explicitly granted connections for its owning VM
  • Apply appropriate confinement on macOS and Linux, preserving required VZ/KVM operations without unrelated session access.
  • Clear worker environments and close unrelated inherited descriptors.
  • Prefer scoped descriptor grants and narrow broker operations over access to entire runtime directories.
  • Install confinement before untrusted input or readiness; fail startup, terminate, and reap on confinement failure.
  • Bind capabilities to trusted session identity and generation. Never authorize from a worker-supplied VM ID, path, or network ID alone.
  • Make cancellation close resources and join/reap owned tasks and children.
  • Retain and verify existing confined switches/relays rather than replace them unnecessarily.

Gateway confinement needs its own policy: it must accept connections and use authorized service/terminal channels. Do not copy the descriptor-only router policy blindly. Preserve SDK gateway authentication; the coordinator must also enforce the authority of its control connection.

2. Extract one reusable proxy implementation

  • Move MITM/TLS/HTTP processing, traffic rules/plugins, provider routing, credential integration, logging, and telemetry out of the VM owner.
  • Run one confined proxy worker per VM; each standalone invocation gets an independent session/worker. Share implementation, not unrestricted authority across sessions.
  • Keep the coordinator on configuration, scoped grants, and lifecycle. Do not relay every HTTP body, packet, or audit record through a central coordinator.
  • Keep database execution/storage in capsem-logger, run inside the confined per-session ledger process (section 5). A worker's logging capability is a granted connection to its own session's ledger process, never a path. There is no global logging daemon: one ledger process per session. No route-owned SQLite access or projection caches.
  • Relocate guest-facing DNS into the confined networking/proxy responsibility, preserving private-name resolution and upstream DNS semantics.
  • Preserve rules/plugins, credential handling/redaction, streaming/cancellation/backpressure/timeouts, upstream errors, usage and session attribution, audit visibility/flushing, and VM boot/restart/stop/recovery.
  • Remove superseded in-process handlers and all unconfined fallbacks.

Provision only the keys/certificates and credential capabilities required by that worker. The threat model must state that a compromised proxy holds its own session's granted authority, never another session's authority. Scoped upstream connection capability must not become arbitrary dialing.

3. Add capsem proxy

Expose the shared processing engine as an OpenAI-compatible API endpoint: configure an existing OpenAI SDK client's base_url to point at it.

  • Use the existing profile/provider model to select upstream and security configuration.
  • Configurable bind address and port; default loopback, OS-assigned port. Report usable base URL and proxy-session identity.
  • Start without booting a VM.
  • Preserve supported OpenAI-compatible surfaces, including Chat Completions and Responses where supported by the landed engine, streaming, and cancellation.
  • Route only to configured provider authority; client headers/payloads cannot create an arbitrary-destination relay or forge trusted VM identities.
  • Use existing scoped credentials and redaction.
  • Keep the data listener separate from the authenticated SDK/control gateway.
  • Stopping the command stops its listener/worker, cancels active requests, and completes bounded audit shutdown.
  • Register the new command in the public-surface contract; update help, examples, and relevant schemas without unrelated API changes.

The proxy listener is unauthenticated by design. Remote binding is supported; deployment/network controls own access restriction. Do not introduce proxy authentication. SDK gateway authentication remains independent and mandatory.

No standalone forward-proxy mode, CONNECT, transparent host interception, or client CA installation. Transparent interception belongs inside Capsem VMs. This supersedes the previous forward-proxy/client-trust wording of #206.

Standalone mode controls and audits only model traffic sent through its endpoint. It cannot prevent an unrestricted agent from bypassing it or observe all host files, tool execution, or other network connections. State that limit accurately.

4. New documentation architecture page

  • Write a canonical page for the rebuilt site based on the landed implementation, not the historical manual.
  • Show actual processes separately from libraries/threads, and distinguish control, private-network, and proxied-traffic paths.
  • Explain per-VM/per-network/global ownership and each sandbox's permitted and denied capabilities.
  • Cover gateway, coordinator, VM owner, switch, relay, proxy, session ledger process, and MCP above the SDK.
  • Show VM interception and standalone API access as adapters to the same processing engine.
  • Explain policy, credentials, logging/telemetry, revocation, failure/recovery, OS differences, and verified limitations.
  • Include a working SDK base_url example.
  • Correct misleading same-UID isolation claims in developer references and replace/redirect contradictory pages through the new site's publication workflow.

Do not restore the old documentation site merely to publish this page. If the rebuild is not ready, retain reviewed content/diagrams as an explicitly pending deliverable and do not claim publication complete.

5. Confine the session ledger: one sandboxed process per session

Why this is needed. The ledger turns guest-controlled bytes (HTTP bodies, model traffic, tool payloads, file events) into SQLite writes. SQLite is C, and it is the only C library in the runtime today. Right now it runs inside the VM owner, which holds the virtualization entitlement and the user's full file authority. A memory-safety bug reachable from guest bytes there is a host compromise. Moving the proxy out (section 2) also makes a second producer of ledger records, while the ledger must keep exactly one writer (enforced by the writer lock since the ledger-archive work for #213). Both problems have the same answer: the ledger gets its own confined process, and the VM owner and proxy worker become clients of it.

Why C belongs only inside it. Pure-Rust compression tops out below what the ledger needs: measured on real ledgers, ruzstd 0.9 (and its master) implements only its fastest level with a fixed 128 KiB window and no dictionary encoder, and loses to our current pure-Rust deflate (7.0x vs 10.8x on an 80 MB ledger). The C zstd library at 1-2 MiB blocks reaches roughly 20-50x on the same data, more with a trained dictionary. We accept C code only where a compromise of it is contained: inside this process's Seatbelt profile on macOS and Landlock + seccomp on Linux, never in an unconfined host process. SQLite moves into the same boundary.

Required:

  • At most one authoritative ledger process per session, with an explicit supervisor and parent-death behavior. Never one process for all sessions: a compromise must stay inside one session. Stopped-session reads must also use a confined ledger process, without booting a VM or falling back to service-side SQLite.
  • It is the only process that executes SQLite or zstd against that session's data. Every read and write of that session's ledger, including retention and WARC export, goes through it. This is a session-ledger boundary, not an implicit migration of unrelated service databases.
  • Readers query it over its socket and decode typed results in Rust. No host process outside it opens session.db with SQLite: a compromised ledger process could otherwise hand the service a crafted database file, moving the C attack surface to the reader side.
  • macOS: deny-by-default Seatbelt, read/write only under that session's directory, no network, no process-exec, no Mach lookups beyond what SQLite needs (ideally none). Linux: Landlock restricted to the session directory plus a seccomp allowlist. Cleared environment, inherited descriptors closed, libraries loaded before confinement, temp storage in memory. Confinement failure fails startup.
  • The session identity is stamped by the ledger process from its grant, never taken from a producer's message.
  • Typed write/query messages use the landed named-field MessagePack framing and limits from feat(sdk): add typed gateway SDKs and npm MCP #199/Unify internal IPC and VSOCK codecs with bounded framing and serde_bytes #209. Keep operation types independent of transport; a future micro-VM is not an implementation requirement here.
  • Refused audit admission still refuses the action: if the ledger process is gone or refuses a required record, the producer fails closed. Preserve A stalled ledger writer can freeze a capsem-process runtime worker; DNS ignores its admission result #225's admission/runtime-liveness fixes and Security ledger event_json.decision says allow on blocked HTTP and DNS requests #203's accurate recorded decisions; those are distinct contracts.

Archive and compression:

  • Reuse the final Session ledgers: v3 archive publication, bounded memory, sparse events #227 generation/publication/recovery protocol, body indexes, checksums, open-block behavior and cross-block deduplication. Those are existing foundations, not features to implement again or call a new "format v2".
  • Add zstd only inside the confined ledger process, with pinned/reviewed C dependencies and explicit codec identification using the landed format's extension contract. Preserve committed-data durability, bounded reads, retention and export semantics.
  • Retain compressor history across durability flushes without weakening the archive-before-index ordering. Measure the zstd change against the landed archive baseline.
  • Trained dictionaries remain optional. If used, define their durable identity and lifecycle with archive generations; they must not become a prerequisite for proving confinement.

Lifecycle contract — settle before implementation:

  • Name the supervisor and granted access for live VMs, standalone proxy sessions and stopped persistent-session reads. Define acquisition/idle shutdown and transitions so two ledger owners cannot coexist.
  • Specify the response to ledger/proxy death: active operations fail explicitly, required audit failure refuses the protected action, queues/resources stay bounded, and unrelated sessions continue.
  • Define restart ownership, fresh grants/generations, committed versus merely accepted records, and replay/idempotency behavior. Do not blindly retry mutations or claim an accepted-but-uncommitted record survived.
  • Preserve named-session stop/resume and historical reads without restarting the VM, bypassing confinement or opening its SQLite database in the coordinator.

Integrity contract:

  • Keep producer-side hashing outside the ledger process, but specify exactly what it proves before implementing the chain: record identity/order, multiple producers, durable checkpoints outside the ledger's authority, acknowledgment boundaries, and restart/replay/retention rules.
  • Verify alteration, substitution and omission against those trusted checkpoints. State the limits: a bare hash chain does not prove completeness, an unanchored tail may be lost, and a compromised producer can send false records. Do not claim detection beyond the implemented and tested contract.

Verification and acceptance

Effective confinement and cross-VM denial

Use test-owned fixtures to demonstrate behavior, not merely inspect configuration:

  • Worker A cannot read/write B's session, obtain B's credentials, open B's control endpoints, or forge access by changing identifiers.
  • A switch cannot access another network's attachments or VM control.
  • Gateway/proxy workers cannot execute arbitrary processes, signal unrelated processes, open arbitrary host files, or establish ungranted outbound connections.
  • Necessary operations still work under confinement on macOS and Linux.
  • Deliberately failed confinement prevents readiness and traffic acceptance.
  • Compromised-worker/broker requests cannot expand authority beyond trusted session/generation grants.
  • Ledger process A cannot open, read or write session B's directory, archive or lock, and cannot open a socket or exec a process; a producer granted A's ledger cannot write rows attributed to B.
  • No host process other than a session's ledger process opens that session's session.db with SQLite (citadel guard).

Functional integration

  • Real VM/OCI workloads retain interception, policy, credentials, private networking, and telemetry behavior.
  • Python and TypeScript OpenAI SDK clients work with hermetic upstreams through the standalone base URL.
  • Verify supported streaming/non-streaming APIs, cancellation, tool-call payloads, usage, and upstream failures.
  • Denied requests reach no upstream; exact audit facts identify session/provider/decision/outcome. Preserve existing fail-closed enforcement/audit-admission semantics.
  • Standalone mode starts no VM and implements no CONNECT/forward-proxy route.
  • Remote binding is unauthenticated as specified; SDK gateway authentication remains effective.
  • Exercise the new CLI and documentation examples through real entrypoints.

Chaos and lifecycle

Use Greyjoy-style fault injection and retain earlier review regressions:

  • Kill a proxy while its VM and another VM are active; effects remain scoped.
  • Kill a ledger process mid-flush: its producers fail closed, the other sessions are unaffected, a restart reopens the ledger without losing committed rows or reusing its old grant, and the hash chain still verifies.
  • Ledger process memory stays bounded by configured queues, the open block and unflushed rows, never by total ledger size. Use bounded fixtures for routine regression proof; any long soak is an explicit manual measurement, not a new per-change gate.
  • Restart workers without reusing stale credentials/grants/session identity.
  • Exercise disconnect/reconnect races and network retirement.
  • Stall clients/upstreams, interrupt streams, and exhaust bounded queues.
  • Repeat creation/destruction without accumulating processes, descriptors, tasks, or buffers.
  • Verify bounded audit shutdown and failure handling without bypassing policy.

Verification cost

Use focused deterministic checks for each milestone, existing platform/VM fixtures for crossed boundaries, and the required final integration gates. Reuse fixtures and evidence rather than repeating the full gate per commit. No new multi-hour stress suite on every change. Keep the accepted abstract archive model under one second; do not translate its enumerated schedules into real VM runs.

Completion gates

  • Required focused tests and platform gates pass on the final candidate.
  • No security behavior regression, unconfined fallback, or duplicate proxy implementation.
  • Architecture diagram/capability table match the actual implementation.
  • Documentation/examples verified against the final SDK; publication status explicit.
  • User-visible changes have a changelog entry.
  • Remaining trust assumptions and standalone proxy limitations are explicit.

This issue is complete only when boundaries are enforced and tested—not when code is merely moved into subprocesses.

Related work

This description replaces the older forward-proxy/client-CA proposal. The only standalone mode in scope is the OpenAI-compatible API endpoint above; the unified sandboxing scope remains intact.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions