Optimising and securing Dockerfiles

Writer: Vesa Vertainen, Project Engineer, JAMK University of Applied Sciences May 2nd 2019

In an earlier post, Analysing malicious PDF documents using Dockerized tools, we covered the use of some PDF analysis tools we have Dockerized in the CinCan project. A Docker image can often be made quite easily, but there are things you should take in account to make it a good one. In this post we take a tool called cincan/r2_bin_carver for a critical review, and make it better step by step.

The function of this tool is to carve files from memory dumps using radare2 framework and a python script called r2_bin_carver.py that is downloaded from https://github.com/countercept/radare2-scripts.

FROM radare/radare2:latest

USER root
RUN apt-get update && apt-get install -y \
    git \
    python3-pip

RUN pip3 install r2pipe \
    && mkdir /r2 \
    && cd /r2 \
    && git clone https://github.com/countercept/radare2-scripts.git

VOLUME ["/r2"]
ENTRYPOINT ["/usr/bin/python3", "/r2/radare2-scripts/r2_bin_carver.py"]

At first sight this Dockerfile looks plain and simple, but there is still work to do. Let's examine the file line by line:

FROM radare/radare2:latest

The Docker guide of best practices instructs to use official images as the base of your image if possible. So radare/radare2 is fine, but for the sake of repeatibility it is recommended to rather pin down the version than to use the latest tag. A Minimal base image, such as Alpine Linux, should also be preferred to avoid the installation of any unnecessary packages.

USER root

This one isn't actually needed here, as Docker containers are run as root by default. It is although recommended to run the services as a non-root user whenever possible.

RUN apt-get update && apt-get install -y \

     git \

     python3-pip

This is the correct form of updating and installing necessary packages, but the packages should be pinned to a certain version here as well, and the apt cache should be cleaned with rm -rf /var/lib/apt/lists/* to keep the image smaller. It is also a good practice to sort the packages in alphabetic order.

RUN pip3 install r2pipe \

     && mkdir /r2 \

     && cd /r2 \

     && git clone https://github.com/countercept/radare2-scripts.git

The packages installed with pip should be version pinned too. The directory r2 is propably not needed as git clone creates a radare2-scripts directory anyway. The Docker guide of best practices also suggests using WORKDIR instead of cd.

VOLUME ["/r2"]

The VOLUME instruction specifies a folder to have it's data saved to the host machine and persist also after the container is deleted. In this case it is not really needed, as the container outputs the file to host machine's folder specified with docker run -v.

ENTRYPOINT ["/usr/bin/python3", "/r2/radare2-scripts/r2_bin_carver.py"]

The ENTRYPOINT instruction defines that the container will be run as an executable, executing "python3 /r2/radare2-scripts/r2_bin_carver.py".

Sophisticating the Dockerfile

So, what does the improved Dockerfile look like. First, a minimal base image pinned to a version to ensure repeatibility:

FROM alpine:3.9

To make the image significantly smaller, we changed the base image to Alpine. The latest version available in the Docker hub (at the time of writing) is 3.9. Alpine 3.9 also has Radare2 in it's repository, so we can easily replace the radare/radare2 base image with Alpine, and thus avoid installing a bunch of extra tools and libraries.

LABEL MAINTAINER=cincan.io

It is also a good practice to use LABEL MAINTAINER to inform users about the author of the Docker image.

RUN apk update && apk add --no-cache \

     git=2.20.1-r0 \

     py-setuptools=40.6.3-r0 \

     py2-pip=18.1-r0 \

     radare2=2.9.0-r1

The software packages are now pinned to version, and sorted. The --no-cache option does basically the same as rm -rf /var/cache/apk/*, cleaning up the cache and keeping the image smaller.

RUN pip install r2pipe==1.2.0 \

     && git clone https://github.com/countercept/radare2-scripts.git

The RUN, COPY and ADD instructions add layers to the image increasing it's size, so it is good to combine commands with &&, when possible.

WORKDIR /radare2-scripts

In this case we use WORKDIR for clarity, instead of the line && cd radare2-scripts.

RUN git checkout 6587867fc7f4a8df50b1f940b1dbfa407a42448d \

     && adduser -s /sbin/nologin -D appuser

Here we use git checkout to pin the script to a certain commit, to verify we always have the exact same script to run. The second line creates a non-root user.

USER appuser

ENTRYPOINT ["/usr/bin/python","/radare2-scripts/r2_bin_carver.py"]

CMD ["--help"]

Since the line USER appuser, all subsequent instructions are run as non-root. If no arguments are given, "python r2_bin_carver.py" is run with argument "--help".

So, this is how the final, improved version looks like:

FROM alpine:3.9

LABEL MAINTAINER=cincan.io

RUN apk update && apk add --no-cache \
    git=2.20.1-r0 \
    py-setuptools=40.6.3-r0 \
    py2-pip=18.1-r0 \
    radare2=2.9.0-r1

RUN pip install r2pipe==1.2.0 \
    && git clone https://github.com/countercept/radare2-scripts.git

WORKDIR /radare2-scripts

RUN git checkout 6587867fc7f4a8df50b1f940b1dbfa407a42448d \
    && adduser -s /sbin/nologin -D appuser

USER appuser

ENTRYPOINT ["/usr/bin/python","/radare2-scripts/r2_bin_carver.py"]
CMD ["--help"]

On this comparison chart we can see that the new version is significantly smaller:

base image base image size cincan/r2_bin_carver
radare/radare2 608Mb 759Mb
alpine (3.9) 92.7Mb 5.53Mb

Docker linters

There are also Docker linters, that can be used to help build Dockerfiles the right way. For example hadolint is a very good linter. There are also other linters like dockerlint and lynis, for example. This is what hadolint outputs when ran on the first version of our Dockerfile:

Dockerfile:4 DL3007 Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
Dockerfile:6 DL3002 Last USER should not be root
Dockerfile:7 DL3008 Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
Dockerfile:7 DL3009 Delete the apt-get lists after installing something
Dockerfile:7 DL3015 Avoid additional packages by specifying `--no-install-recommends`
Dockerfile:11 DL3003 Use WORKDIR to switch to a directory
Dockerfile:11 DL3013 Pin versions in pip. Instead of `pip install <package>` use `pip install <package>==<version>`

To sum up

This post hopefully helps you to optimize and to build more secure Dockerfiles. To learn more about Docker's best practices I suggest reading takacsmark's Dockerfile tutorial and the Docker guide for best practices.

You can learn more about our CinCan project at cincan.io. All tools are available for download at gitlab.com/CinCan and hub.docker.com/u/cincan

Original blog post: https://jyvsectec.fi/2019/04/optimising-and-securing-dockerfiles/