So your organization uses containers, and you’ve got a basic handle on what that means and why they’re being utilized. Frequently, as security professionals, we’ll observe others using the logic that if something is frequently seen as part of a process, it can be considered innocuous. We are all susceptible to getting too far down this road by accepting pieces of container terminology as familiar, without actually knowing how the pieces fit together or the specific roles they play in orchestration. The goal of this entry-level post is to give you a more comprehensive understanding of container runtimes from the ground up, so you can better understand past and present exploits, and look forward more confidently.
Recalling from last week’s blog post, a container at its most basic form is a spawned process that has an associated control group and namespace that limits the resources the process has access to. From this basic principle, we can start building pieces to manage and coordinate these isolated processes.
The mechanisms that make these processes happen are runtimes. Runtimes can be confusing because different runtimes were built for different implementations and levels of abstraction. Some are used for lower-level functions (mechanics of running containers), while some are used for higher-level functions (managing images, APIs, RPC frameworks). They frequently use one another for abstraction depending on where they sit in the overall stack.
Earlier versions of Docker contained high-level and low-level runtime features which were separated into distinct pieces as the project evolved.
For simplicity’s sake, let’s use this post to look at two examples — runC and containerd.
The best known lower-level Docker runtime today is runC. runC was originally developed as part of the Docker engine, but evolved into a separate CLI tool that spawns and runs containers without needing to run the Docker daemon. Remember that containers materialize through Linux namespaces (virtualizing system resources) and cgroups (provisioning system resources). runC manages both of these, and runs commands inside of each. runC is used by Docker to run containers because it already includes the “plumbing code that Docker uses to interact with system features related to containers” [https://www.docker.com/blog/runc/].
containerd — https://containerd.io/
containerd is a high-level runtime and daemon that can be thought of as an API faceplate of types for other runtimes in the stack. While lower-level runtimes like runC handle the process of actually running a container, a higher-level runtime such as containerd handles container lifecycles, image management, and abstracting to a lower runtime. While lower-level runtimes provide mechanisms for building containers, containerd has mechanisms for building container platforms and has an API used by remote applications to provide monitoring and delegation. Behind the curtain, containerd has runC performing the syscalls and more granular work.
In Closing & Next Steps . . .
Understanding the process a container goes through to get from image to live and deployed is critical in taking the first steps toward maintaining a secure containerized environment. Making sense of noteworthy container exploits (like CVE-2019-5736) that allowed for full breakout back up to the host requires a solid understanding of these runtimes, intended functionality, and their capabilities.
Over the next week or so, let’s take a look at some Kubernetes (and lesser known) runtimes, and also how container-shims work!
Want More Container Basics?
Take a look at this blog post:
If you’re incorporating containers into your environment, it’s essential that you understand basic terminology and concepts around containers, Docker, and Kubernetes to make sure you’re following sound development, operational, and security strategies.
This post is an excellent 101-level introduction or refresher on basic terminology, concepts, and resources.