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.