Security and evidence

Security model

Trust boundaries, fail-closed guarantees, deliberate limits, descriptor ownership and supply-chain claims.

Trust boundary

Maelys Warden enforces a decision; it does not make one. Its low-level Executor engine receives the executable path, argv, environment, working directory and sealed SandboxPlan. Those inputs must come from a trusted host configuration or an external policy decision point, never directly from an untrusted MCP request or agent-controlled project file.

Backend selection is a capability intersection. Every capability required by a sealed plan must be present. An unavailable sandbox backend never falls back to POSIX for a confined plan.

POSIX

POSIX provides process creation, fd layout and bounded process-group shutdown. It advertises only EXECUTION. A descendant can call setsid() and escape the group, so POSIX is not an adversarial sandbox.

Bubblewrap on Linux

Bubblewrap uses user, PID, IPC and UTS namespaces, drops capabilities, installs fresh /proc and /dev, and uses a network namespace for NETWORK_IP_NONE. For NETWORK_NONE and EXTERNAL_CHANNEL, it additionally installs a classic BPF seccomp filter that validates the native audit architecture and denies socket, connect, ptrace, process-vm and io_uring entry points. x86-64 x32 syscalls are killed rather than falling through the native-number comparisons. Its behavioral probe runs the actual required flags with an empty environment, closed inherited descriptors and a deadline.

Bind sources are canonicalized and restricted to regular files/directories at prepare time. Device and inode identities are checked again immediately before spawn. Nested user mount destinations are rejected; /proc and /dev are reserved and installed last. Host-controlled source ancestors remain a trust assumption because Ubuntu 24.04's Bubblewrap does not provide every fd-based mount primitive needed to eliminate the final pathname race.

The filesystem begins empty. READ and WRITE rules become canonical pinned bind mounts, then the synthetic ancestor tree is frozen read-only. Explicit DENY rules are installed last as mode-000 read-only mount overlays, so a broader WRITE grant cannot rename, unlink or expose the denied mountpoint. Safe host compatibility symlinks such as /lib -> usr/lib are reconstructed only when the corresponding host link is relative and contains no dot components; they grant no path that was not already mounted by the plan.

Bubblewrap claims strong NETWORK_NONE, explicit DENY, syscall filtering and Unix-socket control under this construction. It does not claim complete hostile-descendant supervision. A caller choosing the weaker NETWORK_IP_NONE explicitly may still expose a pathname-addressed Unix socket through an authorized bind mount.

Seatbelt on Apple Silicon

Seatbelt is supported on native arm64 macOS 15 and newer. The probe requires a root-owned, non-writable /usr/bin/sandbox-exec and executes a behavioral profile before advertising capabilities. The tool is deprecated by Apple, so it is isolated behind the same backend ABI as future Darwin mechanisms.

The profile starts from deny-default, allows the minimum process and platform runtime operations needed by native tools, adds plan READ/WRITE rules as data parameters. Direct network operations are absent for NETWORK_NONE. View remapping, tmpfs and explicit DENY rules are rejected. DENY remains in the plan vocabulary, but no local backend claims it until rename-safe enforcement is available.

Seatbelt policy inheritance does not make POSIX process-group lifecycle complete: a hostile descendant can create another session. Consequently the backend advertises PROCESS_CONFINEMENT, not PROCESS_TREE_LIFECYCLE.

Path rules are pathname enforcement. Existing symlink rule roots are rejected. Global file metadata access is part of the Darwin runtime baseline, so the backend protects contents and mutation, not path-existence secrecy. Rename and absent-path hardening beyond the explicitly claimed capabilities remains for a later release.

Apple Container 1.3.0

The two optional Apple candidates accept only immutable OCI TargetSpecs. The image reference is paired with a mandatory sha256: digest, and the Swift driver verifies the resolved image digest before reporting READY. Warden pins the driver's canonical path, owner, mode, device, inode and SHA-256 across probe, prepare and spawn. A missing driver or runtime is unsupported, never a reason to select a native backend for the OCI target.

apple-container uses the VM boundary, a read-only OCI root, pinned VirtioFS mount sources and tmpfs. It does not advertise portable filesystem access rules or ABI-10 resource guarantees: a root workload can modify the service cgroup. apple-container-bubblewrap additionally lowers recursive READ/WRITE rules inside the Linux guest. The Apple service applies the exact VM vCPU and memory dimensions. Before the workload starts, Warden's trusted bootstrap moves Apple's PID 1 and its own helpers into a supervisor cgroup, creates a separate workload cgroup with exact memory, swap, CPU and task limits, and places the Bubblewrap subtree inside it. Bubblewrap exposes cgroupfs read-only, so the workload cannot widen those limits. The bootstrap reports measured peaks, events, wall-deadline outcome and verified cgroup cleanup through MWAR/2; missing or malformed evidence makes the run fail closed.

The composite driver explicitly clears Apple Container's masked and read-only proc paths, then Bubblewrap installs a fresh procfs inside the nested PID namespace while retaining its user namespace. The workload has empty effective and bounding capability sets and NoNewPrivs; failure to mount procfs aborts admission rather than exposing the inherited guest view. Explicit DENY, create protection and an inner syscall filter remain separate, unadvertised semantics. Missing or unusable Bubblewrap causes failure of the composite rather than fallback to the plain candidate.

For NETWORK_NONE, Apple Container receives no ambient network attachment. For mediated networking, a pinned guest relay reaches the execution-private host Egress listener through Containerization's UDS/vsock bridge. The relay is protocol-blind and the workload receives no host descriptor. Native fd-4 SCM_RIGHTS mediation is not advertised because descriptor passing cannot cross the VM's kernel boundary.

Warden drives lifecycle with its framed control protocol, deadlines and a final host-side kill of an unresponsive Swift driver. The candidates do not advertise PROCESS_TREE_LIFECYCLE: killing the driver alone does not prove that the Apple service deleted every VM resource. See apple-container.md and the capability matrix.

The optional Krun package may be discovered as two ABI 10 candidates. Both admit only a digest-pinned OCI linux/arm64 target whose ext4 root, artifact seal, driver, firmware libraries and guest bundle remain identical between preparation and spawn. krun supports view mounts, tmpfs and network NONE. krun-bubblewrap additionally lowers recursive READ/WRITE rules through a static helper whose digest is bound by the guest manifest, artifact seal and candidate evidence. It mounts a fresh procfs inside its private PID namespace; failure to create that procfs aborts admission before READY. DENY, literal rules and create protection remain fail-closed. For mediated proxy networking, the Warden-owned relay listens only on the guest loopback and dials AF_VSOCK port 4520. libkrun TSI and virtio-net remain disabled; the mapped host endpoint is the identity-pinned per-execution Egress AF_UNIX listener. Before exec the guest init installs a workload seccomp filter that denies AF_VSOCK socket creation and every io_uring entry point, so only the sibling relay reaches that mapping. The host VMM driver is itself wrapped in a behaviorally proven deny-default Seatbelt profile whose identity and pinned dynamic-library closure enter candidate evidence.

Krun and direct Containerization may additionally admit the canonical ROOT_EPHEMERAL_WRITE decision. The ext4 lower remains open read-only and is measured before admission. A per-execution tmpfs of the exact RunSpec ceiling holds OverlayFS upper/work data; workload/Bubblewrap starts only after a write/delete probe. PID 1 reaps the execution domain before unmounting and MI/6 reports the applied size and cleanup result. A forced VMM teardown destroys the guest memory even when synchronous guest cleanup cannot complete.

The direct apple-containerization candidates own their VZ VM lifecycle and boots Warden's pinned kernel and PID 1. Its host driver is wrapped in a deny-default Seatbelt profile compiled from the pinned kernel, init, root and view paths. A behavioral probe proves denial of an out-of-profile file and IP network before registration. The per-execution writable directory is 0700 and cleaned by Warden. Its code-signing entitlement merely permits Virtualization.framework use; it is not confused with the Seatbelt guarantee. For linux/amd64, selection additionally requires an installed, stable Rosetta identity; the guest mounts the Rosetta share read-only and registers only the x86_64 ELF handler.

apple-containerization-bubblewrap adds the same guest-side recursive READ/WRITE lowering used by krun-bubblewrap. The Swift driver transports the resolved rules but cannot compile or widen them. Warden's PID 1 invokes the sealed /maelys-bwrap through libmaelys-guest-isolation.a, requires a bounded preflight before workload admission and drops all capabilities. The simple candidate carries an explicit false selector and is rejected whenever the plan requires FS_ACCESS_RULES.

Descriptors and environment

Executor-created descriptors are close-on-exec until deliberately mapped. Linux uses posix_spawn_file_actions_addclosefrom_np; macOS uses POSIX_SPAWN_CLOEXEC_DEFAULT. Isolated duplex maps protocol bytes to child fd 3, stdin to /dev/null, and stdout to stderr. The kernel fd layout protects MCP JSON from noisy stdout in every provider language.

Explicit and empty environment modes prevent ambient credential inheritance. Inherit mode is an explicit host choice. Probe programs always receive an empty environment and no inherited descriptor above stderr.

Executor owns the wait status of processes it starts. An embedder must not reap these children from a global SIGCHLD handler or enable SA_NOCLDWAIT.

Mediated network

An EXTERNAL_CHANNEL plan requires a non-empty canonical TCP hostname/port allowlist and confinement. Executor does not implement that allowlist, resolve DNS or call outbound connect(). ABI 10 requires the trusted host to install a connected blocking AF_UNIX datagram channel in the request; the backend denies ambient networking and maps only its child endpoint to fd 4. A missing channel, a channel on another socket family/type, or a channel attached to a non-external plan is rejected before backend prepare.

The Warden–Egress adapter creates the pair and translates each validated fd-4 wire request to Egress's authenticated native connector. Egress is the sole engine for the sealed destination policy, DNS/pinning, private-address decision, connect deadline, optional TLS SNI guard, byte relay, quotas and network receipts. It returns a private relay client stream, never the upstream socket. The broker passes that stream to the workload with SCM_RIGHTS; it keeps no allowlist and performs no direct network operation. Calls sharing one fd 4 must be serialized.

SCM_RIGHTS transfers a reference to a same-kernel open file description. It does not authenticate, authorize or cross a VM boundary. Channel privacy, credential binding, the sandbox descriptor layout and Egress policy supply those guarantees. See docs/egress-bridge.md for normative Unix references and the complete sequence.

The same Executor-Egress adapter serves unmodified HTTP/SOCKS clients. Under Bubblewrap it creates one endpoint-authenticated AF_UNIX Egress listener per execution and selects NETWORK_MEDIATED_PROXY. Bubblewrap retains a fresh network namespace. The Executor-owned protocol-blind relay may use loopback and AF_UNIX, while a workload-only seccomp layer permits only AF_INET/AF_INET6 socket creation in that route-less namespace. Thus a Unix socket exposed by a user bind mount cannot bypass Egress. The backend mounts the private Egress socket path after all user mounts, starts the relay before the workload, and injects proxy URLs through a secret-free launcher argv path.

Under Seatbelt the adapter instead starts an authenticated Egress TCP listener on a random 127.0.0.1 port. The backend admits outbound traffic only to the corresponding localhost:PORT SBPL endpoint. Because that Seatbelt token also covers ::1, the adapter binds the same IPv6 port without listening before it exposes credentials; a second host service cannot occupy the admitted port. No relay runs on macOS. Native tests prove Egress reachability together with denial of a neighbouring loopback port and direct external traffic.

The relay has bounded bidirectional buffers and connection slots but no credential, allowlist, DNS or protocol parser. Egress alone assigns the endpoint-bound principal while parsing HTTP CONNECT/SOCKS, applies the sealed destination decision, resolves/connects, guards requested TLS SNI, enforces the per-principal connection quota and emits byte/closure receipts. A private AF_UNIX bridge carries no bearer secret: the mode-0600 listener path and its unchanged device/inode identity select one immutable principal, and same-EUID peer checking stays an additive host condition rather than the authorization source. The TCP Seatbelt and fd-4 frontends keep their explicit credential contracts. See egress-bridge.md.

Termination and receipts

Local shutdown performs a bounded graceful rung followed by a bounded forced rung. If containment cannot be observed, it returns an error and retains the handle; local-only release can deliberately abandon tracking without blocking.

Receipts are immutable snapshots, not cryptographic attestations. The plan digest is SHA-256 over the canonical plan representation. Backend name/version and capabilities describe the enforcement path selected by this process; they do not prove a remote host or kernel state.

Not claimed in 0.40.0

  • policy correctness;
  • TLS termination or content inspection in Executor core;
  • cgroup quotas for native workloads or a comprehensive syscall allowlist;
  • kernel-vulnerability protection;
  • complete supervision of descendants that escape a process group;
  • remote execution attestation;
  • arbitrary or caller-supplied guest syscall policies;
  • persistence of writes made through the ephemeral root overlay;
  • exact usable payload bytes equal to the tmpfs ceiling (metadata consumes part of the bounded filesystem);
  • native fd-4 descriptor mediation across a VM boundary;
  • safety after a trusted caller authorizes sensitive mounts or inherited credentials.

Release supply chain

Release integrity is a separate boundary from process confinement. A signed tag authorizes publication, SHA-256 checksums detect changed downloads, and GitHub/Sigstore attestations bind artifacts to this repository's release workflow. These mechanisms establish origin and provenance; they do not prove that a backend, kernel or policy is vulnerability-free. Verification commands and the build/publish permission split are documented in release-integrity.md.