Container compliance with dockle

Boxing Rules

Article from ADMIN 87/2025
By
The open source dockle tool can help keep your Docker containers compliant.

If you work in cybersecurity, the mention of the word "compliance" invariably means that you are about to be subjected to a few hours of tedious, repetitive work. Ensuring that thousands of resources are compliant is no mean feat. These days, however, I definitely appreciate the need for systems and processes to meet external compliance frameworks and standards.

To-do lists can often help you stay organized, and this is where compliance tools come to the fore. Putting a check mark next to non-compliant resources after you have remediated them is really the only way to ensure consistent security across modern cloud or on-premise workloads and infrastructure.

In this article, I show you how to determine whether your Docker containers are compliant with the use of a fantastic open source tool called dockle [1]. The compliance standard that dockle checks against is the venerable CIS Benchmarks [2]. For good measure, dockle can also offer advice about Dockerfile linting (automatic checks of configuration files). If you need a little help constructing your Dockerfiles, you are gently reminded to look at the best practices page [3] on the Docker website.

On Your Marks

In this example, I look at installing the dockle binary file for Debian Linux derivatives (e.g., Ubuntu). As you can see in Listing 1, the VERSION variable is created to grab the latest version of the software from the dockle/releases/latest page in GitHub.

Listing 1

Installing dockle

VERSION=$( curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/' ) && curl -L -o dockle.deb https://github.com/goodwithtech/dockle/releases/download/v${VERSION}/dockle_${VERSION}_Linux-64bit.deb
$ sudo dpkg -i dockle.deb && rm dockle.deb

If I comment out the commands including and after the &&, the output offers the version 0.4.13 very neatly, without any other characters. The part I commented out uses this VERSION variable in the URL to download dockle with curl, and, on a separate line, install the .deb package file with the stalwart dpkg package manager.

I walked through the installation procedure in a bit more detail than usual, because other Linux distributions use very similar snippets for installing dockle. Have a look at the GitHub README [1] to confirm that even Windows (as incongruous as it seems) uses curl and the VERSION variable in the same way. However, it downloads a ZIP file containing an .exe file, as you would expect.

You might not be surprised in the slightest to learn that a Docker image also exists, which is how I will use dockle. Doing so also makes running dockle friendly to continuous integration and continuous deployment (CI/CD) pipelines; as you will see in a second, the VERSION variable line ensures always using the latest version.

Containers Scanning Containers

The Docker image approach from the GitHub README is shown in Listing 2. You can see that it uses a format similar to the Debian Linux derivative example in Listing 1. Obviously, you need to change [YOUR_IMAGE_NAME] for the container image you want to scan.

Listing 2

dockle Container

$ VERSION=$( curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/' ) && docker run --rm -v /var/run/docker.sock:/var/run/docker.sock goodwithtech/dockle:v${VERSION} [YOUR_IMAGE_NAME]

To make any editing of this code snippet easier, you can paste it into a file and make the file executable. Rather than declare an image name in the file, though, you can change [YOUR_IMAGE_NAME] to $1, so when you pass an image to your script file, you can name the image to scan. Listing 3 shows how I changed and saved the script to a file called dockle_scan.sh.

Listing 3

dockle_scan.sh

#!/bin/bash
VERSION=$( curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/' ) && docker run --rm -v /var/run/docker.sock:/var/run/docker.sock goodwithtech/dockle:v${VERSION} $1

Before scanning, pull down an NGINX container image from Docker Hub:

$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
1f7ce2fa46ab: Pull complete
[...snip...]

then scan the image, where the image name is passed to the script as $1:

$ ./dockle_scan.sh nginx:latest
Unable to find image 'goodwithtech/dockle:v0.4.13' locallyv0.4.13: Pulling from goodwithtech/dockle
7b26c2f269ea: Pull complete
[...snip...]

The surprisingly detailed output from the dockle command is displayed in Figure 1. The dockle image is pulled down at the start of the process. In case you are wondering, Docker Engine will not repeatedly pull the latest version of the image; it's more efficient than that. The image ID is used to figure out whether it needs to download anything and will skip pulling the image again unless a new image ID doesn't match what is saved locally.

Figure 1: Detailed output from an NGINX scan with dockle.

The output reveals a number of setuid files that will be executed as the owner of the file, irrespective of who runs them. When the owner is root, that can be bad news if the permissions are passed on unintentionally.

The output also mentions best practices around how you tag your images. For example, nginx:latest as an image name is too ambiguous for keeping track of versions and making sure you are secure with the most recent release. I've seen commit IDs from GitHub used as the container image tags, so there's no doubt who or what created that specific image. Also common is the use of in-house versioning as tags, but for personal projects, I tend to use a timestamp, because that pins the image to a specific second (e.g., nginx: 27169871000 ), which is helpful if you create lots of images when testing.

Of more consequence to me, however, is the first warning line that mentions Last user should not be root . This warning from dockle is extremely important if you are running containers without any other well-considered security controls on the hosts that run your containers.

The problem arises when a Dockerfile is created, as shown here. The last line declares the user to be root:

USER: root

If you have created Dockerfiles in the past, you are most likely familiar with such an entry. Should that line exist, to all intents and purposes, the container is running as the root user when a container is spawned from the image. (For the purists, I'll cover the exception to that rule in a second.)

If you haven't guessed already, if the running container is compromised through many of the likely package vulnerabilities or non-compliance issues you've just seen dockle discover, then the underlying host is prone to offering the attacker root user permissions on a plate, which can mean taking over the whole host (and therefore all the containers on that host) or being able to move laterally between containers on the same host to gain access.

Whichever scenario an attacker succeeds in advancing, the results can offer a trove of treasures. The likelihood of then being able to get access to sensitive data through file shares (with full control of the host) and then potentially stealing secret keys that permit access to the cloud infrastructure as a whole, increases exponentially.

Moving from one host to another is also much more likely. After all, hosts need access to other systems within on-premises or cloud infrastructure to operate properly. If the attacker becomes the root user, they get to see anything the host itself can see.

This well-known issue is definitely something to keep in mind. You should certainly get into the habit of ensuring that all of your images use a non-root user at the end of each Dockerfile. I mentioned the exception to the rule a second ago. If you think about how containers are run, you might be able to guess what I'm alluding to.

I have looked at the USER instruction in the Dockerfile that creates the image itself. Without any other intervention from the orchestrator (e.g., Kubernetes) or, in this case, the container engine (Docker Engine), the image will in most cases be run as the root user. However, you should also pay attention to the way the container is spawned from an image as it is deployed. Even if the image wants to run as root, it can be prevented from doing so and blocked from running at all. One mitigating control might include the use of user namespaces, where the user the system sees inside a running container doesn't directly correlate to the actual user (and UID and GID) on the underlying host.

In any case, it is better to construct Dockerfiles without the root user at the end. Simply create and use a non-root user on the host and in the Dockerfile for the image to use, then explicitly add that to the USER instruction every time.

The workflow (Listing 4) means that a user is created on the host (I'll call it nginx , because I used that as an example image); then, early on in the Dockerfile, root user permissions are used to move more sensitive files and install packages that need root user permissions into the image. Once completed, the last instruction is the USER line that instructs the container engine to execute as the nginx user.

Listing 4

Non-Root User Dockerfile

# Choose your poison (or more accurately, your Linux distro)
FROM debian
# Declare the USER, UID, and GID you want to use in the container
ARG USER=nginx
ARG UID=1001 # The UID of the "nginx" user you created on the host
ARG GID=$UID # Often the same as the UID
# Run some commands that need root user permissions (e.g., run a package update with "apt")
RUN apt update && apt install -y netcat
# Create your user (you may also want to "chown" files within and also declare a WORKINGDIR so that the user can read files in the directory you want the user to access after this)
RUN groupadd --gid $GID $USER && useradd --uid $UID --gid $GID -m $USER
# Finally, always execute as the non-root user, which is "nginx" in this case, and never the root user
USER $USER

In hand with creating more sensible, secure images, you then also have security controls preventing developer mistakes (and commonly vendor mistakes, too!). The underlying hosts are protected from compromise through root-level permissions if an image tries to run as the root user.

The bare-bones version shown in Listing 4 required to fix the root user image issue is relatively straightforward when you have been shown what to do. This code should work (with a few tweaks) in most containers that run Debian Linux derivatives, such as Ubuntu. Make sure you understand the steps involved and fine-tune the Dockerfile to your requirements. The step I have missed before this is creating a user on the underlying host beforehand so the container user (in this case nginx ) maps perfectly to the host's user.

The command to request a specific UID for a new user is

$ useradd -u 1001 nginx

which should also mean that the same GID is used by the new user as the declared UID. If you make a mistake, just run this command carefully:

$ userdel <username>

You can add the UID and GID to your Dockerfile as you like.

CI/CD Integration

Moving on, there's a number of pieces of functionality that I haven't covered with dockle, such as the CI/CD integration functionality. Listing 5 shows the example provided for a GitHub action failing on a WARN check level. (The dockle software comes with a variety of warnings (Table 1) that should help you grade the level of concern for a scanned image.)

Listing 5

Example GitHub Action

- uses: goodwithtech/dockle-action@main
  with:
    image: 'target'
    format: 'list'
    exit-code: '1'
    exit-level: 'warn'
    ignore: 'CIS-DI-0001,DKL-DI-0006'

Table 1

Warning Levels

Warning Description
FATAL An unwelcome warning; investigate further.
WARN Ensure you are confident that the issue highlighted is either being mitigated to remove the risk or you fully understand the risks.
INFO Informational, including potential performance hits, among other effects.
SKIP Potentially, the target to be scanned isn't found.
PASS No issues discovered.

Dockle has further links in its README to example code snippets about using the tool in popular continuous integration services, including Travis CI, Circle CI, and GitLab CI.

Buy this article as PDF

Download Article PDF now with Express Checkout
Price $2.95
(incl. VAT)

Buy ADMIN Magazine

Related content

comments powered by Disqus
Subscribe to our ADMIN Newsletters
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs



Support Our Work

ADMIN content is made possible with support from readers like you. Please consider contributing when you've found an article to be beneficial.

Learn More”>
	</a>

<hr>		    
			</div>
		    		</div>

		<div class=