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.
| Failure | System profile | Container | Virtual machine |
|---|---|---|---|
| Writes outside the project | Answered — allow list with deny default | Answered if the mount set is narrow | Answered |
| Persistence in your account | Answered — write denies on start-up and config paths | Answered while the container is replaced, not if state is mounted | Answered |
| Egress and credential exposure | Not answered — the process still holds real secrets | Only with egress policy and substitution | Only with egress policy and substitution |
| Kernel bug or a privilege mistake | Not answered — same kernel | Not answered — shared kernel, and a privileged or socket-mounted container removes it | Answered, and you pay in speed and setup |
| Resource exhaustion | Not answered — no CPU, memory or PID bound | Answered if you set limits, which isn't the default | Answered 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.
| What's set | What it actually grants | What to do instead |
|---|---|---|
| Mounting /var/run/docker.sock | Docker'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 mounted | Never mount the socket. If a workload needs to build images, give it a rootless builder, not the daemon |
| --privileged | All capabilities, all host devices, and AppArmor or SELinux relaxed until the container has nearly the same host access as a normal process | Drop it. Add individual capabilities back only when something measurably breaks |
| Root inside plus a bind-mounted home directory | Root 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 network | Routable 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 limits | Not 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.
| Already on | Opt-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 Docker | User 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 loaded | Dropping capabilities — otherwise you keep the default set |
| A private network namespace, which isn't the same thing as restricted egress | Read-only root filesystem, no-new-privileges, custom seccomp or AppArmor profiles |
| Namespace isolation of process IDs | Any 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
- Docker documentation: engine security (daemon socket privileges, user namespaces off by default), seccomp, AppArmor, resource constraints, bridge driver, none driver.
- CVE-2024-21626 (runc file-descriptor leak), CVE-2025-31133, CVE-2026-77179 (Docker Sandboxes on macOS, virtio-fs symlink).
- gVisor security model; Firecracker; Kata Containers; Kubernetes RuntimeClass.
- OrbStack architecture (shared kernel); Apple container (one VM per container, Apple silicon); Lima; Colima.
- MITRE ATT&CK T1071.004 for DNS as an exfiltration channel.
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.
Related
What contains a CLI agent
Four failure modes, the profile that answers them, and why enforcement can't live inside the agent's own instructions.
Where the credential lives
Two shapes of agent isolation, the one that keeps keys out of the process, and the failure both shapes still miss.
Agent blast radius
A bad model decision becomes an incident through tool calls. Agent memory determines how long and how far the damage can spread.
Cloud security
Multi-cloud security practice: landing zones, identity, posture management, MITRE ATT&CK-aligned detection engineering and CIS hardening baselines.
Agent sandbox hardening
Printable checklist for bounding a local coding agent: profile checks, container flags, credential handling and proof that the boundary works.
Containing compromised agents
An agent will eventually be tricked, mistaken, or hijacked. Five architectural controls decide what it can still do when that happens.