Advanced Docker: how to use secrets the right way
Tags: docker, tutorial, devops, codequality
Published: 2020-10-27 15:11:02

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**.

![Tutorial on the advanced usage of Docker build secrets in a safe way](https://dev-to-uploads.s3.amazonaws.com/i/d7lmh5izuqo4dp8ruqbv.jpg)

## What is Buildkit

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`](https://www.gasparevitta.com/posts/docker-unit-test-dockerfile-image/).

## Never use `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!

### Unsafe code example

Let's use this Dockerfile.

```bash
FROM ubuntu:bionic
COPY .netrc /
RUN rm .netrc
```

And let's build it.

```bash
$ 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.

```bash
$ 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.

```bash
$ 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`.

## Buildkit to the rescue with `--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!*

### Safe code example

#### Dockerfile

Let's use the following Dockerfile

```bash
# 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.

#### Build command

To build our Dockerfile this is the command:

```bash
$ 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.

```bash
$ 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.

```bash
$ 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.

## This is it!

I hope this was useful for you, now go and refactor your old Dockerfile!

Reach me on Twitter [@gasparevitta](https://twitter.com/gasparevitta) and **let me know how you use manage secrets**.

You can find the code snippets on [Github](https://github.com/gasparev/blog-code-snippets/tree/master/docker-secrets).

*This article was originally published on my [blog](https://www.gasparevitta.com/posts/advanced-docker-secrets-buildkit/). Head over there if you like this post and want to read others like it!*