Seatbelt, containers, virtual machines: what each layer actually answers

    Every isolation layer answers a specific failure, and the words cover more than one kind of strength. Knowing which one you actually have is the whole exercise.

    By Piotr Zenon Żabrowski · Published · Updated

    The layers differ in what they share with the host, and that one fact decides most of the rest. A system profile shares the kernel and your user account. A container shares the kernel. A virtual machine shares the hardware. Swapping one for another changes which failure becomes survivable — and the usual mistake is assuming a stronger-sounding word covers a failure it never addressed.

    Which layer answers which failure
    FailureSystem profileContainerVirtual machine
    Writes outside the projectAnswered — allow list with deny defaultAnswered if the mount set is narrowAnswered
    Persistence in your accountAnswered — write denies on start-up and config pathsAnswered while the container is replaced, not if state is mountedAnswered
    Egress and credential exposureNot answered — the process still holds real secretsOnly with egress policy and substitutionOnly with egress policy and substitution
    Kernel bug or a privilege mistakeNot answered — same kernelNot answered — shared kernel, and a privileged or socket-mounted container removes itAnswered, and you pay in speed and setup
    Resource exhaustionNot answered — no CPU, memory or PID boundAnswered if you set limits, which isn't the defaultAnswered by the VM boundary

    What a system profile doesn't do

    • It doesn't stop exfiltration when outbound network is allowed. A readable credential plus one request is enough, and the harness's own model keys are readable by design — the agent has to authenticate with them.
    • It doesn't filter mach lookups in most setups, so system services stay reachable from inside the profile.
    • It doesn't isolate temp storage: /tmp and $TMPDIR stay shared with the rest of the session.
    • It doesn't bound resources. A fork bomb isn't an escape, it's a denial of service, and a read-only profile does nothing about it.
    • It doesn't hide that paths exist — metadata reads still map the disk when content is denied.
    • It doesn't come with a platform guarantee. The mechanism is deprecated and its failure mode is silent.

    The container is a thinner line than it sounds

    A container shares the host kernel, and five settings delete the boundary completely. Four of them are one flag away from the default, which is why they keep showing up in production.

    Defaults and near-defaults that end the isolation story
    What's setWhat it actually grantsWhat to do instead
    Mounting /var/run/docker.sockDocker's own docs say access to the daemon is root on the host; anything that can reach it can start a privileged container with the host filesystem mountedNever mount the socket. If a workload needs to build images, give it a rootless builder, not the daemon
    --privilegedAll capabilities, all host devices, and AppArmor or SELinux relaxed until the container has nearly the same host access as a normal processDrop it. Add individual capabilities back only when something measurably breaks
    Root inside plus a bind-mounted home directoryRoot inside owns the mounted files, so the agent edits your shell config, git hooks and CLI tokens again — now from a container--user with your uid:gid, mount the workspace instead of $HOME, mount config read-only
    The default bridge networkRoutable egress through NAT, which Docker documents as unrestricted: exfiltration isn't blocked, it's relocated--network none, or a logged egress proxy that's the only route out
    The default capability set and no resource limitsNot empty — DAC_OVERRIDE, MKNOD and SETUID are in there — and no CPU, memory or PID caps--cap-drop=ALL, no-new-privileges, --read-only with an explicit tmpfs, --pids-limit, memory and CPU limits

    Worth separating what Docker does for you from what it doesn't, because advice on this is stale in both directions.

    On by default, and only if you ask
    Already onOpt-in, and absent from a bare docker run
    The default seccomp profile, which denies tens of syscalls out of several hundred and is maintained by DockerUser namespaces: available for years, off by default, so bind mounts aren't UID-remapped unless you enable them or run rootless
    The docker-default AppArmor profile wherever AppArmor is loadedDropping capabilities — otherwise you keep the default set
    A private network namespace, which isn't the same thing as restricted egressRead-only root filesystem, no-new-privileges, custom seccomp or AppArmor profiles
    Namespace isolation of process IDsAny resource limit at all: without flags, a container can eat whatever the host scheduler allows

    Two consequences follow. A container running as root with your home directory mounted isn't meaningfully safer than the unsandboxed process, because escaping the namespace was never the attacker's problem — reaching files it could already read was. And a credential passed in as an environment variable is readable by everything inside; an injected instruction doesn't need to break out to use it, it can just send it. Egress is a separate question from isolation, and the cheapest exfiltration channels aren't HTTP at all: DNS carries data in query names, which is T1071.004 in MITRE ATT&CK.

    The tightening list, none of which is the default

    Container flags that change the answer

    • Run as your uid:gid rather than root, so bind-mounted files aren't owned by root on the host.
    • Drop all capabilities and add back only what the workload provably needs.
    • Set no-new-privileges so a setuid binary inside can't win privilege back.
    • Make the root filesystem read-only and mount an explicit tmpfs for temp writes.
    • Cut the network, or force every packet through a proxy you log — and make the proxy the only route out.
    • Keep credentials out of the container entirely: pass a placeholder and let a proxy outside swap in the real value at egress.
    • Set PID, memory and CPU limits so a runaway loop can't take the machine down.
    • Enable user namespaces or run rootless, so container root isn't host root on a mounted path.
    • Never mount the runtime socket, and never go privileged to make a build step work.

    What a virtual machine changes, and what it doesn't

    A VM replaces the shared kernel with a separate one, which is what lets it survive a kernel bug or a container escape. The runtime history is why that phrasing isn't pedantry: runc's CVE-2024-21626 leaked a file descriptor and handed a container process a working directory on the host filesystem, and CVE-2025-31133 was a bind-mount masking flaw in the same lineage. The machinery around a microVM has its own record too — CVE-2026-77179 covers Docker Sandboxes on macOS, where a symlink followed in the shared filesystem lets a guest read or modify arbitrary host files as the VM user, potentially reaching host code execution, with a second issue in the same area covering a guest-to-host relay through a Unix socket. A VM moves the target. It doesn't remove it.

    Be precise about which VM you mean, because the word covers several different strengths. gVisor calls itself a userspace application kernel and explicitly not a virtual machine, and its own security model says a sandbox isn't a substitute for a secure architecture — it intercepts syscalls in a Go process, which is a different trade-off from hardware virtualisation. Firecracker runs five emulated devices and boots in about 125 ms, which is what made per-workload microVMs practical for serverless platforms. Kata Containers runs OCI containers inside lightweight VMs, and Kubernetes picks either one through a RuntimeClass. OrbStack documents that its machines share one kernel and that this isn't a substitute for a full VM against actively malicious code. Apple's container tool gives each Linux container its own lightweight VM through Virtualization.framework, on Apple silicon and recent macOS only. Lima and Colima are VM managers with file sharing and port forwarding, and Colima's default is a single VM for all containers unless you ask for more.

    Sources

    Frequently asked questions

    Does a container make a system sandbox unnecessary?

    No, and the reverse is true as well. A container answers kernel and process isolation and, if you configure it, resource bounds. It doesn't answer credential exposure, egress without a policy, or the agent's own tool surface. A profile answers the write and persistence failures cheaply, on every run, with no build step.

    Are container escapes realistic, or is that just vendor marketing?

    Both exist, and the honest position is that escapes are rare but not exotic: runc has shipped escape-class bugs, and the shared-filesystem layer around a microVM has produced host file access on macOS. The reason layer order matters more than layer names is that the layers fail differently, and those failures aren't correlated.

    If I only have time for one thing, which layer?

    The credential decision, because it holds when the others fail. Then the system profile, because it's cheap and always on. Container and VM hardening earns its keep once the code itself is untrusted and you've already decided where the secret lives.