Docker secrets - Using your SSH key during the build process
There are plenty 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 at 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.
When I'll access to 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 more authentication are possible.
But, why is it important?โ
As soon as you need to access to something private like a private repository (on Github, Gitlab or anywhere else; it doesn't matter) during the build stage, Docker has to be able to connect as yourself.
You can provide your own credentials or using a token or copying your SSH key in the image or ... You can do this but you'll be making a serious design error: by reading your Dockerfile anyone will be able to see your private information(in case of hardcoding) or by starting an interactive bash session, it'll be able to f.i. display the list of environment variables (like using printenv
) or trying to access to display files (like trying to access to .git
folders, ssh folders, ...) and it'll work!.
Also, there are existing tools like SecretScanner allowing to deeply scan layers (a Docker image is composed in multiple layers) and even if the secret is stored in a file that no longer exists in the final image, if it has been saved in a layer, then this type of tool will be able to retrieve it.
So, in conclusion: there is only one way to use secrets using Docker and you'll learn how in this article.
Which key to useโ
This part is one of the most important ones. First, of course, you should already have created a SSH key (see my Github - Connect your account using SSH and start to work with git@ protocol 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:
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_25519
so I'll use that one in the next chapter.
Creating filesโ
We'll need to files; docker-compose.yaml
and Dockerfile
.
docker-compose.yamlโ
Here is my docker-compose.yaml
content:
services:
app:
build:
context: .
dockerfile: Dockerfile
secrets:
- my_ssh_key
args:
- KEY_NAME=id_ed25519
secrets:
my_ssh_key:
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:
secrets:
a_secret_name:
file: existing_file
You can use what you want for a_secret_name
; for instance, my_ssh_key
.
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.
Dockerfileโ
Time to create our second file, Dockerfile
:
FROM alpine:3.19
ARG KEY_NAME="id_rsa"
USER root
RUN apk add --no-cache bash ca-certificates git openssh-client
RUN mkdir -p -m 0700 /root/.ssh && ssh-keyscan "github.com" >> /root/.ssh/known_hosts
RUN --mount=type=secret,id=my_ssh_key,dst=/root/.ssh/${KEY_NAME} \
mkdir -p /app && cd app \
&& ssh -T git@github.com || true \
&& git clone git@github.com:cavo789/my_private_repo.git
WORKDIR /app
# Keep the container running
ENTRYPOINT ["tail", "-f", "/dev/null"]
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 so 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
find his interest 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 below, we can confirm that our SSH key was shared during the build process.
You'll now create the container; by running docker compose up --detach
.
Now, just to check, we can jump inside the container by running docker compose exec app /bin/bash
.
We can verify that our key wasn't stored in the image by running ls -alh /root/.ssh/
We can check too: jumping in our project folder and running git pull
will fail with the error below:
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 docker-compose.yaml
file and add these two lines:
services:
app:
build:
context: .
dockerfile: Dockerfile
secrets:
- my_ssh_key
args:
- KEY_NAME=id_ed25519
volumes:
- ${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
:
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 docker-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.
In our example here, the default user is root
as we can see by jumping in the container and running whoami
.
So, when we've started git pull
, it was under root
. This is why we've mounted our volume like below:
services:
app:
[...]
volumes:
- ${HOME}/.ssh:/root/.ssh
Imagine the current user was christophe
. In that case the mount should be done like this:
services:
app:
[...]
volumes:
- ${HOME}/.ssh:/christophe/.ssh