Exposing your Docker API

As I wrote some weeks ago in “How to live inside a docker container?”, when you pass a docker socket to your container, you’re essentially enabling the container full access to your Docker host OS. This means that you can create containers, pull images, or more destructively - stop and delete containers and images.

What is the problem?

For those of you familiar with RESTful APIs, you know a simple rule - GET requests should not modify data, POST should. The Docker APIs follow this principle in the sense that creating, updating, deleting or stopping containers is a POST request. Just by limiting the request method to only GET, you’re limiting the damage that full access can do.

That being said, there are also other commands, which are better left disabled for read-only clients. For example, you can view the process list of a container via the ps API call. You can attach the stdout and stderr of a container with the logs API call. You can even export a container over the export API call. When it comes to things you definetly don’t want to do - these are all on the list.

How to limit access?

I’ve written an application in Go, called docker-proxy-acl. Similar to existing reverse proxies like nginx or haproxy, it forwards requests from clients to the back end. The difference? It works only on UNIX sockets. And it’s even possible to run it from a Docker image, with no additional pitfalls.

The application registers it’s own unix socket which listens for requests. The socket is configurable with the command line, but it’s by default available in /tmp/docker-proxy-acl/docker.sock. The application takes arguments for which endpoints to enable, a number of them are supported.

You can then forward this socket to any container. The container has all the benefits of isolation, and the docker host is exposed only in limited capacity - depending on which endpoints you enable with the command line.

You can enable an endpoint with the -a argument. Currently supported endpoints are:

  • containers: opens access to /containers/json and /containers/{name}/json.
  • networks: opens access to /networks and /networks/{name}
  • info: opens access to /info
  • version: opens access to /version
  • ping: opens access to /_ping

How to run it within docker?

It’s very possible to run the whole thing from a docker image. The only requirement is that you use the docker -v option to mount the full docker.sock, and to mount the location /tmp/docker-proxy-acl/docker.sock as well. Everything will be then created and run within docker.

In fact, the ./run script packaged with docker-proxy-acl already is using docker image golang to run the application. Everything is already set up to run it and create this ACL controlled socket file. It’s very easy to test this. Run the server just with -a version.

$ ./run -a version
Registering version handlers
[docker-proxy-acl] Listening on /tmp/docker-proxy-acl/docker.sock

In another console we can test some requests against the new socket file:

$ echo -e "GET /version HTTP/1.0\r\n" | nc -U /tmp/docker-proxy-acl/docker.sock
HTTP/1.0 200 OK
Content-Type: application/json
Date: Mon, 25 Apr 2016 19:59:44 GMT
Content-Length: 200

{"Version":"1.10.0","ApiVersion":"1.22","GitCommit":"590d5108","GoVersion":"go1.5.3","Os":"linux","Arch":"amd64","KernelVersion":"3.13.0-68-generic","BuildTime":"2016-02-04T18:36:33.411858653+00:00"}

As you can see, the request is being returned for this allowed API endpoint. As a test we can try a POST request against it:

$ echo -e "POST /version HTTP/1.0\r\n" | nc -U /tmp/docker-proxy-acl/docker.sock
HTTP/1.0 400 Bad Request
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Mon, 25 Apr 2016 19:59:49 GMT
Content-Length: 36

400 Bad request ; only GET allowed.

The request gets block already due to the request method not being GET. And we return 404 on not-enabled requests:

$ echo -e "GET /_ping HTTP/1.0\r\n" | nc -U /tmp/docker-proxy-acl/docker.sock
HTTP/1.0 404 Not Found
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Mon, 25 Apr 2016 19:59:37 GMT
Content-Length: 19

404 page not found

That’s pretty secure if you ask me. Less things to worry about.

If you want to enable several endpoints, you can combine multiple -a arguments: -a version -a info -a ping....

Wrapping up

Once the server creates the new docker.sock file, you can pass this to your containers which might need it. One option is to pass it to docker-proxy to expose some subset of the API over HTTP. Another option is to pass it to applications which retrieve information about containers from the Docker API. There are many of them: jwilder/nginx-proxy, boomfire/haproxy-docker, or the underlying jwilder/docker-gen which is a base for many others.

The main usage for the application is to provide the translation between the Id of the container, which is available in the cgroups filesystem, to a readable name, the one specified with –name, or which is generated automatically.

While I have you here...

It would be great if you buy one of my books:

I promise you'll learn a lot more if you buy one. Buying a copy supports me writing more about similar topics. Say thank you and buy my books.

Feel free to send me an email if you want to book my time for consultancy/freelance services. I'm great at APIs, Go, Docker, VueJS and scaling services, among many other things.