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:
- Restricting access to who can pull the image containing the binaries and the configuration of a closed source project.
- 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:
Now we can use the private key passed as a BP environment variable to start the container:docker build --build-arg KEY=abc -t encrypted .
If we start the container using the wrong key, it will fail:docker run -e BP=abc encrypted
And we can check if we can see the plain text in the binary:docker run -e BP=wrong encrypted
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.