<!--
  canonical-url: https://www.avonture.be/blog/docker-use-ssh-during-build
  published:     2024-09-03
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# Docker secrets - Using your SSH key during the build process

> Securely access private Git repositories during your Docker build using SSH keys and Docker secrets. Includes complete Dockerfile and compose.yaml examples to prevent secrets from being stored in your final image.

---
![Docker secrets - Using your SSH key during the build process](/img/v2/docker_secrets.webp)

**TL;DR**

This article shows how to `git clone` a private repository during a Docker build without ever baking the SSH key into the final image, using Docker's `RUN --mount=type=secret` BuildKit feature declared in `compose.yaml` and referenced in the `Dockerfile`. It also covers the separate case of sharing your SSH key with the running *container* (not the image) via a bind-mounted volume, for ongoing `git pull`/`push` work inside it.

There are plenty of articles on the Internet but I didn't find the one that allowed me, without an impressive amount of trial and error, to find the solution.

So here's another article to add to the long list: how to access a private project stored on Github when creating a Docker image. In other words, the SSH key is not stored in the image. Docker will just use your key when executing the project recovery *layer* (the one containing the `git clone` instruction) and will not keep track of the key afterwards.

My use case: I wish to build a Docker image and during the build phase, I need to grab a copy of a private repository I've put on github.com. *The CI equivalent of this problem — pulling a **private Docker image** rather than a private repository — is covered in [GitLab - Using Docker private images](/blog/gitlab-using-private-images).*

When I access the container, the project will then be available but I don't have, anywhere in my image, a copy of my SSH key so I won't be able to run a `git pull` f.i. since no further authentication is possible.

## Preview

![You're authenticated](./images/authenticated.webp)

That's the proof the technique works: the SSH key was used to authenticate against GitHub during
the build — without ever being baked into the final image.

## Why it works

* Docker's `RUN --mount=type=secret` mounts the secret only for the duration of that one build layer — it never gets written to a layer that ends up in the final image.
* Copying your SSH key into the image instead (or hardcoding a token) is a serious design error: anyone reading the Dockerfile, or starting an interactive bash session and running `printenv` or browsing `.ssh`/`.git` folders, will find it.
* Even a secret deleted in a later layer can survive: tools like [SecretScanner](https://github.com/deepfence/SecretScanner) do deep layer-by-layer scanning and will still find it if it was ever written to disk in an earlier layer. *The same reasoning applies to your git history: [Git - pre-commit-hooks](/blog/git-precommit) lists hooks that block a commit as soon as a credential is detected.*

## Which key to use

> **Caution:**
>
> This part is one of the most important ones. First, of course, you should already have created an SSH key (see my [Github - Connect your account using SSH and start to work with git@ protocol](/blog/github-connect-using-ssh) if needed).
>
> Then you should know which protocol you've used and that's really important. Is your key stored in a file called `id_ed25519` or `id_rsa` or something else? Only you know.
>
> Just run `ls -alh ~/.ssh` to get the list of your keys:
>
> ![My own keys](./images/ssh_keys.webp)
>
> I'm using two different keys as you can see: `id_ed25519` or `id_rsa`. Files having the `.pub` extensions are the public keys; those without extensions are the private keys.

It's important to know which key has been used to link your Github profile.

You can just display the file to your console like f.i. `cat ~/.ssh/id_ed25519.pub` or `cat ~/.ssh/id_rsa.pub` (the public key file) and look at the end: is the mentioned email is the one you're using on Github.com? If yes, you've probably identified the key.

In my case, the key I've used for github.com is `id_ed25519` so I'll use that one in the next chapter.

## Creating files

We'll need two files: `compose.yaml` and `Dockerfile`.

### compose.yaml

Here is my `compose.yaml` content:

**compose.yaml**

```yaml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      // highlight-next-line
      secrets:
        // highlight-next-line
        - my_ssh_key
      args:
        // highlight-next-line
        - KEY_NAME=id_ed25519

// highlight-next-line
secrets:
  // highlight-next-line
  my_ssh_key:
    // highlight-next-line
    file: ${HOME}/.ssh/id_ed25519
```

As you can see, we've five specific lines.

In our `services -> app --> build` entry, we should add the `secrets` and the name of one (or more) secrets.

I've also added an argument called `KEY_NAME` just because I need to inform Docker which key I'm using (is it `id_ed25519` or `id_rsa` or another one).

Secrets have to be defined at the same indentation level of `services` and the notation is this one:

**compose.yaml**

```yaml
secrets:
  a_secret_name:
    file: existing_file
```

You can use what you want for `a_secret_name`; for instance, `my_ssh_key`.

> **docker compose config:**
>
> You can, if you want, run `docker compose config` to check if your file is correct. You'll also see the full path for the used key.
>
> ![Docker compose config](./images/config.webp)

### Dockerfile

Time to create our second file, `Dockerfile`:

**Dockerfile**

```docker
FROM alpine:3.19

ARG KEY_NAME="id_rsa"

USER root

RUN apk add --no-cache bash ca-certificates git openssh-client

// highlight-next-line
RUN mkdir -p -m 0700 /root/.ssh && ssh-keyscan "github.com" >> /root/.ssh/known_hosts

// highlight-next-line
RUN --mount=type=secret,id=my_ssh_key,dst=/root/.ssh/${KEY_NAME} \
    // highlight-next-line
    mkdir -p /app && cd /app \
    // highlight-next-line
    && ssh -T git@github.com || true \
    // highlight-next-line
    && git clone git@github.com:cavo789/my_private_repo.git

WORKDIR /app

# Keep the container running
ENTRYPOINT ["tail", "-f", "/dev/null"]
```

> **Remember to replace cavo789/my\_private\_repo.git with one of your own repositories:**

As you can see, we are setting `KEY_NAME="id_rsa"` as the default value (but will be overwritten by our declaration in the yaml file) then later on, we need to create the `/root/.ssh` folder and we are adding `github.com` in the list of known hosts.

The most important thing comes then. We should use the `RUN --mount=type=secret` syntax to inform Docker that this layer will use a Docker secret. We need to provide the name of our secret (which was set in our yaml file and it's `my_ssh_key` here) then we need to define where that secret has to be stored **during this layer**. Our variable `KEY_NAME` finds its use here: our SSH key was `id_ed25519` and this is the value of the `KEY_NAME` variable.

So, in short `--mount=type=secret,id=my_ssh_key,dst=/root/.ssh/${KEY_NAME}` will be translated to `--mount=type=secret,id=my_ssh_key,dst=/root/.ssh/id_ed25519`.

During this layer thus, our local SSH key will be saved to `/root/.ssh/id_ed25519`. The other lines in that `RUN` layer will first create a `/app` folder, jump in it, then run `ssh -T git@github.com` just for debugging (we expect to see on the screen `Hi your_name! You've successfully authenticated`) and finally we're git cloning our private repository using SSH.

## Create the image and jump in the container

Ok, so you've the created the two files on your hard disk.

Run `docker compose --progress plain build --no-cache` in your console to build the image and enable the verbose mode.

As you can see, we can confirm that our SSH key was shared during the build process — that's the "You're authenticated" output shown at the top of this article.

You'll now create the container; by running `docker compose up --detach`.

![The container is being created](./images/container_created.webp)

Now, just to check, we can jump inside the container by running `docker compose exec app /bin/bash`.

![Starting an interactive console](./images/bash_session.webp)

We can verify that our key wasn't stored in the image by running `ls -alh /root/.ssh/`

![No keys in the .ssh folder](./images/root_ssh_folder.webp)

We can check too: jumping in our project folder and running `git pull` will fail with the error below:

```bash
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
```

And this is what we expect: our key isn't part of the image so we can't connect anymore to Github.

## Sharing your keys also with the container

Ok, we've seen how to share the SSH key with Docker but only during the build phase. As we've seen above, jumping in the container and running `git pull` has failed; exactly what we wanted in the previous chapter.

But now, if we need to share our key also with the container so we can work on the project, update it and push changes / pull modifications, how to do?

Please edit the `compose.yaml` file and add these two lines:

**compose.yaml**

```yaml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      secrets:
        - my_ssh_key
      args:
        - KEY_NAME=id_ed25519
    // highlight-next-line
    volumes:
      // highlight-next-line
      - ${HOME}/.ssh:/root/.ssh

secrets:
  my_ssh_key:
    file: ${HOME}/.ssh/id_ed25519
```

Then recreate the container by running `docker compose up --detach`. Jump in the container by running `docker compose exec app /bin/bash` and go in your project folder. Now, by running `git pull`, as you'll see, it'll work.

Why? Because your SSH keys are now part of the container as we can see by running `ls -alh /root/.ssh`:

![Keys are now part of the container](./images/container_ssh_keys.webp)

> **Keys are not stored in the image even when you've shared them with the container:**
>
> Just to be clear: the notion of volume we've just implemented concerns the container and not the image. In other words, SSH keys are not stored in the Docker image. You could give it to someone else (e.g. by saving the image on Docker Hub); your keys won't be there and will remain on your computer. Your image and your secrets are safe.
>
> Then, when the user run `docker compose up --detach` using the `compose.yaml` file where there are the two lines we've just added (the ones for adding the volume); these are his keys, on his computer, not yours.

> **Please take note of the running user in your container:**
>
> In our example here, the default user is `root` as we can see by jumping in the container and running `whoami`.
>
> ![Whoami](./images/whoami.webp)
>
> So, when we've started `git pull`, it was under `root`. This is why we've mounted our volume like below:
>
> **compose.yaml**
>
> ```yaml
> services:
>   app:
>     [...]
>     // highlight-next-line
>     volumes:
>       // highlight-next-line
>       - ${HOME}/.ssh:/root/.ssh
> ```
>
> Imagine the current user was `christophe`. In that case the mount should be done like this:
>
> **compose.yaml**
>
> ```yaml
> services:
>   app:
>     [...]
>     // highlight-next-line
>     volumes:
>       // highlight-next-line
>       - ${HOME}/.ssh:/christophe/.ssh
> ```

## Conclusion

Two different needs, two different mechanisms: a **build-time secret** (`--mount=type=secret`)
that never touches the final image, for one-off operations like cloning a private repo during
`docker build`; and a **bind-mounted volume**, for when the running container itself needs your
key on an ongoing basis to `git pull`/`push`. Pick the first by default, and only add the second
when you actually need to work from inside the container.
