In this short article, I wanted to share two approaches to accessing private Git repositories from within Dockerfile, while building Docker images. It can be useful to install dependencies which are hosted in a private Git repo, like Ruby gems, NPM modules, you name it.
Spoiler: there is a “two-in-one” method described in the end, which is actually two methods wrapped in a convenience script. I found it very handy for both building images locally during development and running the build pipeline in a CI environment using the same Dockerfile.
SSH key authentication is a simple and secure way to authenticate access to the Git repository. It is supported by most popular hosting platforms like GitHub and GitLab, and also natively supported by the Git server itself.
This is a simple Dockerfile with instructions to clone one of my private GitHub repositories (a repository of this blog):
FROM alpine RUN apk add --update --no-cache openssh-client git RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts RUN git clone [email protected]:flexoid/yegor-blog.git
It is based on the
alpine image, where we additionally install
~/.ssh directory is created and
ssh-keyscan command is used to gather SSH public key
of the GitHub server and put it to the
This step is required due to SSH strict host checking mechanism enabled by default, which provides
additional protection against man-in-the-middle attacks. Running
ssh-keyscan during the build is simple,
but to be fair, not the most secure solution. There are a few alternatives, you can read more about it
If we try to run
docker build with this Dockerfile, that’s what we get:
> [4/4] RUN git clone [email protected]:flexoid/yegor-blog.git: #7 0.923 Cloning into 'yegor-blog'... #7 1.619 [email protected]: Permission denied (publickey). #7 1.620 fatal: Could not read from remote repository. #7 1.620 #7 1.620 Please make sure you have the correct access rights #7 1.620 and the repository exists.
Method 1: Passing SSH private key via build argument
By using this method, SSH private key is passed into the Docker building context via build argument.
FROM alpine RUN apk add --update --no-cache openssh-client git RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts ARG SSH_PRIVATE_KEY RUN eval $(ssh-agent -s) && \ echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null && \ git clone [email protected]:flexoid/yegor-blog.git
This method is easy to use from CI pipelines. You only need to configure a secret and
make it available as
SSH_PRIVATE_KEY environment variable
The build command should look like this (omitting irrelevant arguments):
docker build --build-arg SSH_PRIVATE_KEY .
Do not use your personal SSH key, as it usually allows way more access to various repositories than required. Instead prefer configuring dedicated read-only deploy keys for the repository (GitHub, GitLab).
Method 2: SSH agent forwarding
I find this method the most convenient for local image building and testing. It does not require dealing with build args or env variables. Also raw SSH keys are not passed, therefore the chance of accidental leaking of the private key to the image is minimal.
It requires BuildKit - a new generation container image builder, which is integrated into the Docker from v18.06. More information on BuildKit here, as well as the instruction on how to enable it by default. If you’re using an up-to-date version of Docker Desktop, it should be enabled already.
You also need to add your SSH key to the ssh-agent, but if you’re active SSH and Git user - it’s probably done already. If not - there is good GitHub documentation covering this topic.
FROM alpine RUN apk add --update --no-cache openssh-client git RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts RUN --mount=type=ssh git clone [email protected]:flexoid/yegor-blog.git
--mount=type=ssh part of
RUN command, that’s where the agent forwarding from local machine
into Docker context happens, similar to how the agent can be forwarded when you go to the host via SSH.
Build it with:
docker build --ssh default .
Bonus: convenience script to support both methods
Let’s create a short script
docker/ssh_agent_auth.sh. It checks if ssh-agent is already running
(which means it was forwarded from the host system), otherwise it starts the agent and adds SSH key
from the variable.
ssh-keyscan command is also moved here to simplify Dockerfile a bit.
#!/bin/sh set -e if ssh-add -l; then echo "Using forwarded ssh agent" else eval $(ssh-agent -s) echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null fi mkdir -p -m 0600 ~/.ssh ssh-keyscan github.com >> ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts
And updated Dockerfile:
FROM alpine RUN apk add --update --no-cache openssh-client git COPY ./docker ./docker ARG SSH_PRIVATE_KEY RUN --mount=type=ssh \ . docker/ssh_agent_auth.sh && \ git clone [email protected]:flexoid/yegor-blog.git
Now the same image can be built with the forwarded SSH agent
docker build --ssh default .
…or passed private key variable
# Don't do this except for testing export SSH_PRIVATE_KEY="$(cat ~/.ssh/id_rsa)" docker build --build-arg SSH_PRIVATE_KEY .
As described methods basically configure SSH access from the Dockerfile, the usage is not limited
to fetching Git repositories. For example, the same approach can be applied to copy files from
a private remote host with
Whichever method you choose, treat your private keys with extreme care.
Never use your private keys to set up CI pipelines. An accidental leak could give access to your private data. Follow the least privilege principle.