Harden Docker with CIS – (P3) Docker daemon configuration – Part 1

So in P3 of the Harden Docker with CIS series, I’ll continue with the hardening process of the Docker installation which we setup in the P1. We’ll start with the module two of the benchmark (CIS Docker Benchmark v1.2.0) i.e. Docker daemon configuration. There are seventeen items in total out of which one is “Not scored”, thus it will be not be entertained in detail in this post. In this port we’ll cover eight out of the total sixteen scored items, and the others will be covered in the next part. So let’s begin.

Not Scored

CIS ControlDescription
2.7Ensure the default ulimit is configured appropriately

Scored

CIS ControlDescription
2.1Ensure network traffic is restricted between containers on the default bridge
2.2Ensure the logging level is set to ‘info’
2.3Ensure Docker is allowed to make changes to iptables
2.4Ensure insecure registries are not used
2.5Ensure aufs storage driver is not used
2.6Ensure TLS authentication for Docker daemon is configured
2.8Enable user namespace support
2.9Ensure the default cgroup usage has been confirmed
2.10Ensure base device size is not changed until needed
2.11Ensure that authorization for Docker client commands is enabled
2.12Ensure centralized and remote logging is configured
2.13Ensure live restore is enabled
2.14Ensure Userland Proxy is Disabled
2.15Ensure that a daemon-wide custom seccomp profile is applied if appropriate
2.16Ensure that experimental features are not implemented in production
2.17Ensure containers are restricted from acquiring new privileges

2.1 Ensure network traffic is restricted between containers on the default bridge

By default, all the containers in the default bridge can communicate with each other, without any restrictions. However this can be dangerous configuration if a malicious container is running on the default bridge as the other privileged containers. Thus, ensuring that “Inter container communication” (ICC) is turned off.

To verify if the ICC is turned off/on your Docker installation

[email protected]:~$ docker network ls --quiet | xargs docker network inspect --format '{{ .Name }}: {{ .Options }}' bridge: map[com.docker.network.bridge.default_bridge:true <strong><em>com.docker.network.bridge.enable_icc:true</em></strong> com.docker.network.bridge.enable_ip_masquerade:true com.docker.network.bridge.host_binding_ipv4:0.0.0.0 com.docker.network.bridge.name:docker0 com.docker.network.driver.mtu:1500]
Code language: Bash (bash)

As it is evident, in my Docker installation, ICC is turned on, thus containers can talk to each other, let’s see how this works.

# All the running containers [email protected]:~$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b2dff2170ca2 nginx:alpine "/docker-entrypoint.…" 47 minutes ago Up 46 minutes 80/tcp eager_blackwell b709bf22a35c nginx:alpine "/docker-entrypoint.…" 47 minutes ago Up 46 minutes 80/tcp hardcore_bhabha # IP address and hostname of containers [email protected]:~$ docker exec -it b2dff2170ca2 sh $ hostname b2dff2170ca2 $ ip addr 1: lo: mtu 65536 qdisc noqueue state UNKNOWN qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 24: [email protected]: mtu 1500 qdisc noqueue state UP link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever [email protected]:~$ docker exec -it b709bf22a35c sh $ hostname b709bf22a35c $ ip addr 1: lo: mtu 65536 qdisc noqueue state UNKNOWN qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 26: [email protected]: mtu 1500 qdisc noqueue state UP link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever # Now let's ping and curl one box from the other # Curl 172.17.0.2 from 172.17.0.3 $ curl 172.17.0.2 Welcome to nginx! $ ping 172.17.0.2 PING 172.17.0.2 (172.17.0.2): 56 data bytes 64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.058 ms 64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.067 ms
Code language: Bash (bash)

As shown in the command line snippet above, we can ping, as well as curl the other container running nginx. Now let’s turn off ICC and try to do the same.

We can add our settings for the Docker daemon in the /etc/docker/daemon.json settings file, and then restart the Docker daemon with sudo systemctl restart docker and we can verify that if the settings took effect.

To turn off ICC, create daemon.json file it already doesn’t exist and then add the following content

{ "icc":false }
Code language: JSON / JSON with Comments (json)

After restarting the Docker daemon, let’s verify the settings.

[email protected]:~$ docker network ls --quiet | xargs docker network inspect --format '{{ .Name }}: {{ .Options }}' bridge: map[com.docker.network.bridge.default_bridge:true <strong><em>com.docker.network.bridge.enable_icc:false</em></strong> com.docker.network.bridge.enable_ip_masquerade:true com.docker.network.bridge.host_binding_ipv4:0.0.0.0 com.docker.network.bridge.name:docker0 com.docker.network.driver.mtu:1500]
Code language: Bash (bash)

So as ICC has been turned off, now let’s try and ping one container from the other.

NOTE: As we have restarted the Docker daemon, all the containers would have been stopped. So start all the containers (docker start $(docker ps -a -q)) before attempting anything.

$ curl 172.17.0.2 curl: (28) Failed to connect to 172.17.0.2 port 80: Operation timed out $ ping 172.17.0.2 PING 172.17.0.2 (172.17.0.2): 56 data bytes --- 172.17.0.2 ping statistics --- 41 packets transmitted, 0 packets received, 100% packet loss
Code language: Bash (bash)

So now after turning off ICC, containers can no longer communicate with each other.

2.2 Ensure the logging level is set to ‘info’

By default Docker daemon logging level is set to ‘info, however, we’ll be explicit and enable this setting in the /etc/docker/daemon,json file as well.

Add the following to change the logging level to ‘info’

"logging-level" : 'info'
Code language: JavaScript (javascript)

2.3 Ensure Docker is allowed to make changes to iptables

Docker uses iptables to manage networking and other network related configurations, thus Docker daemon should be allowed to make changes to the iptables. By default, this setting is enabled, however, following the suit, we’ll be explicit.

"iptables" : true
Code language: JavaScript (javascript)

2.4 Ensure insecure registries are not used

Insecure registries should not be used as they present a risk of traffic interception and modification. This can be mitigated using TLS communication, and ensuring that only secure registries are used to pull/push the images. By default Docker considers or looks for every registry to be trusted except the local one. The output of the following command should always only list one single registry i.e. local one.

$ docker info --format 'Insecure Registries: {{.RegistryConfig.InsecureRegistryCIDRs}}' Insecure Registries: [127.0.0.0/8]
Code language: Bash (bash)

If there are any registries other than the local one, then that registry should be removed.

2.5 Ensure aufs storage driver is not used

This is an obsolete setting, as these days only overlay2 is used for Docker images and containers. However, it is still important to know how to look for this setting.

$ docker info --format 'Storage Driver: {{ .Driver }}' Storage Driver: overlay2
Code language: Bash (bash)

This is the expected output, if there is anything except overlay2, it should be changed to any of the available ones except aufs such as devicemapperbtrfszfsoverlayoverlay2, and fuse-overlayfs

2.6 Ensure TLS authentication for Docker daemon is configured

To avoid confusion and to ensure best suggested practices, I haven’t delved deep into this topic. Docker‘s official documentation about the topic should serve the purpose and provide up to date information as to how to setup certificates for Docker daemon.

Reference: https://docs.docker.com/engine/security/https/

2.8 Enable user namespace support

The best way to prevent privilege-escalation attacks from within a container is to configure your container’s applications to run as unprivileged users. This can be achieved using user-namespace remapping in Docker. There are a few prerequisites to enable this feature for Docker daemon, else default options can also be used to achieve the same. We’ll use the default option and for detailed process and other intricacies official document can be followed.

Add the following line to the /etc/docker/daemon.json file to enable user namespace remapping.

"userns-remap": "default"
Code language: JavaScript (javascript)

This will create a default (dockremap user) mapping and will be utilized to run containers. We can verify if this mapping was created by running a container (docker run hello-world) and verifying the contents of the /var/lib/docker directory.

~$ sudo ls -la /var/lib/docker/ drwx--x--x 16 root root 4096 Dec 13 11:31 . drwxr-xr-x 24 root root 4096 Dec 13 09:55 .. drwx------ 14 231072 231072 4096 Dec 13 11:30 231072.231072 drwx------ 2 root root 4096 Nov 22 14:30 builder drwx--x--x 4 root root 4096 Nov 22 14:30 buildkit drwx------ 8 root root 4096 Dec 13 11:24 containers drwx------ 3 root root 4096 Nov 22 14:30 image drwxr-x--- 3 root root 4096 Nov 22 14:30 network drwx------ 21 root root 4096 Dec 13 11:30 overlay2 drwx------ 4 root root 4096 Nov 22 14:30 plugins drwx------ 2 root root 4096 Dec 13 11:30 runtimes drwx------ 2 root root 4096 Nov 22 14:30 swarm drwx------ 2 root root 4096 Dec 13 11:30 tmp drwx------ 2 root root 4096 Nov 22 14:30 trust drwx------ 2 root root 4096 Nov 22 14:30 volumes
Code language: Bash (bash)

As it is evident, the files in the folder 231072.231072 is owned by the 231072 user thus limiting escalation attacks from within the containers.

There are a few limitation to this approach as well, and ways to exclude running containers from this mapping. This is particularly true for containers that need to run as root. This can be referred to in the official documentation.

2.9 Ensure the default cgroup usage has been confirmed

Cgroups can be utilized to ration a lot of resources within the host operating system. Thus ensuring that Docker containers are running under a specific cgroup is important. By default Docker utilizes /docker for cgroup driver and system.slice for systemd cgroup driver.

These settings can be changed, on container to container basis using --cgroup-parent flag while initiating a container, or can be changed globally by setting up the values in the /etc/docker/daemon.json file.

#cgroup driver "cgroup-parent": "/foobar" #systemd cgroup driver "cgroup-parent": "a-b-c.slice"
Code language: PHP (php)

This completes the part 1 of our Docker daemon configuration section of the CIS Docker Benchmarks. We’ll continue with other controls in the next post.

If you have questions or need help setting things up, reach out to me @jtnydv