Use Docker Buildkit to securely configure your Docker image to access private resources
This article was originally published in Medium on Jul 30, 2021
Challenges in accessing protected information
Accessing private resources like a Nexus or a GitHub repository from within a Docker image and not leaking any security-related information it’s greatly appreciated.
Firstly, we will never include credentials or any other secure information directly in the Dockerfile.
Secondly, one can create a multi-stage¹ build and clear the final image and layers from any private information. However, this is not a straightforward solution and heavily depends on who is developing.
Finally, some of us can rely on using arguments — ARG— and pass the information in the docker build² command. However, this alone leaves the secret information available in the image layers. I will show an example of it next.
Using arguments to build an image
Consider the following Dockerfile that downloads a file from a Nexus repository:
FROM ubuntu
ARG USERNAME
ARG PASSWORD
RUN apt-get update && \
apt-get upgrade && \
apt-get install -y curl
RUN curl -o nginx_policy.yaml -u $USERNAME:$PASSWORD http://nexus:8081/repository/raw/policy/nginx-policy.yaml
Using the docker build command we can successfully create the image:
docker build -t secret:args --build-arg USERNAME=$user --build-arg PASSWORD=$pass .
Sending build context to Docker daemon 3.072kB
Step 1/5 : FROM ubuntu
---> c29284518f49
Step 2/5 : ARG USERNAME
---> Using cache
---> 720c9732f5db
Step 3/5 : ARG PASSWORD
---> Using cache
---> 193f8044461b
Step 4/5 : RUN apt-get update && apt-get upgrade && apt-get install -y curl
---> Using cache
---> 894b791e5ec3
Step 5/5 : RUN curl -o nginx_policy.yaml -u $USERNAME:$PASSWORD http://172.17.0.2:8081/repository/raw/policy/nginx-policy.yaml
---> Using cache
---> e4050d5c1743
Successfully built e4050d5c1743
Successfully tagged secret:args
I used the environment variables in the build-arg option to not store the credentials operating system history. Also, in the image, I am not saving this information anywhere.
The issue with this approach appears when you run docker history command against this image:
➜ docker history secret:args_env
IMAGE CREATED CREATED BY SIZE COMMENT
f11cb0b139f3 2 minutes ago |2 PASSWORD=admin123 USERNAME=admin /bin/sh … 0B
6ca5f3fc074a 2 minutes ago |2 PASSWORD=admin123 USERNAME=admin /bin/sh … 0B
193f8044461b 43 minutes ago /bin/sh -c #(nop) ARG PASSWORD 0B
720c9732f5db 43 minutes ago /bin/sh -c #(nop) ARG USERNAME 0B
c29284518f49 3 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 3 days ago /bin/sh -c #(nop) ADD file:5c3d9d2597e01d1ce… 72.8MB
Clearly, we can see that the credentials are stored in the metadata of the image. Despite our best efforts, we are still leaking confidential information. BuildKit³ was integrated⁴ with Docker precisely to help us to stay secure.
Enabling Buildkit
Buildkit is present in Docker since the 18.06 release and currently only supports Linux containers. To enable Buildkit builds, set the DOCKER_BUILDKIT=1 environment variable when invoking the docker build command, such as:
$ DOCKER_BUILDKIT=1 docker build .
or set the daemon configuration⁵ feature to true and restart the daemon:
{ “features”: { “buildkit”: true } }
Docker Build secret information
The new --secret flag for docker build allows the user to pass secret information to be used in the Dockerfile for building docker images in a safe way that will not end up stored in the final image.
To use this feature we need to override the default frontend⁶ in our Dockerfile. In the very first line of the Dockerfile enter:
# syntax=docker/dockerfile:1.2
A --mount flag was added to the RUN command to allow the build container to access secure files such as private keys without baking them into the image.
# shows secret from default secret location:
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret
Building a container with Buildkit
For example, with a secret piece of information stored in a text file:
echo 'SUPER_SECRET_PASSWORD' > mysecret.txt
And with a Dockerfile that specifies the use of a BuildKit frontend docker/dockerfile:1.2, the secret can be accessed when performing a RUN:
# syntax=docker/dockerfile:1.2
FROM alpine
# shows secret from default secret location:
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret
This Dockerfile is only to demonstrate that the secret can be accessed. As you can see the secret printed in the build output. The final image built will not have the secret file:
$ DOCKER_BUILDKIT=1 docker build --no-cache --progress=plain -t secret:buildkit --secret id=mysecret,src=mysecret.txt .
#1 [internal] load build definition from Dockerfile
#1 sha256:5c65425f6fc0d6c65a5ddd6784812097b4eff778b9bcbf39bc708aacbad59abd
#1 transferring dockerfile: 196B done
#1 DONE 0.0s
#2 [internal] load .dockerignore
#2 sha256:35c8e51716823a06d8dbd04a2594a31a90f3d02bb55a0b8c3e9f2c1b44f901c7
#2 transferring context: 2B done
#2 DONE 0.0s
#3 resolve image config for docker.io/docker/dockerfile:1.2
#3 sha256:b239a20f31d7f1e5744984df3d652780f1a82c37554dd73e1ad47c8eb05b0d69
#3 DONE 2.5s
#4 docker-image://docker.io/docker/dockerfile:1.2@sha256:e2a8561e419ab1ba6b2fe6cbdf49fd92b95912df1cf7d313c3e2230a333fdbcc
#4 sha256:37e0c519b0431ef5446f4dd0a4588ba695f961e9b0e800cd8c7f5ba6165af727
#4 resolve docker.io/docker/dockerfile:1.2@sha256:e2a8561e419ab1ba6b2fe6cbdf49fd92b95912df1cf7d313c3e2230a333fdbcc done
#4 CACHED
#5 [internal] load metadata for docker.io/library/alpine:latest
#5 sha256:d4fb25f5b5c00defc20ce26f2efc4e288de8834ed5aa59dff877b495ba88fda6
#5 DONE 0.0s
#6 [1/2] FROM docker.io/library/alpine
#6 sha256:665ba8b2cdc0cb0200e2a42a6b3c0f8f684089f4cd1b81494fbb9805879120f7
#6 CACHED
#7 [2/2] RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret
#7 sha256:75601a522ebe80ada66dedd9dd86772ca932d30d7e1b11bba94c04aa55c237de
#7 0.494 SUPER_SECRET_PASSWORD#7 DONE 0.5s
#8 exporting to image
#8 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
#8 exporting layers 0.0s done
#8 writing image sha256:8df0fe20ceab547858702c32456d962b99d66fd40ab29f4e191ac0b52e383039 done
#8 DONE 0.0s
Running docker history we cannot see any information related to the secret:
$ docker history secret:buildkit
IMAGE CREATED CREATED BY SIZE COMMENT
7da5afe48753 16 seconds ago RUN /bin/sh -c cat /run/secrets/mysecret # b… 0B buildkit.dockerfile.v0
<missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:f278386b0cef68136… 5.6MB
Instead of using a secret file, one can pass the secret information using an environment variable⁷, like:
$ export PASSWORD=SUPER_SECRET_PASSWORD
$ DOCKER_BUILDKIT=1 docker build --no-cache --progress=plain -t secret:buildkit --secret id=mysecret,env=PASSWORD .
Conclusion
This article explained the steps necessary to make the use of restricted information safer within Docker containers.
By setting one environment variable before your docker build and a few changes in your Dockerfile, you can prevent the leaking of credentials used to configure your Docker containers.
Docker BuildKit not only supports secrets but also other build mounts⁸ such as cache and ssh. Start or continue to explore Buildkit and understand how it can help you improve the security of your environments.
References
https://docs.docker.com/develop/develop-images/multistage-build/
https://docs.docker.com/develop/develop-images/build_enhancements/
https://docs.docker.com/develop/develop-images/build_enhancements/#overriding-default-frontends
https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/syntax.md
https://medium.com/@tonistiigi/build-secrets-and-ssh-forwarding-in-docker-18-09-ae8161d066
Comments