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 authentication
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 git
and openssh-client
packages.
~/.ssh
directory is created and ssh-keyscan
command is used to gather SSH public key
of the GitHub server and put it to the known_hosts
file.
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
here
and
here
.
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
(GitHub
,
GitLab
).
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.
Dockerfile:
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
Note --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 .
Afterword
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 scp
or rsync
.
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.