Monday, 18 December 2017

Using public Docker Hub for private Golang project images


Ever wondered if you really have to either pay for a private Docker Hub repository or run your own private Docker Registry in order to keep your Docker images private? Might there a way to use a public repository as a private one? Yes, there is - read on!

To answer this question, let's take a step back and analyse the main reasons for keeping your Docker images private. In my experience, the reasons usually boil down to one of the following:
  1. Restricting access to who can pull the image containing the binaries and the configuration of a closed source project.
  2. Hiding confidential/sensitive information such as passwords, tokens and private keys contained in the images.


Problem

In order to pull an image from a private Docker Hub Repository or a private Docker Registry, a Docker client needs to provide valid credentials when pulling the image, either in the form of username/password or as a valid authentication token. Assuming that the credentials are kept private, access to the image is restricted to the people/systems that are in possession of those credentials.

The only problem with this solution is that it costs money. Docker Hub provides free hosting for an infinite number of images, as long as they're public; but for private images it charges $12 to $16.80 per image and year, depending on the plan. This might be acceptable if you're running a business that generates revenue, but if you would like to store the content of your private research project on Docker Hub, then you might feel that price is relatively steep.

What are your options? You could run your own Docker Registry. But guess what? That registry has to run somewhere. You'd have to take care of backups, monitoring etc. You will end up paying even more, both in money and time.

Infrastructure free solution

But wait, before your admit defeat and give your credit card details to Docker Hub, let's see if we can achieve our goals using only public images hosted on Docker Hub.

The obvious solution is to encrypt the contents of the files in the Docker image using some kind of symmetric encryption. Everyone can read the encrypted image, but no one can access the data without knowing the secret key.

In theory this works, but in practice there are a few obstacles to overcome. First of all, you have to modify your project to decrypt the data when reading. Secondly, at least one binary of the image must be ELF executable, in order to be used as the init command for the Docker container.

Enter Golang

If you happen to be a Golang developer, both of these problems can be reduced to one. If the files that are distributed together with the compiled binary are small enough to fit into the heap, one can use bindata and assetfs to generate a Golang source that can be compiled into one binary and accessed using the filesystem interface. After doing that, the public image contains only one binary that is self-contained. Now we only have the problem of how to make sure that this one binary cannot be run and/or inspected by anyone not owning the secret key.

Enter Midgetpack

Fortunately for our endeavour, there is already an open source project that can encrypt an ELF binary with a symmetric key: Midgetpack. In its original version, it will prompt the user for the password on TTY, which is not very useful when running the binary in Docker. In order to pass the password as an environment variable, I have modified Midgetpack slightly. I have also created a Docker image containing the modified Midgetpack binary that can be used as part of a multi stage Docker build.

Putting everything together

Now that we have all the moving parts, we can put them all together!
For demonstration purposes, we'll use the following Golang source:

package main

import "fmt"

func main() {
 fmt.Println("super-secret-text")
}
To compile and encrypt the binary file we can use the following Dockerfile:
FROM golang:1.9.2 as build
RUN mkdir -p /go/src/github.com/draganm/midgetpack-demo
WORKDIR /go/src/github.com/draganm/midgetpack-demo
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go install .

FROM dmilhdef/midgetpack:v2.0 as encrypt
ARG KEY
COPY --from=build /go/bin/midgetpack-demo /
RUN /midgetpack  -p -P $KEY /midgetpack-demo -o /midgetpack-demo-encrypted

FROM alpine:3.6
COPY --from=encrypt /midgetpack-demo-encrypted /midgetpack-demo
CMD ["/midgetpack-demo"]
 In order to build the image we need to pass a private key as a build argument to the docker build:
docker build --build-arg KEY=abc -t encrypted .
Now we can use the private key passed as a BP environment variable to start the container:
docker run -e BP=abc encrypted
If we start the container using the wrong key, it will fail:
docker run -e BP=wrong encrypted
And we can check if we can see the plain text in the binary:
docker run encrypted strings /midgetpack-demo | grep super-secret-text

Conclusion

It is possible to use publicly accessible Docker images to distribute closed source projects, as long as you're ready to accept some limitations and jump through a few hoops. This solution is not limited to Golang. As long as the image consists of ELF binaries, midgetpack can be used to encrypt them. A very similar approach could be used with Java/Clojure. In this case, the class files have to be encrypted and a custom class loader has to be implemented.