We’ve been building container images for quite some time (read almost decades) now. Although tooling, editor, and IDE support have matured, we should not forget about hardening container images in the inner loop. This post demonstrates how to use Dockle to lint your container images according to security best practices.

Why should we lint container images

We’ve to talk about the „why". Why should we lint container images? We should lint container images to

  • enforce security best practices for every container image
  • minimize attack surface by hardening individual container images
  • prevent ourselves from using anti-patterns

Does linting replace vulnerability scanning

Now that we know why one should lint container images, you may ask yourself if local linting could replace vulnerability scanning (typically invoked once the container image gets pushed to a particular container registry).

The simple answer to this question is no. Most linters (and Dockle is no exception here) do not scan dependencies. That’s why one should always combine linting with vulnerability scanning.

What is Dockle

So you may already guess that Dockle is a linter for container images. Compared to other linters in that space, Dockle gives us the confidence that our container images have been built according to well-known, proven security best practices. For example, Dockle checks many best practices specified as part of the CIS benchmarks. If you want a fine-granular overview of all best practices checked by Dockle, look at the checkpoint details published on the Dockle repository. It’s also important to memorize that Dockle does not lint Dockerfiles. It lints container images.

Install Dockle

Now that we have a common understanding of what Dockle is and what it does, it’s time to try it. We have to install the dockle -CLI before we can scan a container image.

Install Dockle on macOS

On macOS, we can easily install the dockle-CLI with brew:

# install dockle-CLI
brew install goodwithtech/r/dockle

Install Dockle on Linux

The installation process on Linux differs a bit depending on the Linux distro you use. For demonstration purposes, let’s take a look at how to install dockle-CLI on an Arch-based Linux using the Arch User Repository (AUR):

# clone the repo
git clone https://aur.archlinux.org/dockle-bin.git

cd dockle-bin

# build and install the package
makepkg -sri

If you’re not running Arch Linux, look at the dockle repository. It contains detailed installation instructions for all popular Linux distros.

Install Dockle on Windows

Last but not least, you can install dockle-CLI also on Windows by following the steps in this snippet:

# find the latest dockle version and download the release
VERSION=$(
 curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | \
 grep '"tag_name":' | \
 sed -E 's/.*"v([^"]+)".*/\1/' \
) && curl -L -o dockle.zip https://github.com/goodwithtech/dockle/releases/download/v${VERSION}/dockle_${VERSION}_Windows-64bit.zip

# Extract and delete the ZIP archive
unzip dockle.zip && rm dockle.zip

Lint container images locally

Now that we’ve installed dockle-CLI on our machine, it is time to lint some images. If you’re already using container technologies, the chances are good that you have some container images sitting on your local machine. However, for demonstration purposes, let’s create a simple web server image:

FROM nginx:alpine
EXPOSE 80

Build the container image using docker build . -t test:latest Next, we use dockle test:latest to lint the container image to see where we can improve:

# lint container image
dockle test:latest

WARN  - CIS-DI-0001: Create a user for the container
    * Last user should not be root
WARN  - DKL-DI-0006: Avoid latest tag
    * Avoid 'latest' tag
INFO  - CIS-DI-0005: Enable Content trust for Docker
    * export DOCKER_CONTENT_TRUST=1 before docker pull/build
INFO  - CIS-DI-0006: Add HEALTHCHECK instruction to the container image
    * not found HEALTHCHECK statement

As you can see, we’ve several findings here. Although we can address most of the findings by updating our Dockerfile, CIS-DI-0005 must be addressed by configuring your local installation of Docker to sign container images (Docker Content Trust). We can also address DKL-DI-0006 by building our image with a tag different from latest. First, let’s address all findings that we can solve in the Dockerfile. Update the Dockerfile as shown in the following snippet:

FROM nginx:alpine
EXPOSE 80

# Add health check to address CIS-DI-0006
HEALTHCHECK --interval=30s --timeout=2s --start-period=5s --retries=3 CMD curl -f http://localhost/index.html || exit 1

# Add a dedicated user
RUN addgroup -S samplegroup && adduser -S sampleuser -G samplegroup \
 && mkdir -p /var/run/nginx /var/tmp/nginx \
 && chown -R sampleuser:samplegroup /usr/share/nginx /var/run/nginx /var/tmp/nginx

# Copy custom NGINX configuration to the image
COPY nginx.conf /etc/nginx/nginx.conf

# Switch user context to address CIS-DI-0001
USER sampleuser:samplegroup

This version of our Dockerfile references a custom nginx.conf add the following nginx.conf to the same folder:

worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid    /var/run/nginx/nginx.pid;
events {
  worker_connections 1024;
}
http {
  client_body_temp_path /var/tmp/nginx/client_body;
  fastcgi_temp_path /var/tmp/nginx/fastcgi_temp;
  proxy_temp_path /var/tmp/nginx/proxy_temp;
  scgi_temp_path /var/tmp/nginx/scgi_temp;
  uwsgi_temp_path /var/tmp/nginx/uwsgi_temp;
  include    /etc/nginx/mime.types;
  default_type application/octet-stream;
  log_format main '$remote_addr - $remote_user [$time_local] "$request" '
           '$status $body_bytes_sent "$http_referer" '
           '"$http_user_agent" "$http_x_forwarded_for"';
  access_log /var/log/nginx/access.log main;
  sendfile    on;
  keepalive_timeout 65;
  include /etc/nginx/conf.d/*.conf;
}

Now let’s build a new version using docker. We also specify the tag as 0.0.1 to address DKL-DI-0006. Once the build has finished, lint the container image again with dockle:

# build the container image
docker build . -t test:0.0.1

# lint test:0.0.1 with dockle
dockle test:0.0.1
INFO  - CIS-DI-0005: Enable Content trust for Docker
    * export DOCKER_CONTENT_TRUST=1 before docker pull/build

Finally, let’s enable content trust and build version 0.0.2 of our container image. (You may want to read this article to dive deeper into Docker Content Trust (DCT))

# enable content trust for the current terminal session
export DOCKER_CONTENT_TRUST=1

# build test:0.0.2
docker build . -t test:0.0.2

# lint test:0.0.2 with dockle
dockle test:0.0.2

At this point, you should not see any findings being prompted by dockle, which means our image has successfully passed the linter.

What we have covered today

  • 💡Understand why container images should be linted
  • 🔹Learned what Dockle can do for us
  • 🖥 installed dockle-CLI
  • 🧊Build and linted several versions of a container image

Recap

As we have seen in this article, linting container images with Dockle will discover weak container images and provide detailed information about how to improve, harden, and optimize our container images.

Continuously using Dockle to lint your container images will not just help you secure your container images. Dockle also helps you in learning techniques for solving critical incidents.