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.