Security is part of everyday life. We lock our doors, protect our banking information with passwords that are usually so complicated that we tend to forget them. Using common sense to secure systems is just good practice. It’s really easy to assume that because a system is internal, there is no need to enable authentication or a secure transport for it, but in our current era of remote workers, that internal network can be quite wide.
With that in mind, we spent some time investigating various systems we’re bringing up and this week, we set out with the goal of adding authentication to our private Docker repository. As you may be aware, the Docker registry does not provide a mechanism for authentication so we decided that the easiest solution to this problem would be to add an authentication proxy in front of our image repository. In our case we decided to use Nginx over SSL coupled with an internal authentication API:
This solution provided us with a few advantages:
- allows us to use our internal authentication API
- can be re-used to provide authentication to other systems
- can be implemented using a Docker container (we <3 Docker)
We put together a simple authentication service and an Nginx container which we made available here (https://registry.hub.docker.com/u/opendns/).
Simple basic authentication service
As a reference for the Nginx proxy container, we built an authentication API using NodeJS, which made creating a Basic authentication service a breeze. All we need to do is to create a really simple server.js, generate a credentials file using the htpasswd utility and wrap the whole thing in a Docker container which we created with the following Dockerfile:
FROM google/nodejs ADD . /app WORKDIR /app RUN npm install http-auth EXPOSE 8000 ENV NODE_PATH /data/node_modules/ CMD ["node", "server.js"]
We then deployed and tested our service:
ubuntu@trusty-64:/basic-auth# docker build -t opendns/basic-auth-service . ubuntu@trusty-64:/basic-auth# docker run --name simple-auth opendns/basic-auth-service ubuntu@trusty-64:/basic-auth# docker inspect --format '{{ .NetworkSettings.IPAddress }}' simple-auth 172.17.0.40 ubuntu@trusty-64:/basic-auth# curl 172.17.0.40:8000 401 Unauthorized ubuntu@trusty-64:/basic-auth# curl -u testuser:testpassword 172.17.0.40:8000 User authenticated successfully
You can find the full example code for the basic authentication service here and the container available here.
Nginx authentication proxy
The key component of the Nginx proxy is the configuration:
# define an /auth section to send the request to an authentication service location = /auth { proxy_pass {{auth_backend}}; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Docker-Token ""; } # use the auth_request directive to redirect all requests to the /auth section above location / { proxy_pass {{backend}}; auth_request /auth; proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; proxy_buffering off; }
It uses the http_auth_request module which sends the user using the proxy_pass directive to our simple authentication service which either returns a 200 or a 401. The 401 Authorization response triggers the Docker client to respond with a set of credentials using basic auth. Once the credentials are accepted and the API returns a 200, nginx can send the request through to the private registry. Putting the two containers together:
ubuntu@trusty-64:/nginx-auth-proxy# docker run -d --name hello-world hello-world # run a simple web server that prints out “Hello world” ubuntu@trusty-64:/nginx-auth-proxy# docker inspect --format '{{ .NetworkSettings.IPAddress }}' hello-world 172.17.0.41 ubuntu@trusty-64:/nginx-auth-proxy# docker run -d -e AUTH_BACKEND=http://172.17.0.40:8000 -e BACKEND=http://172.17.0.41:8081 -p 0.0.0.0:8080:80 nginx-auth ubuntu@trusty-64:/nginx-auth-proxy# curl 0.0.0.0:8080 <html> <head><title>401 Authorization Required</title></head> <body bgcolor="white"> <center><h1>401 Authorization Required</h1></center> <hr><center>nginx/1.6.1</center> </body> </html> ubuntu@trusty-64:/nginx-auth-proxy# curl -u testuser:testpassword 0.0.0.0:8080 Hello world
The only thing left to add to these containers are SSL certificates and you’re good to go! You can find the code for the Nginx authentication proxy here and the container available here. A few things to note with this solution:
- using basic auth means that every request will hit the authentication API
- basic auth means your credentials will be sent in the clear unless you secure your connection with SSL. NOTE: never send basic auth credentials without SSL!!! Also, Docker’s client & registry doesn’t like basic auth over HTTP.
- access to the private registry will need to be restricted to the authentication proxy
Another interesting side effect of this solution is that enabling SSL on the private registry has reduced the amount of time it takes for each pull request as the client initially attempts to connect to port 443 before falling back to port 80 unless the port is specified in the registry URL.