Secrets are one of the sneakiest vulnerability issues you can have in a Docker image if you don’t know how to handle them.
If you need to clone a private repository or to download a private package you must use sensitive data during your docker build
, there’s no easy way around that.
In this tutorial on the advanced usage of Docker series, I’ll explain how to use a build secret in a safe way.
I explained last week what is the Buildkit build engine, how to set it up, and how you can use Buildkit to speed up docker build
.
COPY
and rm
If you are dealing with secrets during your development, I’m sure the first thing you’ve tried is to COPY
a file with credentials from your Dockerfile and then remove it with rm
when you don’t need it anymore…
This is so wrong
because you are just deleting the file from that layer but the credentials are still in the layer above!
Let’s use this Dockerfile.
FROM ubuntu:bionic
COPY .netrc /
RUN rm .netrc
And let’s build it.
$ docker build -t unsafe . -f Dockerfile.not-safe
Sending build context to Docker daemon 4.096kB
Step 1/3 : FROM ubuntu:bionic
---> c14bccfdea1c
Step 2/3 : COPY .netrc /
---> 18d1eb74c6da
Step 3/3 : RUN rm .netrc
---> Running in fafd31acf728
Removing intermediate container fafd31acf728
---> d7d4315738a6
Successfully built d7d4315738a6
Successfully tagged unsafe:latest
Now we want to use the command docker history
to list all the layers of the image.
$ docker history d7d4315738a6
IMAGE CREATED CREATED BY SIZE COMMENT
d7d4315738a6 10 seconds ago /bin/sh -c rm .netrc 0B
18d1eb74c6da 14 seconds ago /bin/sh -c #(nop) COPY file:a0fa732884be950b… 19B
c14bccfdea1c 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 4 weeks ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 4 weeks ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 4 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 745B
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:84f8ddc4d76e1e867… 63.2MB
Here we can see the latest layer created, d7d4315738a6
, but you don’t care about it.
The best part is the previous layer, 18d1eb74c6da
, which we can analyze deeper.
$ docker run -it 18d1eb74c6da
root@09fb719ec3dc:/# cat .netrc
my secret password
This is the deal: every layer of your image is available, including the ones with your secrets!
Think about it next time you do COPY
and rm
.
--secret
Buildkit adds a new flag called --secret
for the docker build command. You can use it to provide safely a secret to your Dockerfile at build time! Buildkit mounts the secret using tmpfs
in a temporary file located in /run/secrets
that we can use to access a secret in the Dockerfile.
Using this feature we are sure that no secrets will remain in the image!
Let’s use the following Dockerfile
# syntax = docker/dockerfile:1.0-experimental
FROM ubuntu:bionic
RUN --mount=type=secret,id=mynetrc,dst=/.netrc cat /.netrc
RUN cat /.netrc
The first thing to notice is # syntax = docker/dockerfile:1.0-experimental
, we tell Docker to use the new syntax to exploit the new Buildkit functionality.
Then, with the first RUN
command, the magic happens. We tell Docker to mount
a secret
with the id mynetrc
to the destination /.netrc
and in the same line we execute the cat
command just for the sake of the example.
Then we RUN
again the cat command on the same file.
To build our Dockerfile this is the command:
$ DOCKER_BUILDKIT=1 docker build --secret id=mynetrc,src=.netrc --progress=plain --no-cache -f Dockerfile.safe -t safe .
You can notice here the flag --secret
which tells Docker the secret name and location. We also need to set DOCKER_BUILDKIT=1
to use the Buildkit build engine.
OK, let’s build it.
$ DOCKER_BUILDKIT=1 docker build --secret id=mynetrc,src=.netrc --progress=plain --no-cache -f Dockerfile.safe -t safe .
#...
#8 [1/3] FROM docker.io/library/ubuntu:bionic
#8 CACHED
#9 [2/3] RUN --mount=type=secret,id=mynetrc,dst=/.netrc cat /.netrc
#9 0.808 my secret password
#9 DONE 1.7s
#10 [3/3] RUN cat /.netrc
#10 DONE 2.0s
#11 exporting to image
#11 exporting layers
#11 exporting layers 0.7s done
#11 writing image sha256:b86ed6e0585c2f2e5cb14796b896dae0004f75004ccece0949a3de0ca600b113 0.0s done
#11 naming to docker.io/library/safe 0.0s done
#11 DONE 1.0s
As you can see, in the RUN --mount=type=secret,id=mynetrc,dst=/.netrc cat /.netrc
we can access the content of the file, instead on the following RUN
there’s no output.
The .netrc
file, in fact, is present in the final layer of the image but it’s empty.
Let’s use the command docker history
to list all the layers of this new image.
$ docker history b86ed6e0585c
IMAGE CREATED CREATED BY SIZE COMMENT
b86ed6e0585c 5 hours ago RUN /bin/sh -c cat /.netrc # buildkit 0B buildkit.dockerfile.v0
<missing> 5 hours ago RUN /bin/sh -c cat /.netrc # buildkit 0B buildkit.dockerfile.v0
<missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 4 weeks ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 4 weeks ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 4 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 745B
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:84f8ddc4d76e1e867… 63.2MB
As you can see, it’s not possible now to load an older layer to read the secret.
I hope this was useful for you, now go and refactor your old Dockerfile!
Reach me on Twitter @gasparevitta and let me know how you use manage secrets.
You can find the code snippets on Github.