Secure-by-design Docker Compose
You may or may not be aware but Docker has some security options by default, which is good. You may also be aware that many Docker images on Docker Hub are woefully insecure, and provide very little or no meaningful security boundaries between the code running in those images and the machines they run on.
Thankfully, Docker provides some features that can help mitigate some of these, and you can leverage them in your Compose files without having to rebuild images!
Let's look at some low hanging fruit in that regard. We will look at how to tighten the default options for your containers, and for each one I will show you how to loosen those options as needed.
Note: This guide is aimed at developers who do not specialize in container development. If you're a security engineer or work on containerization runtimes directly, this is probably stuff you already know
First, here's the TLDR:
# compose.yml
# Define a custom YAML anchor with all the hardening options
x-common-hardening-opts: &hardening
security_opt:
- "no-new-privileges=true"
cap_drop:
- ALL
user: "${UID:-1000}:${GID:-1000}"
runtime: runsc
services:
my_service:
<<: *hardening # Include the anchor
Let's break this down:
YAML Anchors
Firstly, let's look at that syntax. I'm describing a YAML anchor which is basically a reusable snippet, and I include it on every service I run. This is how I achieve the "by default" part of "secure by default".
security_opt
This parameters lets you set seccomp options for the container. The `no-new-privileges=true` option prevents the processes in the container from switching users, which will make privilege escalation much harder.
You can read more about the options available in Docker here.
Caveats: If processes in the container need to switch users at runtime, they won't be able to.
To disable this:
# If your container needs to change users, disable this option like so
service:
my_service:
<<: *hardening
security_opt: []
cap_drop:
Docker runs on Linux, always (on Windows, Docker actually runs a virtual machine on which it runs its containers). The Linux kernel has a notion of capabilities which define what a process may or may not do on a system. Some of these are used to leverage attacks on a system.
Docker makes some assumptions about what capabilities your average container needs. You can see the capabilities they add by default here: https://github.com/moby/moby/blob/master/oci/caps/defaults.go#L6-L19
That said, if we want a secure by design method of deploying Compose projects, we should follow the least privilege principal. The `cap_drop` feature comes in handy here, allowing us to drop capabilities from the container, and in this case we are dropping ALL capabilities.
Since many processes won't work with zero capabilities, the idea is to try to run the containers with none, then re-enable them as needed. For example, Caddy needs the following:
service:
caddy:
<<: *hardening
cap_add:
- NET_RAW
- NET_BIND_SERVICE
- NET_ADMIN
In this way, the container only has the minimum set of capabilities it needs to do its job.
Caveats: Enumerating over the default enabled capabilities and adding them one-by-one is tedious.
To disable this:
# If you want the default capabilities enabled on a service
service:
my_service:
<<: *hardening
cap_drop: []
领英推荐
Running as anybody other than root
Many Docker images run as the root user when their containers run code. This is obviously woefully poor practice for security concerns.
The macro above sets the user to the `$UID` and `$GID` of the environment Docker's running in, with `1000:1000` as the defaults (by default this is the first user on the system - usually your user).
Since Docker Compose picks up environment variables from dotenv files found in the project's work directory, you can easily update them like that:
# .env
UID=65532
GID=65532
With this, Docker will run processes with the `$UID` and `$GID` set to 65532
Caveats: Some images don't play nicely when running as unexpected users. They might try to interact with parts of the filesystem they ship with that have restricted privileges.
To disable:
# If want to let the container run as the user it was designed to run as (or root)
service:
my_service:
<<: *hardening
user: ""
runtime: runsc (gVisor)
As mentioned earlier, Docker provides very little meaningful security boundaries, and one reason for that is that process in the containers interact directly with the host kernel. If there's an exploitable bug in the host's kernel, a process in your container and try to conduct a container escape leveraging it.
That's where gVisor comes in. Google created this alternative runtime for Docker that secure shims syscalls to the kernel. Processes no longer interact directly with the host kernel, but with the shims provided by gVisor.
To install gVisor, run the following commands (as documented in their getting started page):
To download and install the latest release manually follow these steps:
(
set -e
ARCH=$(uname -m)
URL=https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH}
wget ${URL}/runsc ${URL}/runsc.sha512 \
${URL}/containerd-shim-runsc-v1 ${URL}/containerd-shim-runsc-v1.sha512
sha512sum -c runsc.sha512 \
-c containerd-shim-runsc-v1.sha512
rm -f *.sha512
chmod a+rx runsc containerd-shim-runsc-v1
sudo mv runsc containerd-shim-runsc-v1 /usr/local/bin
)
To install gVisor as a Docker runtime, run the following commands:
/usr/local/bin/runsc install
Then restart the Docker service.
gVisor is its own beast and going into detail about that here is out of scope, but I will say it has a lot of limitations with regard to networking and particularly DNS.
Caveats: DNS can be a nightmare
To disable and just use the default runtime:
# If want to let the container run as the user it was designed to run as (or root)
service:
my_service:
<<: *hardening
runtime: default-runtime
Conclusion
Docker bad. Just kidding (sorta), but it is important to be mindful of what you're running on your system, that Docker isn't a security boundary, and that you have mitigation tactics you can employ to protect your system.
Good luck!
Kubernetes Specialist | Platform Engineering Consultant | Bar Owner at Turbo Haüs | Technically a Cyborg
4 个月Very well written and informative thanks for putting this together
Software / Ops / Security / FOSS
4 个月If you think this article was interesting, you might find something else interesting: me! And, interestingly enough, I'm looking for work! So if your team is looking for developer who is mindful about security or is just really funny, you should hit me up ??
Sr. Community Manager @ G2i │ Promoting developer health through community
4 个月So well-written, Martin ▽ Sweeny!
Software / Ops / Security / FOSS
4 个月If anybody has anything they'd like to share - especially in the way of corrections or improvements - feel free to correct me :)