Don’t leak your Docker image’s build secrets

Building a Docker image often involves installing packages or downloading code, and if you’re installing private code you often need to gain access with a secret: a password, a private key, a token. You don’t want those secrets to end up in the final image, though; if it’s in the image, anyone with access to the image can extract it.

Unlike docker run, which supports environment variables (-e) and volumes, docker build has traditionally never had a good solution for securely using secrets. So how do you use build secrets in Docker without leaking them?

In this article you’ll learn:

  1. Some seemingly reasonable but actually insecure or problematic solutions.
  2. The correct solution, using modern Docker features.
  3. Other potential approaches.

(Preventing leaking other kinds secrets, like runtime secrets, is covered in a different article.)

Insecure options you should not use

Some seemingly reasonable approaches will actually result in the secret (a password, your SSH key) being embedded in the image. That means any attacker getting access to the image will be able to extract your secret.

Insecure solution: COPY the secret in as a file

Let’s say you have a .netrc file with usernames and passwords for your package repository. It can be tempting to do something like the following:

FROM python:3.9
# Copy in config file with credentials.
# DO NOT DO THIS IT IS INSECURE.
COPY .netrc /root
# pip will use credentials from the .netrc file:
RUN pip install https://passwordprotected.repo.example.com/packages/mypackage
# Hide the file with credentials:
RUN rm /root/.netrc

Don’t do this! Deleting a file does not actually remove it from the image, because Docker uses layer caching: all previous layers are still present in the image. That means the secret ends up in one of the image’s layers, even if you delete it in a later layer.

Any attacker with access to the image can retrieve the secret.

Insecure solution: Pass the secret in using –build-arg

Another tempting approach is to use the --build-arg option to docker build. Unfortunately build arguments are also embedded in the image: an attacker can run docker history --no-trunc <yourimage> and see your secrets. See my article on docker history for more details.

Technically you can work around this leak by using multi-stage builds, but that will result in slow builds, so I don’t recommend it.

The easy solution: BuildKit

The latest versions of Docker support a new build system called BuildKit, which includes support for adding secrets, as well as for SSH agent authentication forwarding. On macOS and Windows Docker Desktop is usually enabled by default, and it’s the default on Linux starting with version 23.0. If it’s not on, you can enable it with the DOCKER_BUILDKIT environment variable.

Let’s say you have a secret you need to use in your build:

$ cat secret-file
THIS IS SECRET

First, configure your Dockerfile to use BuildKit, and add a flag to RUN telling it to expose a particular secret:

# syntax = docker/dockerfile:1.3
FROM python:3.9-slim-bullseye
COPY build-script.sh .
RUN --mount=type=secret,id=mysecret ./build-script.sh

The build-script.sh will be able to find the secret as a file in path /run/secrets/mysecret.

Then, to build your image with the secret set the appropriate environment variable and pass in the newly enabled command-line arguments:

$ export DOCKER_BUILDKIT=1
$ docker build --secret id=mysecret,src=secret-file .

Docker 20.10 adds the additional ability to load secrets from environment variables, not just files. For example, if you have an environment variable MYSECRET, you can access it like this:

$ export MYSECRET=theverysecretpassword
$ export DOCKER_BUILDKIT=1
$ docker build --secret id=mysecret,env=MYSECRET .

If you’re OK with the id and env variable name being the same, you can replace the last line with:

$ docker build --secret id=MYSECRET .

Note that it will still be exposed inside the build as a file in /run/secrets, it is merely read from an environment variable on the host.

Other notes:

Note: Outside any specific best practice being demonstrated, the Dockerfiles in this article are not examples of best practices, since the added complexity would obscure the main point of the article.

Python on Docker Production Handbook Need to ship quickly, and don’t have time to figure out every detail on your own? Read the concise, action-oriented Python on Docker Production Handbook.

Other solutions

Use the build secrets outside of Docker

You don’t necessarily have to use build secrets inside the Docker build, i.e. inside the Dockerfile’s RUN commands. You might be able to download all files outside of the Docker build as part of the driving build process, much like you usually check out your code outside of the Dockerfile.

Once you have the files downloaded, you can just COPY them in as usual, and the Docker build never sees the secrets.

Expiring tokens

Another approach you can take is using expiring tokens, which are supported by some package repositories. Here you login to the package repository outside of the Docker build, and get an access token that will expire in 5 minutes.

You then can pass this temporary token in to the Docker build using COPY or --build-arg, and then use it inside the build process. The secret will get leaked, it’s true, but since it expires after 5 minutes, as long as you ensure no one can access your image until that deadline is hit, leaking the token isn’t a problem.

Don’t leak those secrets!

If you’re using build secrets, now’s a good time to go and check you’re not leaking them via COPY or --build-arg. You don’t want to find out the hard way that you’ve leaked a secret to an attacker. And you might also want to use a secrets scanner to catch anything you’ve leaked by mistake.