Secure Your Docker Images with Docker Secrets
Add docker secrets to prevent your docker image from leaking passwords
When you leak confidential information into your Dockerfile, you open your image up to all kinds of attackers that can steal your credentials, take control of you container, or inject malicious code into your containers. This article focusses on providing confidential information to your Dockerfile WITHOUT leaving any trace of it in the resulting image.
There will be no confidential information in your image when you follow the simple steps in this article. We’ll go through a full example and at the end you’ll have a complete Dockerfile that implements the solution. Let’s code!
TL;DR? Scroll down to “The correct way — docker secrets”
Unfamiliar with Docker? Check out this article
What problem are we solving?
Sometimes you need to provide your Dockerfile with confidential information like API keys, credentials or certificates, for example to git clone a private repository.
We need to provide this information without “baking” it into the image. If you hardcode credentials into your Dockerfile or copy your .env
files into the image you open your image up for abuse; attackers can extract your credentials or gain access to your containers for example. Even if you remove a file after using it in the Dockerfile; it still exists in the image layers as we’ll find out later.
The solution: Docker secrets
Docker secrets lets you use confidential information in your Dockerfile without leaving any trace of it. We’ll get into how and why this works in the section below. First we’ll set up our demonstration.
Setup demo
For this article we have a real simple app: myapp.py
that only contains one line of code: print('hello’)
. When we Dockerize this script our Dockerfile might look like this:
If the Dockerfile above does not seem familiar to you please first read this article about the basics of docker.
We’ll build the image with docker build . -t "my_app"
and run the container with docker run my_app
. This just prints “hello” on the commandline. Great! Everything works. Let’s introduce our problem.
Adding private stuff
Imagine that our python app wants to make use of a package called privatestuff
that we’ve put on our private pypi. We can install this package with the pip command below.
How can we install this package in our Dockerfile?
The bad way — hardcoding
The first thing you might think of is just to hardcode your credentials in your Dockerfile. Don’t ever do this, this is just asking for problems.
As you see we’ve only added a single extra line on line 4. Here we specify that we want to install the privatestuff
package from my.pypicom/pypi
. In addition to baking the credentials into our image we’re exposing them for everyone who has access to the Dockerfile. Imagine pushing this project (which includes this Dockerfile) to github; everyone will be able to see your credentials!
Also the bad way — build args
Even though this seems like a good option it’s also super bad. Check out the augmented Dockerfile:
As you see we expect three arguments which we’ll provide with the command below:
This will pass the arguments to our Dockerfile but it is very easy to extract them from the layers in the image. We can even see the values when we inspect the docker image’s history:
The command above searches in the docker history of our image (called “my_app”). We extract all lines of text that contain the word PYPI_PASS with grep. This will show us our credentials in plain text!
Slightly less bad but still bad — using and deleting a file
You might come up with the idea to copy over and then delete your a file with credentials. In the example below we copy over .netrc
; pip will use the credentials from this file. Then we remove the file again.
https://gist.github.com/mike-huls/79633dd8ef466d040f8c7c97b1d1cd22
In Docker, deleting a file does not actually remove it from the image. Since Docker cashes its layers; all previous layers are still present in the image. This means that a malicious user can easily extract your .netrc
file.
The correct way — docker secrets
The way we’ll got through here is not only the only secure one, it is also very easy to implement! Since I’m a big fan of .env
files we’ll use one for this example. Using secrets has three simple steps:
1. Create an .env file
Simple enough. We create a file called .env
and give it the following content:PYPI_URL=my.pypi.com/pypi
PYPI_USER=m.huls
PYPI_PASS=supersecretpassword
More info in using environment files here. More info about using env variable files with Docker here.
2. Define our docker build command
We use this command to build our image. Here we pass the file from step 1 to our Dockerfils as a secret.docker build -t "my_app" --secret id=my_env,src=.env .
You can see the command above as: load the .env
file and give it a key called my_env
. We can use this key in the next step to access the .env
.
3. Modify Dockerfile so that it mounts the secret
Here we take the file that and use we passed in the docker build
command:
As you can see we just add one extra line of code to our Dockerfile. We mount
a secret
specifying the id my_env
(which we’ve specified in step 2). Next we source
the content of the .env
which load its content as variables. This is why we can use the variables in the — extra-index-url as specified in the pip install.
The beauty of this approach is that the content of the .env
file is only accessible in the RUN
command where it’s referred in. This is why we have to put the mount and the pip install together using the &&
. After the RUN command is finished the secret is discarded and no evidence of its existence will be left in the container. Docker history will also not contain any information as you can see below.
Bonus: also a correct way but harder to implement
You can also use a two-stage build; this will first build your image in one stage and then copy only the relevant things to the final one, leaving behind all unrequired information, including credentials etc. Check out this article for more information on multi-stage builds in Docker.
Conclusion
In this article we’ve gone through 3 ways of NOT handling secrets in docker images and one easier and correct, secure way. Hardcoding credentials is plain stupidity, build-args won’t save you and even removing credentials files in your image linger in the layers after deletion.
Docker secrets are a very secure and easy to use confidential information and get rid of it once it’s used. With this article I hope to have made your docker images a bit more secure. If you have suggestions/clarifications please comment so I can make improvements. In the meantime, check out my other articles on all kinds of programming-related topics like these:
- Docker for absolute beginners
- Docker Compose for absolute beginners
- Turn Your Code into a Real Program: Packaging, Running and Distributing Scripts using Docker
- Create and publish your own Python package
- Create Your Custom, private Python Package That You Can PIP Install From Your Git Repository
- Advanced multi-tasking in Python: applying and benchmarking threadpools and processpools
- Write you own C extension to speed up Python x100
- Getting started with Cython: how to perform >1.7 billion calculations per second in Python
- Create a fast auto-documented, maintainable and easy-to-use Python API in 5 lines of code with FastAPI
Happy coding!
— Mike
P.S: like what I’m doing? Follow me!