Side reading
Containers are not virtual machines
What the boundary actually is, what it stops, what it does not, and how much to trust it on a machine holding your family's data.
"It runs in a container" is used as though it settled a security question. It changes the question rather than settling it.
What actually separates a container from your machine #
Two kernel features, and a handful of smaller ones.
Namespaces decide what a process can see. There are several, each covering a different kind of thing:
| Namespace | What it hides |
|---|---|
| mount | the filesystem. The container sees its image, not your disk |
| pid | other processes. Inside, the container's own process is number 1 |
| net | interfaces, addresses, ports, routing |
| uts | the hostname |
| ipc | shared memory and message queues |
| user | maps user ids inside to different ones outside |
Cgroups account for resources and enforce the limits you configure: memory, CPU, block-device throughput and process count. Defaults are not automatically safe; mem_limit and pids_limit matter because they turn accounting into an availability boundary.
On top of those sit capabilities (splitting root's powers into pieces that can be dropped individually), seccomp (restricting which system calls a process may make at all) and AppArmor or SELinux (mandatory access control policies). Docker applies default profiles for all three, and they are a large part of why the default is not as weak as it might be.
The part people get wrong #
There is one kernel.
When a container makes a system call, it makes it to the same kernel your SSH session is talking to. Namespaces limit what that call can refer to. They do not put a second kernel in the way.
So a kernel vulnerability that allows privilege escalation is potentially a container escape. A virtual machine, running its own kernel on emulated hardware, does not have that property: an attacker would need to break the guest kernel and then the hypervisor.
Practically, on a home server, that means:
- Patch the host. This is the single most important thing, and it is why chapter 3 put upgrades on a timer.
- Do not run images you have no reason to trust. A container from a random registry is code running on your machine with a thinner boundary than you imagine.
- "It's only a container" buys you slightly less worry, not none.
Things that make the boundary much thinner #
Several common flags hand back most of the isolation. Recognise them in a compose file you found on the internet:
--privileged turns nearly all of it off: capabilities, device restrictions, and most of the profiles. Assume a privileged container is equivalent to root on the host. Very little genuinely needs it.
Mounting the Docker socket (-v /var/run/docker.sock:/var/run/docker.sock) gives the container the ability to start other containers, including a privileged one that mounts your root filesystem. Equivalent to root, one step removed. Plenty of popular management tools ask for exactly this.
--net=host removes the network namespace. The container binds ports directly on your host, which also means your firewall rules apply normally again, which is occasionally the reason people do it.
--pid=host lets the container see and signal every process on the machine.
Bind-mounting sensitive paths. -v /:/host is the obvious one. -v /etc:/etc:ro is subtler and still tells an attacker a great deal.
Running as root inside with no user namespace remapping. Root in the container is root on the host for anything it can reach through a bind mount. If a container writes to a mounted directory as uid 0, those files are owned by real root.
What to do about it on a home server #
You are not defending against a nation state. You are defending against automated exploitation of a known vulnerability in something you exposed and forgot to update. Ranked by how much they actually help:
- Keep the host patched, automatically, with reboots you actually perform.
- Run fewer things, and fewer things reachable from outside.
- Pin images to digests and review before bumping, so a compromised publisher cannot push new code onto your machine silently.
- Set measured resource limits. Availability is a security property. Leave headroom and verify that normal upgrades and background jobs still complete.
- Never
--privileged, and treat a socket mount as a decision rather than a detail. - Separate networks, so an application that is compromised cannot see a database it never needed.
- Drop capabilities and run read-only where the image tolerates it. Worth doing one image at a time, after checking it still works across an upgrade, rather than as a blanket change.
The first two are worth more than the rest combined and are the ones people skip.
When you actually want a virtual machine #
If you are running something you genuinely do not trust, or something that needs its own kernel modules, or you want snapshots of a whole machine rather than of a directory, a VM is the right tool and modern hardware makes it cheap enough.
For running a photo library and a file server that you chose deliberately and keep updated, containers are a reasonable boundary and an excellent packaging format.