Using TLS with the Docker engine

If you already use Docker, you probably know that, by default, the Docker engine’s REST API is accessible via a UNIX socket which is created when the daemon starts.  This allows the local Docker client (or any Docker API-compliant client) to interact with the Docker containers on that engine instance, but is limiting if you need to use a client which is not local to the Docker engine host.

With one simple setting change to the Docker daemon, you can add one or more TCP endpoints as API listeners for the Docker daemon, but given the Docker API is not multi-tenant nor does it have any authentication built-in, once you add a TCP-based listener, any client with reachability to the daemon’s IP address and that TCP port has full control of all containers–including the ability to create and start new ones–on that Docker engine instance.

While user authentication and multi-tenancy are open discussions in the Docker community without a currently defined design or reference in the official roadmap, in the interim you can add some security while exposing a TCP endpoint on your Docker engine using TLS security.

While official project documentation exists for these TLS options, I thought it might be useful to look at the three basic modes of operation with TLS and what each of them require from a configuration and setup perspective.

Step 1: TLS enabled daemon, no verification on either server or client

The first step enables TLS communication between the client and daemon API server, but doesn’t perform any CA verification or client certificate validation.  This is really only useful if you want to protect the stream of bytes being passed during API communication with TLS between client and server.  It doesn’t solve the issue of limiting who or what can access the TCP socket for the API server.

To get started you will need to create the proper certificates–I’m personally using a simple self-signed certificate with a CA I created for these examples.  Once you have created your own you can start the daemon with the options shown below.  (Note that a full walk-through of creating these files using the openssl utility is described in the Docker daemon TLS article.)

$ docker -d -H tcp://ubuntuvm:2376 --tls \
    --tlskey ~/docker-tls/server-key.pem \
    --tlscert ~/docker-tls/server-cert.pem

Using the above command, the Docker daemon should start up and begin listening for API requests over the TCP port specified.  In this example we are using port 2376 (the recommended TCP port for TLS listed in the documentation), enabling TLS with the --tls flag, and providing paths to the previously created server key and certificate.  If you followed the documentation properly to create the certificate and key, you should now be able to verify that the daemon is listening over TLS:

$ docker -H tcp://ubuntuvm:2376 --tls version
Client:
 Version: 1.8.0-dev
 API version: 1.20
 Go version: go1.4.2
 Git commit: c8523d7-dirty
 Built: Fri Jul 17 04:04:51 UTC 2015
 OS/Arch: linux/amd64

Server:
 Version: 1.8.0-dev
 API version: 1.20
 Go version: go1.4.2
 Git commit: c8523d7-dirty
 Built: Fri Jul 17 04:04:51 UTC 2015
 OS/Arch: linux/amd64

You can also verify that if you don’t specify --tls on the client side, you receive an error message:

$ docker -H tcp://ubuntuvm:2376 info
Get http://ubuntuvm:2376/v1.20/info: malformed HTTP response "\x15\x03\x01\x00\x02\x02".
* Are you trying to connect to a TLS-enabled daemon without TLS?
* Is your docker daemon up and running?

At this point your Docker daemon is TLS enabled, listening over a specified TCP port.  However, at this stage there are no restrictions regarding access to this TCP port, so it will be up to you to protect this new TCP-based API listener in some other way (for example, via firewall rules).  Now we’ll take the next step, adding validation of the server’s certificate against the CA we specify.

Step 2: TLS-enabled daemon, verify server certificate/CA

In step 2, we take our setup one step further and add client flags to verify the server certificate is signed by the CA we specify, which will also require that the “common name” (or CN) of the server certificate matches the hostname.  If you weren’t paying attention during the certificate creation time you may encounter problems here, but you can go back and read the documentation to make sure you set up the common name properly in the certificate Subject field.  This step provides a similar promise to what your browser offers when you visit a secure website: assuming the browser shows the proper icon/validation notice, you can trust you are visiting the site your browser URL bar claims you are visiting.  However, as noted in the prior step, any client for which the IP address of the Docker engine is reachable may still connect to the API server, and each client connection can choose whether or not to verify the server’s certificate is signed by a specified CA.  However, in our case we wish to prove that we can validate the server against a provided CA, so the following command adds the verify flag as well as a directive to the ca.pem file:

$ docker -H tcp://ubuntuvm:2376 \
    --tls --tlsverify \
    --tlscacert ~/docker-tls/ca.pem info

Now, you might be getting tired of adding flags to various commands, so it’s probably a good time to talk about ways to configure these settings by default.  First of all, any time you want to use a TCP socket connection to any Docker daemon, you can use the environment variable DOCKER_HOST to configure this in your shell.  Secondly, if you always want TLS verification to be enabled, you can set the environment variable DOCKER_TLS_VERIFY to any non-empty value.  Finally, you can place client certificates (which we will discuss in the next step) and CA certificates in your $HOME/.docker directory where they will be loaded by default.  Note that the current default names of these files are ca.pem, cert.pem, and key.pem.  If the files are in the correct location but not named properly they will not be found automatically.  See the following for an example of the same case as above, but with these added shortcuts:

$ export DOCKER_HOST=tcp://ubuntuvm:2376
$ export DOCKER_TLS_VERIFY=1
$ cp ~/docker-tls/ca.pem ~/.docker/
$ docker info
Containers: 24
Images: 563
Storage Driver: aufs
 Root Dir: /var/lib/docker/aufs
 Backing Filesystem: extfs
 Dirs: 729
 Dirperm1 Supported: false
Execution Driver: native-0.2
Logging Driver: json-file
Kernel Version: 3.13.0-58-generic
Operating System: Ubuntu 14.04.2 LTS
CPUs: 2
Total Memory: 3.858 GiB
Name: ubuntu
ID: N2WA:XTB6:KIOA:6NHR:MM5G:SG3V:GNUV:T6PR:AU2U:2N5H:S7DB:TBB2
Username: estesp
Registry: https://index.docker.io/v1/

Step 3: TLS-enabled daemon, client and server verification enabled

Finally, in step 3 we add the missing puzzle piece that completes the picture: TLS-enabled API traffic over TCP, with client certificate validation: verifying that clients are using certificates signed by our CA.  Of course, this also includes the functionality from step 2 where the server certificate is also verified against the same CA, as long as we continue using the right client flags or the DOCKER_TLS_VERIFY environment variable.  This gives us fully controlled and protected TLS communications between clients holding our CA-signed certificates and the server.  We must restart our daemon with this added capability now:

$ docker -d -H tcp://ubuntuvm:2376 --tls \
--tlskey ~/docker-tls/server-key.pem \
--tlscert ~/docker-tls/server-cert.pem \
--tlsverify \
--tlscacert ~/docker-tls/ca.pem

Note that we have added a pointer to the CA certificate and added the --tlsverify flag to the daemon startup command.  At this point if we change nothing on our client side we should get the following error because we haven’t provided a client certificate.  Side note: this error used to be more cryptic, but PR #13779 clarified it so it is more easily understandable as a client-side certificate error.

$ docker info
The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: Get https://ubuntuvm:2376/v1.20/info: remote error: bad certificate

To remedy this we need a client certificate signed by the CA we specified on both client and daemon sides, which we can either provide via command line flags, or now that we know about putting the files in $HOME/.docker we can simply copy the client certificate and key files to that location and try again:

$ cp {cert,key}.pem ~/.docker/
$ docker info
# we should see the output of `docker info` here

If you see the output from docker info, congratulations!  You have successfully configured a Docker daemon listening on TLS with validation of certificates on both ends.  Of course, clearly we have not provided any authentication or multi-tenancy by adding this capability, but we have provided a means to limit access to our TLS-enabled API server to only client’s holding a certificate signed by our CA.  As the Docker documentation on this feature notes, this requires protecting client certificates as you would a root password, because any holder of the certificate will now have full access to your Docker daemon.

You may also like...

1 Response

  1. April 8, 2016

    […] You could set up a TCP endpoint for your host-located Docker daemon that can be accessed from any container. The downside is that the Docker API is now accessible from any system which has a route to your host.  You could protect it with firewall/iptables rules, but containerizing the proxied TCP endpoint effectively handles this for you. If you want to use a TCP endpoint, make sure you consider using TLS certificate access to the endpoint, which I describe in another post here on my blog. […]

Leave a Reply

Your email address will not be published. Required fields are marked *