Running Secured Docker Registry 2.0

Written by: Jaroslav Holub

This article was originally published on Container Solutions by Jaroslav Holub, and we are pleased to share it here for Codeship readers.

The new Docker Registry 2.0 was released on April 16th, 2015. It was completely rewritten in Go with added support for the new Docker Registry HTTP API V2 (thus only working with Docker 1.6+), promising to provide faster and more secure distribution of images.

If you work with Docker and for some reason decided not to use the public Docker Hub, a private Docker registry is an essential part of your architecture. But even if you don’t have private images, you will likely need to use your own registry in production/testing for efficiency.

The default installation, however, runs without encryption and authentication. I was wondering what’s involved in securing it. There is an official tutorial on how to configure TLS on a registry server. TLS/SSL is absolutely necessary for any secure setup, but I also wanted to enable an authentication mechanism.

The Configuration Reference document describes two authentication options supported by Docker Registry itself: so-called silly and token solutions. The silly one is apparently only useful for very limited development use cases. The token solution seems to be more serious, but because of the lack of documentation (at the time of writing), I decided to find an alternative approach to secure it.

In this article, I’m going to show you how to set up the Docker Registry 2.0 with username/password authentication and SSL using the official Docker Registry image and a custom-configured nginx as a proxy server.

Note: Docker daemon considers any private registry secure only if it uses transport layer security, a copy of its CA certificate is placed on the Docker host at /etc/docker/certs.d/:/ca.crt, and Docker is able to verify the certificate validity. In other cases (including usage of self-signed certificates), you need to run the Docker daemon with --insecure-registry flag.

First, run the registry:

docker run -v $(pwd)/data:/tmp/registry-dev --name docker-registry registry:2.0

I gave it a name, so we can refer to it from the nginx configuration. Because it’s nice to have stateless containers, I attached a volume where registry stores its data.

Because we want our registry to be accessible only by people who know the password, let’s create a .htpasswd file. You can do it like this: htpasswd -c .htpasswd exampleuser.

The last bits are writing the nginx configuration and finally running the proxy. The nginx config file might look like this:

server {
  listen 443 ssl;
  server_name localhost;
  add_header Docker-Distribution-Api-Version: registry/2.0 always;
  ssl on;
  ssl_certificate /etc/nginx/ssl/docker-registry.crt;
  ssl_certificate_key /etc/nginx/ssl/docker-registry.key;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Original-URI $request_uri;
  proxy_set_header Docker-Distribution-Api-Version registry/2.0;
  location / {
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/.htpasswd;
    proxy_pass http://docker-registry:5000;
  }
}

We need to use the always parameter of the add_header directive, introduced only recently in nginx 1.7.5. The reason is that nginx doesn’t send headers with auth_basic requests by default. Previously, you’d have to use the nginx-extras package that includes the HttpHeadersMore plugin.

Looking at the rest of the nginx configuration, it’s SSL only proxy, so we need to attach the certificates to the container to /etc/nginx/ssl/docker-registry.crt and /etc/nginx/ssl/docker-registry.key respectively. There is auth_basic enabled, for which we need to attach /etc/nginx/.htpasswd file, which we just generated.

We use the name of the already-running registry:2.0 container in the proxy_pass directive, together with port 5000, exposed from the container by default.

Now we run the proxy container. You can use the Docker Registry Proxy image, that we created for your convenience. It’s derived from nginx:1.7 and applies the configuration described above. You only need to provide it with REGISTRY_HOST and REGISTRY_PORT variables pointing to the registry container and the SERVER_NAME variable that stands for the nginx server_name directive.

A directory with SSL certificates must be mounted to the container as well as our .htpasswd file. Expose HTTPS port 443 to the host and add a link to our already running docker-registry container:

docker run -p 443:443 \
  -e REGISTRY_HOST="docker-registry" \
  -e REGISTRY_PORT="5000" \
  -e SERVER_NAME="localhost" \
  --link docker-registry:docker-registry \
  -v $(pwd)/.htpasswd:/etc/nginx/.htpasswd:ro \
  -v $(pwd)/certs:/etc/nginx/ssl:ro \
  containersol/docker-registry-proxy

Let’s verify that our registry works properly.

docker login -u <username> -p <password> -e <email> localhost:443
docker pull hello-world
docker tag hello-world:latest localhost:443/hello-secure-world:latest
docker push localhost:443/hello-secure-world:latest

You should see a successful push to your private Docker registry, secured with SSL and basic HTTP authentication. Although this setup seems to work, use it at your own risk and please let us know if you spot any issues!

We're all about making things easier. Have you tried Codeship's Continuous Integration and Delivery service? Check it out here.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.