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:
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.
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.
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”.
So, what does the improved Dockerfile look like. First, a minimal base image pinned to a version to ensure repeatibility:
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.
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.
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|
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>`
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.
Project Engineer at JYVSECTEC