Docker Security πŸ˜‡ A hands-on guide to security for Docker

Β· by

Contents

Photo by Andrey Sharpilo on UnsplashPhoto by Andrey Sharpilo on Unsplash

Most companies I have seen deploy Docker images in at least one project or service. Docker is great because it makes stuff reproducible by specifying the environment to a big degree. However, you still have to think about security. Let’s have a closer look!

Host Security

All Docker containers run on a host system. The host needs to be secure AND the container needs to be secure.

There are various vulnerability scanning, auditing, and hardening tools for Linux systems:

  • Lynis: Execute sudo apt-get install lynis && sudo lynis audit system, wait for a couple of minutes, and you get a pretty nice report indicating what you can do to harden your system.
  • SELinux: Provides Mandatory Access Control (MAC) as a kernel module. Thomas Cameron gave an introduction to SELinux. The key point for SELinux and AppArmor is the access control policy. Linux, by default, uses Discretionary Access Control (DAC). SELinux and AppArmor enforce MAC. Learn more about the differences. Luc Juggery gave a nice introduction to SELinux & Docker.
  • AppArmor: Provides MAC as a service. It distinguishes unconfined and confined processes. It ignores unconfined processes. Confined processes may only do what they are allowed to do according to the AppArmor profile of that process. Seth Arnold gave a nice talk about AppArmor 3.0. Again, Luc Juggery wrote a hands-on guide for AppArmor & Docker.
  • Docker Daemon: Run the daemon as a non-privileged user. Especially not as root.

You should run regular checks against vulnerability databases. If they find an issue, you need an effective way to get notified, e.g. by posting to a Slack channel.

You could also use an OS that is optimized for containers, e.g. Google's Container-Optimized OS (COS).

There are many more things to say about the host system, but that is not the focus of this article. If you’re interested, I’ll write a follow-up πŸ™‚

Base Image

The base image is the foundation of your Docker image. Within your Dockerfile, you define the base image with FROM. For me, it typically is python:3.8.7-slim-buster or similar. You need to ask yourself:

  • Do I trust the base image’s author to have good intentions?
  • Do I trust the base image’s author to have a secure development setup so that malware isn’t uploaded unintentionally, e.g. by leaking the credentials to the account or password re-use?

You should also scan your base image for vulnerabilities. Even for very standard images, there are often vulnerabilities. Some can be fixed by directly running an update (e.g. RUN apt-get update && apt-get upgrade), others don’t have an update within the repository. But pretty often you also don’t need all the installed stuff.

Be aware that Alpine only shares vulnerabilities that they have already fixed. So the scan might look better for them, although they are not better. Alpine images are smaller, though. So the attack surface is smaller.

Harden Your Image

Hardening is the process of reducing the attack surface or increasing the difficulty to find and use existing vulnerabilities. It reduces the blast radius any ticking bomb in your system could have.

Copy only necessary files

You can use the .dockerignore file to make sure that some files are not added.

Run as a non-privileged user in the container

By default, the code you execute within a Docker container runs with the user ID 0 — with root. It is recommended not to do that. You can change that in multiple ways:

Within the Dockerfile — I prefer that one:

RUN groupadd -r noroot && useradd -r -g noroot noroot
USER noroot

When you start the container:

$ docker run -u 1000 -it python:3.9.1-buster bash
I have no name!@a70ba4f24042:/$ echo $UID
1000

In Kubernetes via runAsUser in the securityContext (docs).

Multi-Stage Builds

If an attacker gets access to your container, you want them to have as few tools there as possible. Use multi-stage builds for that. Build your code in a build-container and use the built artifact in another container. As a bonus, your image size will be smaller.

The Docker docs give a very good example:

FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]

Harden Your Containers

Read-Only Root File System

This depends on how you run the Docker image, but if you use docker run, you can add the --read-only flag. This makes the root file system read-only. This means that if an attacker gets into the system, they cannot store anything on disk or change any of the executables. They can still change the memory.

You should also be aware that some pretty standard tasks like creating a temporary file obviously don’t work anymore:

$ sudo docker run -it --read-only python:3.9.1-buster
Python 3.9.1 (default, Jan 12 2021, 16:45:25)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import tempfile
>>> a = tempfile.mkdtemp()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.9/tempfile.py", line 348, in mkdtemp
    prefix, suffix, dir, output_type = _sanitize_params(prefix, suffix, dir)
  File "/usr/local/lib/python3.9/tempfile.py", line 118, in _sanitize_params
    dir = gettempdir()
  File "/usr/local/lib/python3.9/tempfile.py", line 287, in gettempdir
    tempdir = _get_default_tempdir()
  File "/usr/local/lib/python3.9/tempfile.py", line 219, in _get_default_tempdir
    raise FileNotFoundError(_errno.ENOENT,
FileNotFoundError: [Errno 2] No usable temporary directory found in ['/tmp', '/var/tmp', '/usr/tmp', '/']

You can work around this issue by mounting /tmp as a volume:

$ sudo docker run -it --mount source=myvol2,target=/tmp --read-only python:3.9.1-buster
Python 3.9.1 (default, Jan 12 2021, 16:45:25)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import tempfile; a = tempfile.mkdtemp()


$ sudo docker run --rm -i -v=myvol2:/tmp/v busybox find /tmp/v
/tmp/v
/tmp/v/tmpbhw8djco

Even better is using a tmpfs mount (an in-memory file system):

$ sudo docker run -it --tmpfs /tmp --read-only python:3.9.1-buster

Limit Capabilities

You can limit the Linux kernel capabilities:

$ docker run --cap-drop all -it python:3.9.1-buster bash
root@3c568219116e:/# groupadd -r noroot
groupadd: failure while writing changes to /etc/gshadow

You can then grant the ones your application needs:

$ docker run --cap-drop all --cap-add CHOWN -it python:3.9.1-buster bash
root@3c568219116e:/# groupadd -r noroot
groupadd: failure while writing changes to /etc/gshadow

In Kubernetes, this is done via capabilities in the securityContext (docs).

no-new-privileges

You might want to always set --security-opt=no-new-privileges. It prevents container processes from gaining new privileges (docs). In Kubernetes, this is called allowPrivilegeEscalation (docs).

Scanning for vulnerabilities

Clair by quay seems to be a commonly used tool to scan containers for vulnerabilities. I haven’t used it so far, though.

Inter-Container Communication

A key thought of “defense in depth” is to make every single step as hard as possible for an attacker. If something is not strictly necessary for the application to run, it is not allowed. Restricting the way the containers communicate with other containers is one part of that.

Scenario how an attacker is blocked by a controlled network communication / inter container communication. Image by Martin ThomaScenario how an attacker is blocked by a controlled network communication / inter container communication. Image by Martin Thoma

Most companies have a lot of different microservices running in containers. Some of the containers need to communicate, others don’t need it. Maybe two have vulnerabilities as shown in the image above. The backend has a vulnerability that allows the attacker to get into the container and another service might suffer from the same issue. But there is no direct way the attacker can communicate with the other vulnerable service and thus harm is prevented.

Have a look at Docker container networking or Kubernetes network policies.

Conclusion

Container Security is an extremely broad field. The NIST Application Container Security Guide is way more extensive than this article; the OWASP Docker Cheat Sheet is of similar length. Tsvi Korren gave a pretty good presentation about container security:

In security, it is hard to recommend what to do. For maximum security, you want to do everything. But a very short and actionable guide would be:

  • Make sure you use a well-known, trusted, maintained base image.
  • Install only software you need, copy only files you use. Try multi-stage builds if you need software to build the software.
  • Use a non-root user.
  • Restrict privileges / inter-container communication.
  • Use a read-only file system.
  • Get a workflow that automatically scans for vulnerabilities and alerts you if anything new was found.

More in this series

In this series about application security (AppSec), we already explained some of the techniques of the attackers 😈 and also techniques of the defenders πŸ˜‡:

The following articles are about to come:

  • Part 18: Secure Messaging πŸ˜‡
  • Part 19: Cryptojacking 😈
  • Part 20: Backups πŸ˜‡
  • Part 21: Cryptotrojans 😈
  • Part 22: Single-Sign-On πŸ˜‡
  • Part 23: Clipboard Hijacking 😈
  • Part 24: Certificates πŸ˜‡
  • Part 25: Race Condition Attacks in Blockchains 😈
  • Part 26: Mobile Device Management (MDM) πŸ˜‡
  • Part 27: Server-Side Request Forgery (SSRF) 😈
  • Part 28: Network Separation πŸ˜‡
  • Part 29: Social Engineering (including Phishing) 😈
  • Part 30: Virtual Private Networks (VPNs) πŸ˜‡
  • Part 31: CSRF 😈

Let me know if you are interested in more articles around AppSec / InfoSec!