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.
2 Responses
[…] 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. […]
[…] Read this guide to using TLS with the Docker engine. (@Docker) […]