Tools, techniques, and procedures to write secure Dockerfiles
Docker is a familiar name by now. It has been instrumental in streamlining and improving the workflows of developers, operations, and other engineering teams. In this article, we are going to learn best practices to write Dockerfiles using BuildKit features, linters, and other tools. We’ll also touch on leveraging OPA (Open Policy Agent) to write custom policies.
TL;DR: this article is based on A practical guide to writing secure Dockerfiles, a presentation that took place at the recent Container Day conference. The talk is available online as a video recording.
What is Dockerfile?
Before learning about Dockerfiles — what is Docker?
Docker is an open-source platform for building, deploying, and managing containerized applications. It has become the de facto standard to build and share apps, from desktop to cloud, including edge devices like Raspberry Pi.
One of the features that have made Docker so popular among developers is the ability to easily pack, ship, and run applications as lightweight, portable, and self-sufficient containers that can run virtually anywhere.
The instructions to build a Docker container image are stored in files called Dockerfile. This is an example of a typical Dockerfile :
A Dockerfile is a text document that contains all the commands a user can call on the command line to assemble an image.
Why Dockerfile security?
If you perform a quick search for Dockerfiles in GitHub, you can see that it returns more than 3 million files.
- Dockerfiles are a blueprint for building your Docker container images
- Dockerfiles are a codified version of your application and infrastructure
- Dockerfiles are among the key components in the entire supply chain security
- Dockerfiles need to be part of your security posture to maintain the highest level of security comprehensively
- Insecure Dockerfiles can cause serious security issues
Best practices to write Dockerfiles
Here is a collection of standard best practices that Docker recommend in their Documentation, as well as community-driven best practices:
- Start with a small version of the image
- Create ephemeral containers
- Understand the build context
- Exclude files from the image with .dockerignore — it works similarly to .gitignore in Git
- Use multi-stage builds to reduce the image size and its attack surface
- Create multi-line arguments in a structured way, and reduce the image layers
- Minimize the number of layers
- Leverage the build cache
- Create your own base image like a golden image
The Docker community recommends a number of other best practices when creating Dockerfiles . For example:
- Order the steps in the Dockerfile from least to most frequently changing content
- Use the COPY instruction to copy only the necessary files. Avoid executing instructions such as COPY . .
- Only install what you need. For example, use the --no-install-recommendsoption
- Group similar commands. For example: RUN apt-get update && apt-get install -y curl
- Remove the package manager cache: rm -rf /var/lib/apt/lists/*
- Use a specific image tag; avoid the latest tag
- Set non-root user and group
- Disallow acquiring new privileges
- Use only trusted and official base images
- Don’t store secrets or sensitive information in Dockerfiles
- Don’t install SSH or similar services that may expose your containers
- Apply image lifecycle management updates, if required
So far we explored a number of standard best practices to follow. Now let’s see how we can apply them in practice in our DevOps workflow
Linters, tools, techniques to validate
We can automate these tasks and checks to enforce them in our workflow, and to ensure the highest level of security.
Let’s start with securing the place where we build the Dockerfiles to create Docker container images.
Say hello to BuildKit
BuildKit is a toolkit for converting source code to build artifacts in an efficient, expressive, and repeatable manner.
- Available from Docker 20.10
- Enabled by default in the latest release (export DOCKER_BUILDKIT=1)
- It significantly improves performance and security
A couple of cool features in BuildKit are its support for securely passing secrets, and forwarding the SSH authentication agent from the host to the Docker build.
BuildKit — Secrets usage in the build (security use case)
Sometimes, developers and organizations use insecure ways to pass secrets and sensitive information to the Dockerfile during build time. For example, they hardcode the data in the Dockerfile, or they pass it via build arguments.
Both examples are flawed: if you hardcode AWS secrets in the Dockerfile, any user or attacker with access to the file has access to the AWS environment. Similarly, if we pass the secrets as build arguments, they are available in the Docker build history, which is easy to obtain and to look up to gain access to the AWS environment.
BuildKit offers a best practice approach to pass secrets to the Dockerfile.
In the example above we pass the AWS secrets via the mount option from the host system. In this way, the secrets are available only during build time; they aren’t stored in the Docker build history or in the Dockerfile.
BuildKit — SSH Socket (security use case)
Organizations hosting their code on private version control systems and running Docker builds in CI/CD pipelines may sometimes use workarounds to pass the SSH authentication credentials to have SSH access to the container build. In the example below, the SSH key is copied to the Dockerfile in an insecure manner.
BuildKit enables passing the SSH socket by mounting it. This forwards the SSH agent from the host in a secure manner.
Learn more about Docker BuildKit on Build images with BuildKit.
hadolint — Haskell Dockerfile Linter
A smarter Dockerfile linter that helps you build best practice Docker images. The linter parses the Dockerfile to an AST, and then it runs rules on top of it.
hadolint is inspired by ShellCheck, which lints Bash code inside RUN instructions.
hadolint is available also online at https://hadolint.github.io/hadolint. However, it is valuable to add these linters and checks to our CI/CD pipelines as part of the deployment workflow.
dockle — Container Image Linter for Security
Container Image Linter for Security. It helps build best practice Docker images. To learn more about dockle, check the GitHub repo: https://github.com/goodwithtech/dockle
dockle performs multiple CIS benchmark checks, as well as more generic checks that are considered recommended best practices, and which we mentioned in the lists at the beginning of the article.
CIS Benchmarks security checks comparison
Generic Checks for Dockerfiles
docker-slim — Minify and Secure Docker Containers
DockerSlim is a project to minify and secure Docker containers. The process doesn’t change anything in your Docker container image; but it minifies it by up to 30x, making it secure too! DockerSlim can do more, besides optimizing images: also help you understand and author better container images. Find out more about the project on https://dockersl.im/.
DockerSlim — Security Profiles
DockerSlim collects application information to optimize containers for security. It also generates Seccomp and AppArmor (potentially SELinux as well) profiles.
Generating the Seccomp profiles may not work for some use cases
- Run the DockerSlim
docker-slim build your-name/your-app
- Use the generated Seccomp profile
docker run — security-opt seccomp:<docker-slim directory>/.images/<YOUR_APP_IMAGE_ID>/artifacts/your-name-your-app-seccomp.jso n <your other run params> your-name/your-app
dive — Explore each layer in a Docker image
dive enables exploring a Docker image, layer contents, and discovering ways to shrink the size of your Docker/OCI image. Find out more about the project on https://github.com/wagoodman/dive.
Dive offers a rich exploratory feature set. For example, it can:
- Show broken Docker image contents by layer
- Indicate what’s changed in each layer
- Estimate image efficiency
- Execute quick build/analysis cycles
- Be included in CI integration
IDE linters and plugins
We write our Dockerfiles in our IDEs; this is a good opportunity to mention some IDE linters and plugins that help us enforce best practices and identify potential security issues in the early stages of our SDLC lifecycle.
For example, this is a Docker linter plugin for Microsoft Visual Studio Code. It enables running linters in Docker containers.
Introducing Open Policy Agent (OPA)
Open Policy Agent (OPA) is an open-source, general-purpose policy engine that unifies policy enforcement across the stack. It is a policy-based control for cloud native environments providing flexible, fine-grained control for administrators. It features a high-level declarative language that lets administrators specify policy as code and simple APIs to offload policy decision-making from their software. Find out more about the project on https://www.openpolicyagent.org.
Rego — OPA policy language
- OPA policies are expressed in a high-level declarative language called Rego. Rego (pronounced “ray-go”) is purpose-built for expressing policies over complex hierarchical data structures
- Rego was inspired by Datalog, which is a well-understood, decades-old query language. Rego extends Datalog to support structured document data models such as JSON
- Rego queries are assertions on data stored in OPA. The queries can be used to define policies that enumerate instances of data that violate the expected state of the system
This is an example of a Rego policy from the Rego Playground for the Role-Based Access Control (RBAC) use case scenario.
Conftest — Tests against structured configuration data
Conftest is a utility to help you write tests against structured configuration data. For example, you can use Conftest to validate Kubernetes configurations, Terraform code, Serverless configurations, or any other structured data. In this context, Conftest helps write validation policies for Dockerfiles.
Conftest relies on the Rego language from Open Policy Agent to write the assertions.
Conftest supports multiple formats of input types, such as:
- YAML
- JSON
- INI
- TOML
- HOCON
- HCL
- HCL 2
- CUE
- Dockerfile
- EDN
- VCL
- XML
- Jsonnet
The sample policy below checks the Kubernetes YAML manifests to verify if the security context of the container is running as root or not, and if it has an app label.
To learn more about leveraging the power of OPA and Conftest, check the Conftest project on https://www.conftest.dev.
Dockerfile security checks using OPA Conftest Rego policies
Let’s take an intentionally insecure Dockerfile example that doesn’t comply with security best practices and standards.
FROM ubuntu:latest
LABEL MAINTAINER "Madhu Akula"
ENV SECRET AKIGG23244GN2344GHG
ENV GITLAB_API_ID gig32oig3bgi34gb43gb43uigb43i
WORKDIR /app
ADD app /app
COPY README.md /app/README.md
ADD code /tmp/code
RUN sudo apt-get udpate
RUN apt-get update && apt-get install -y htop
CMD ["/bin/bash", "/app/entrypoint.sh"]
The snippet is part of the docker-security-checker tool, based on OPA and Conftest. Find out more about the project on https://github.com/madhuakula/docker-security-checker.
Sample Rego policy to check ADD vs COPY
The example below checks if a Dockerfile contains occurrences of the ADD command. If it finds occurrences of ADD, it throws an error, and it notifies users about replacing ADD with COPY.
warn[msg] {
input[i].Cmd == "add"
val := concat(" ", input[i].Value)
msg = sprintf("Use COPY instead of ADD: %s", [val])
}
Running docker-security-checker against Dockerfiles
In the example below, we run a set of custom security OPA Rego policies with Conftest on Dockerfiles to validate best practices and to perform security checks.
Why custom policies?
Most organizations have common patterns across their workflows. Some policies can be specific to the organization. For example, the policy below allows only base images from a predefined trusted source ( exampletrustedregistry.com ).
deny[msg] {
input[i].Cmd == "from"
image := input[i].Value
not startswith(image, "exampletrustedregistry.com/")
msg := sprintf("Base image '%v' is used from untrusted registry", [image])
}
Simple and powerful policies like this help prevent using untrusted images, and they enforce only images from internal private registries.
Want to try it yourself?
I created a simple Katacoda online interactive playground where you can play with the docker-security-checker, OPA policies, and Conftest.
What should I do next?
- Try following best practices when writing Dockerfiles; use linters
- Include these checks in your GitOps workflow, and in your usage of Git hooks
- Create and standardize organization-wide custom policies to make your workflow consistent and predictable
- Add these checks to your CI/CD pipelines to enable and to validate security best practices
- Extend these practices besides Dockerfiles, and implement them in each workflow layer https://medium.com/media/e52b9f88224437d919138d1d160ae4fb/href ### Resources and references
The list below includes a selection of resources and reference I used to create, follow, and learn more about Dockerfile security, as well as the ecosystem around it:
- https://docs.docker.com/engine/reference/builder
- Dockerfile Best Practices talk at dockercon 19
- https://docs.docker.com/develop/develop-images/dockerfile_best-practices
- https://github.com/hexops/dockerfile
- https://engineering.bitnami.com/articles/best-practices-writing-a-dockerfile.html
- https://snyk.io/blog/10-docker-image-security-best-practices
- https://pythonspeed.com/docker/
- https://github.com/moby/buildkit
- https://github.com/goodwithtech/dockle
- https://github.com/hadolint/hadolint
- https://github.com/docker-slim/docker-slim
- https://www.openpolicyagent.org
- https://play.openpolicyagent.org
- https://www.conftest.dev
- https://github.com/madhuakula/docker-security-checker
Thank you so much, Marco Spinello and Ivan Remizov for the review :)
Seems cool, exciting, and fun stuff? Then Join our team at Miro!
Do you like building things and working at scale while we are in hypergrowth? Would you like to be an Engineer, Team Lead, or Engineering Manager at Miro? Check out opportunities to join the Engineering team.
Top comments (0)