In this article, I would like to present seven ways to build secure and easy-to-maintain Docker containers. Of course, there are more optimization possibilities than the seven I describe here. However, using the ones listed here is already a good approach.
Using a minimal base image automatically reduces the attack surface. Fewer functions mean a reduced number of possibilities that an attacker can exploit in case of a breach.
For example, if curl
or nc
is not installed, an attacker cannot use these tools to download additional malicious code.
A good option for a base image is Alpine Linux. Compared to Debian, it is very small. In a direct size comparison, a current Debian 12 image is 49 MB, while the Alpine image is only 3.4 MB.
In the linked animation, I compare the sizes of the two images. If possible, Alpine should always be used.
When building images in a Dockerfile, always update the already pre-installed packages. This is especially important when using custom base images to speed up the build process.
In this example, I update an Alpine image.
In IT, the principle of least privilege is a standard. After installing the necessary components, switch from the root user to a regular user. Only the minimal necessary permissions should be granted, which are required for the container’s operation.
This ensures that, in the event of a successful attack on the application itself, there is another hurdle that the attacker must overcome. To minimize the attack surface, this should be implemented by default for all Docker containers.
This approach is demonstrated using a minimal Python3 application:
FROM python:3.11-slimRUN useradd -m app_user # Create a new userUSER app_user # Change context to the newly created userCOPY . /appWORKDIR /appENTRYPOINT ["python3", "/app/my_app.py"]
Besides switching the user, it is also possible to set the root filesystem to read-only mode. This ensures that existing binaries in the container cannot be modified by the user.
This creates a double layer of protection to the already reduced permissions from the previous section. For more information, you can find further details here: https://docs.datadoghq.com/security/default_rules/cis-docker-1.2.0-5.12/
By explicitly setting the --read-only
flag, the root filesystem is set to read-only mode. Writing is no longer possible, even with root permissions.
If an attacker gains shell access through a vulnerability in the application, they will not be able to modify existing binaries in the container, even if they break out of the user context and gain root privileges.
Environment variables should not be used for sensitive information such as passwords or API keys, as they are visible in many contexts. For example, they can be easily viewed via docker inspect
on the host system or in log files.
Ideally, secrets management tools such as Docker Secrets, HashiCorp Vault, or similar solutions should be used to increase security. In practice, however, full integration of these tools can be challenging, especially when dealing with legacy code or a rapidly changing system landscape. To consistently implement this approach, the appropriate client component of the secrets management tool would have to be integrated into the application.
Therefore, I still use environment variables for now. My preferred way when using plain Docker is to store secrets in a separate environment file, which I then populate using a secrets management tool.
Secrets should always be externalized and not delivered directly with the source code or Docker image.
By using digital signatures, it is possible to verify the integrity and origin of Docker images. This secures the software supply chain and prevents the execution of tampered images.
This technique is used consistently by Apple in its operating system. The execution of applications that are not signed with an Apple Developer Certificate is blocked by default and must be explicitly allowed by the user.
An example of a successful attack on the software supply chain can be read here: https://www.heise.de/news/VoIP-Anbieter-3CX-Die-doppelte-Supply-Chain-Attacke-8974948.html
With Docker Content Trust, there is a similar system where images can be digitally signed. The host system is then configured to only run signed images.
Multi-Stage Builds are a good way to reduce the size of the image and avoid unnecessary files and dependencies in the images where the application runs. The build process of a Docker image is divided into several stages.
In the example shown here, an application needs to be built with a very specific dependency. In this case, it is the ldap-client
for python3.9.
Since the dependency itself needs to be built, an additional layer is added where the ldap-client
is built. This layer is discarded at the end, and only the compiled application is copied into the application image.
By using the methods outlined here, a high level of security can already be achieved by simply applying rules and principles. Of course, these principles do not replace adherence to rules for creating secure software. However, they can make it significantly more difficult for an attacker to compromise a system.
Netcup bietet virtuelle Root-Server für einen fairen Preis an.
Mit dem Gutschein 36nc17009374344 erhälst du 5€ auf deine erste Bestellung.
Hier bestellen und einen Rabatt erhalten.
Falls der Gutschein nicht mehr funktioniert, bitte in die Kommentare schreiben.
Quick Links