We will attempt to cover some of the basic security considerations of running an application (in this case, a Drupal site) within a Docker container. This is intended as an introduction to the concepts of container security for somebody who may be more familiar with the security of a virtual server, and should not be considered a complete guide.
Much of the discussion around Docker security is focussed on the lower levels: kernel namespaces for process isolation, control groups for resource isolation, and comparisons with tradition VMs for hypervisor/host isolation.
I will attempt to summarise some of the more frequently discussed security features of Docker, and Linux containers in general, along with some other areas that should be considered as part of a security strategy for containers.
Possibly the most important concept of containers in general, Namespaces are a feature of the Linux kernel that can isolate resources or processes. Some examples of resources that can be virtualised and isolated include process IDs, user IDs, hostnames, network access, and filesystems. Namespaces are a fundamental aspect of containers on Linux. At its simplest, this means that processes running in container A cannot see processes running in container B, or on the host system itself.
With namespacing, each container gets its own network stack, and doesn’t get privileged access to the sockets or interfaces of any other containers or the host. It is still possible for containers to communicate between themselves, depending on how the host system and the inter-container networking layer is configured. Traditionally this was configured through explicit ‘links’ defined between containers that need to communicate with each other, but is handled differently depending on your Docker version and orchestration software. In general, all containers on a Docker host are using bridge interfaces, similar to physical or virtual machines connected via an Ethernet switch, and will be assigned a unique IP address.
Control Groups (or cgroups) is another Linux kernel feature that is a core component of containers. They implement resource accounting, limiting, prioritisation and control. As well as providing useful metrics and configuration, they also allow for control over container memory, CPU, and disk I/O resource usage, ensuring that no single container can exhaust one of those resources (if configured correctly!).
While cgroups are not involved in preventing one container from accessing the data and processes of another container, they can be used to prevent some basic denial-of-service attacks and to control misbehaving applications.
Most traditional servers will be running a lot of processes as root, such as cron, SSH, syslogd, network configuration tools, a mail server, etc. A container, in general, does not need to do this, as these tasks are (or can be) managed outside of it. Logging and mail is generally encouraged to be offloaded to a remote system or service when using containers, and cron should either be run as a user process specific to the task(s) required, or handled by ‘docker exec’ commands outside of the container. In many cases, containers will not need any true root privileges at all, which means that the container can run with a reduced set of permissions.
The security concept of ‘Capabilities’ allows for fine-grained access control that goes beyond the basic ‘privileged’ and ‘unprivileged’ system. If a service needs to bind to a low port (<1024), that does not necessarily mean that it needs to be run as root. The CAP_NET_BIND_SERVICE privilege can be granted specifically to the process (or thread) that requires it, without inherently granting superuser privileges to the same process. The man page has a complete list of available capabilities.
By default, Docker containers are run a restricted set of capabilities, which is important for container security. Default containers cannot, for example, run another Docker daemon within themselves, as they are not allowed to access any devices of the host directly. You should take a look at this default list of capabilities and drop as many as possible. If you insist on running a --privileged container, be sure that you understand what this means and that you have a very good reason for doing so.
The best practice for running containers would be to explicitly remove all capabilities except those which are 100% required for the processes being run within the container. This can be a powerful security tool, as even if breached you can limit the damage that can be done from within your container by disallowing things such as ‘mount’ operations, alterations to filesystems, module loading, and plenty of other scary things.
Running containers with Docker means that you’re running the Docker service itself somewhere, either on a physical server or a VM. This service requires root privileges and could be considered the most vulnerable part of your system if not handled carefully.
Only trusted users should be allowed to access your Docker daemon. Anybody who can launch containers has the ability to modify the host filesystem. If you are wrapping the Docker daemon with a web service or control panel, be extremely careful with access controls, APIs, and parameters.
The Docker server itself should ideally be running nothing except Docker, with other services moved into containers. You probably won’t be able to avoid running the usual core tools such as monitoring and an SSH server off there, but try to keep it simple.
Security of your Docker server is critical, but outside the scope of this brief introduction. This does mean that Docker security itself is an extra layer of problems that you need to think about on top of traditional security, not a replacement for it.
In practice, the average Docker container hosting a complex PHP/MySQL website is likely to have almost as many services and dependencies as a hardened VM, especially if your container images are built on top of a larger distribution such as Ubuntu or CentOS. This means that you still need to follow the usual security practices that you would for a VM: you will need to ensure that both your base and application image layers are kept up to date, and that you trust whatever images you’re inheriting from. It may be worth considering using a more minimal OS such as Alpine or Container Linux (formerly CoreOS Linux) inside (and outside) your containers, but again only if you consider their level of security support to be acceptable. You are likely to drag in fewer dependencies by building on top of a minimal OS, but you cannot avoid the dependency chain of your application itself. It is sensible to aim for as small a footprint as possible for your container, just as you should for your VMs. The fewer components you have, the less patching you’ll need to do.
Patching and updating your containers is likely to require a different approach to that of a VM, unless you already follow an “immutable server” workflow of some kind, and are familiar with rebuilding base VM images whenever updates are required. Traditionally, updates would be applied to running systems and ideally not break the application in the process. With containers, this is not a realistic option as the application and dependencies are more tightly integrated and often “baked in” to the containers themselves. Ideally, it will not even be possible to alter a running container, as you will have implemented a restricted filesystem for security reasons. You will need to rebuild your container for every update, and possibly every configuration change. It’s important that you have a reliable and automated way of pushing such changes through to production without disruption, or maintenance of your containers will quickly become a burden.
While Docker does isolate many parts of the underlying host from an application running in a container, this separation is not as strong as that of a traditional Virtual Server which does not share a kernel with the host. This makes container security Serious Business, and not something that you should take for granted just because your application is tucked away inside a magical container.
On the positive side, Docker containers are relatively secure “out of the box”, so long as you’re careful with the capabilities that you grant them and avoid running the processes as a non-root user and follow the “traditional” system security practices that you’re used to with a VM. They provide integration with the powerful security features of the Linux kernel, and countless tools and tutorials to help people make use of them.
- Containers allow for (relatively) simple isolation of applications that would traditionally run alongside each other on the same host, or require individual servers (which can be inefficient and therefore expensive). Each application only has access to the files and ports explicitly exposed or allowed to it.
- Containers encourage us to treat servers as disposable, instead of unique and precious "snowflakes" that may have been running for years with many (often undocumented) modifications.
- Containers encourage repeatable automation and scripting as part of their workflow, which can minimise the effort of patching and updating.
- Assuming each container is only responsible for a single task and only has access to the data and networking required for that task, this can help to limit the damage should a container be breached.
- Container isolation is not (generally speaking) as robust or complete as the separation provided by type 1 VM hypervisors.
- Running large swarms of containers hosting an application can potentially lead to varying security patch levels between these containers if nothing is directly enforcing their state and versioning.
- You still have to manage the traditional security of the VM or physical server(s) that are hosting your containers, so containers have not replaced this.
- Still a relatively new technology, with many new challenges not just to security.