Docker security: Battle of the base image

We’re continually looking for ways to ensure that everything we deliver to our clients is secure by default. A colleague recently wrote about some quick wins to secure your Docker containers, and in this post I’ll dive deeper into one of his recommendations: using a stripped down base image.

Alpine has become an extremely popular base for building Docker images, mostly due to its reputation as a minimal image that doesn’t add much weight (only 5MB). This has lead to it being used in a wide variety of use cases, but is it the right image for you? A significant proportion of the Docker images we produce contain nothing more than a JVM-based microservice. Do we really need everything that Alpine gives us just to run a single Java process?

In this post, I’ll put two base images (Alpine and Distroless) head-to-head to see how they perform under attack. This face-off is centred around JVM-based microservices. If you run a different tech stack, I recommend you perform a similar comparison based on your needs.

We work hard to promote secure delivery practices and share knowledge across our global network. Sometimes text doesn’t convey as much as a demonstration, so this post is all about showing the difference between these two base images.

What are you trying to protect against?

Equal Experts consultants are well-known for issuing the challenge: “What problem are you trying to solve?”

Security is much the same with: “What are you trying to protect against?” Both of these questions are designed to make sure you’re solving a real problem and not something fictitious or theoretical. It also gives you something to test your solution against.

So, what are we trying to protect against by using a minimal base image? We’re trying to protect against an attacker being able to easily expand their attack further into the environment in the event of a compromise. We’re trying to limit the damage that can be caused if an attacker did manage to compromise our application. This is one of many approaches to defence in depth, which basically means we put multiple layers of security in place because at some point one layer will probably fail.

Presumably you’re already aware that Docker shares the kernel with the host. So that raises the question… what do I really need in my container? For most JVM-based apps, all you need is the JVM (and by implication, only those things that are strictly necessary for the JVM to function correctly).

The test

I created a very basic Spring Boot app containing a single controller that simulates a command injection vulnerability, similar to the Struts2 vulnerability that affected Equifax. Strictly speaking, the Struts2 vulnerability was a remote code execution vulnerability, but these are often used to invoke operating system commands by instantiating a Process. In our demo app, we have a single controller that accepts a cmdparameter, which allows the attacker to provide a command that will execute within the Docker container.

I packaged up the app into two Docker images: one based on Alpine JRE and the other based on Distroless. I then issued a number of different requests to show what an attacker might do, and it quickly becomes obvious which base image provides greater protection against this particular threat.

Now onto the demonstrations (you may want to run it full screen so the text isn’t truncated).

Alpine

Distroless

Choose wisely

The objective here isn’t to say “Distroless is better than Alpine”. In some cases it might be and in others it might not. The key takeaway is that you should always know what you’re trying to protect against and test your solutions against that threat – particularly under the knowledge that other layers of defence will fail.

I hope this has helped illustrate why seemingly small decisions (like choosing a Docker base image) matter. The key thing to take away from this is that there are different kinds of minimal base image, and you should choose carefully. Always consider whether you’re including more than you need in your Docker image… much like you shouldn’t import Java dependencies unless you really need them.