The Boundary Is a View, and the Kernel Underneath It Is Shared
Last timeHow It Talks to Anything
A container's isolation is a set of values one shared kernel checks. What that leaves possible, what answers each class of weakness, and when the boundary has to be a kernel of its own.
What the separation is actually made of
Everything taken apart so far has the same shape, and it is worth saying once in
plain terms before asking what it is worth.
A container is an ordinary process. Attached to that process are some values the
kernel keeps about it: which set of host names it belongs to, which set of
process numbers, which set of mounted filesystems, which network stack, which
mapping of user numbers, which accounting group, which privilege units it still
holds. When the process asks for something, the kernel looks at the relevant
value and answers accordingly. That is the whole mechanism. There is no guard
standing between the process and the kernel, no second program inspecting
requests, no wall in any physical sense.
This has a consequence that is easy to state and unpleasant to absorb. The
thing doing the confining is the same program that is answering the process's
requests. A boundary made of decisions is exactly as good as the thing making
them, and here the thing making them is a general purpose kernel of some tens of
millions of lines, serving every process on the machine from the same code. The
separation is real and it is useful. What it is not is independent of the
kernel, and almost every wrong belief about container security is a belief that
some part of it is.
One kernel, one surface
The shared kernel is usually described as a risk. It is more useful as a number.
The interface a process reaches the kernel through is a set of entry points,
somewhere around 350 of them on a current system, though the exact count depends
on the kernel version and the processor it was built for. An ordinary web
service, the 180 MB five-layer image used throughout here, needs about 60 of
them to do its work: open a file, read it, write to a socket, wait, allocate,
and so on. Narrowing what the process may ask for to roughly those 60 leaves
of the surface reachable, about a sixth, and that is a real
reduction worth making. It reduces the number of places a flaw can be reached
from. It does nothing whatever to the consequence of reaching one.
The consequence is the part that does not shrink. A flaw behind one of those
entry points is a flaw in the decision maker itself, so it is not one container's
boundary failing. It is every container behind that kernel failing at once, which
on a host running ten containers from that image is ten boundaries for one
defect. Ten virtual machines in the comparison fixed at the start of all this,
1.2 GB each and 12 GB in total, have ten kernels between them, so the same defect
costs one. That ratio, ten against one, is not improved by any amount of care
taken inside the ten boxes, because none of the care is applied by anything other
than the thing that is broken.
Four classes of weakness, and what answers each
With that established, the classes can be named honestly. There are four, they
are not equally likely, and the most common one is not an attack at all.
A view handed back at start-up. The container is given the host's process
view, or the host's network stack, or a large part of the host's filesystem
mounted inside it, or the undivided privilege set. Nothing is broken in these
cases. The separation was never created. This is the overwhelming majority of
what gets reported as a container escape, and every instance of it was granted
by somebody with a real problem to solve: a monitoring agent that needs to see
the host's processes, a build that needs the machine's own socket, a tool that
needs a device. The reason it keeps happening is that each grant is reasonable
in isolation and the document that records what was granted is never the document
that records what was intended.
A privilege that reaches the kernel. The superuser's power is not one thing,
it is a set of named units, and they can be held or dropped individually. A few
of them allow a process to change the kernel on behalf of the whole machine
rather than on behalf of itself. A process holding one of those is not confined
by anything in the picture above, because the values the kernel checks are
themselves things it can reach. The defence is the one Saltzer and Schroeder
wrote down in 1975: start from nothing and add back by name, with a sentence
attached to each addition.
An identity that means something outside. Without a mapping of user numbers,
the administrator inside the box is the administrator outside it, and the only
thing standing between that identity and the host's files is the filesystem view.
Map the numbers, so that the process which believes it is user 0 is some
unprivileged number on the host, and a view that is widened by accident is
widened onto files that identity cannot read anyway. This is the cheapest of the
four defences and the most frequently skipped.
A flaw in the shared kernel. The residual. Narrowing the request list and
applying mandatory access control both reduce how reachable such a flaw is, and
keeping the kernel current reduces how many are known, and none of the three
changes the blast radius when one is found.
The lesson stops here
5 more paragraphs to go
You have read the opening. The rest of the argument, the problems that check whether it landed, and the lines worth keeping at the end all come with a plan.
The first lesson of every course in the library reads the whole way through, free, so you can see exactly what the rest of them are.
See the planThe contentsThis is the reading half
Starting the course gives you your own copy of it. Every idea on every page has problems standing under it, marked with a reason rather than a tick, and any sentence you do not believe can be opened and argued with. None of that can happen on a page nobody owns.
The contents