Running Secured Docker Registry 2.0

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/<ip>:<port>/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 always parameter of 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 nginx-extras package that includes HttpHeadersMore plugin and use that.

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 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 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 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   \
(out)    -e REGISTRY_HOST="docker-registry"  \
(out)    -e REGISTRY_PORT="5000"   \
(out)    -e SERVER_NAME="localhost"  \
(out)    --link docker-registry:docker-registry  \
(out)    -v $(pwd)/.htpasswd:/etc/nginx/.htpasswd:ro  \
(out)    -v $(pwd)/certs:/etc/nginx/ssl:ro  \
(out)    containersol/docker-registry-proxy

Let’s verify that our Registry works properly.

  
docker login -u  -p  -e  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!

Comments
Leave your Comment