Using SSH private keys securely when building Docker images

Use case: SSH keys required to access private GitHub repository

Couple of days ago, I needed to build image for Ethereum (this had to be custom build that included code from our private repository).

Accessing private GitHub repositories is easy: all you need is to generate RSA key, and add its public part as deploy key to GitHub repository of interest (see Repository Settings).

Then on client side, you need to load private part of the key and…well, that’s it - you will be able to clone from the private repository.

Problem

The problem is that if you ADD private key to your Docker image (in Dockerfile), it will be persisted as a layer. And even if you then remove the private key (once you are done pulling from the repository), your private key will still be available (that’s how layers work - key will be missing from the latest layer, but still available in the original layer it was added to). And that is a huge no-no!

There are number of solutions available:

  • pull the code before building, so that you do now need to use SSH key at all
  • squashing layers, so that temporary layer, which was created when SSH key was added, is merged (and thus is wiped out)
  • forward SSH agent (sounds nice, but is hard to accomplish)
  • go as far as to use Habitus
  • make a note that Habitus solution is very nice, but a bit of an overkill, and proceed from there to..

My solution

Which is pretty simple: expose any required artifacts using local web-server, and fetch them into the image using wget or similar.

Without further ado, here is Dockerfile:

FROM alpine:3.4

RUN \
  apk add --update git openssh-client && \

  # add SSH key
  wget -O /tmp/id_rsa http://192.168.99.1:8080/some_id_rsa && \
  chmod 600 /tmp/id_rsa && \
  eval $(ssh-agent) && \
  echo -e "StrictHostKeyChecking no" >> /etc/ssh/ssh_config && \
  ssh-add /tmp/id_rsa && \

  # clone some private repo
  git clone --depth 1 --branch develop git@github.com:farazdagi/some-repo.git  && \

  # install and cleanup
  # ..some installation instructions
  apk del git openssh-client && \
  rm /tmp/id_rsa

EXPOSE 8545

ENTRYPOINT ["/foo"]

  • As you see, everything is within a single RUN instruction - which means single layer added for all the cloning, installing and cleaning up.
  • Another point of interest is that I rely on wget to obtain some_id_rsa (which is the private key). And IP I use is default VM IP address of Mac that runs docker-machine (I am on OSX). On Linux box, it can be Docker network IP address of host machine.

Now, the last thing before we docker build is to make sure that RSA keys are exposed indeed.

You can use Python’s SimpleHTTPServer or PHP’s bundled server, or whatever you have experience with.

I’ve used PHP’s internal server:

cd ~/folder-that-has-your-deploy-id-rsa
php -S 192.168.99.1:8080 # that's it, all files in the directory are exposed

The benefits are immense:

  • solution is trivial to setup and easy to use (it will work with other “secrets” too)
  • you do not need to post-process your images (to squash layers)
  • you are still using conventional Dockerfile - no magic here, just wget command to obtain artifacts

References