Securing Docker With TLS Certificates

14 Jul 2016

By default, Docker has no authentication or authorization for its API, instead relying on the filesystem security of its UNIX socket, /var/run/docker.sock, which by default is only accessible by the root user.

This is fine for the basic use case of only accessing the Docker API on the local machine via the socket as the root user. However if you wish to use the Docker API over TCP, you'll want to secure it so you don't have to give out root access to anyone that happens to poke you on the TCP port.

Docker supports using TLS certificates (both on the server and the client) to provide proof of identity. When set up correctly it will only allow clients and servers with a certificate signed by a specific CA to talk to eachother.

While not providing fine grained access permissions, it does at least allow us to listen on a TCP socket and restrict access with the bonus of also providing encryption.

In this post, I will detail what is required to secure Docker running on a CoreOS server. I will assume you already have a CoreOS server set up and running. If not, check out this previous Deis blog post covering CoreOS and VirtualBox.

Creating Certificates

I will cover two ways of creating certificates.

The first involves using openssl to create a CA and then sign a key and certificate pair. The second way uses the paulczar/omgwtfssl Docker image, which automates the certificate creation process for you.

Either way, you'll want to start off by creating the directories for both the server and client certificate sets, like so:

$ sudo mkdir -p /etc/docker/ssl
$ mkdir -p ~/.docker

Here, we're creating the keys and certificates on the server itself. Ideally, you would do this on your laptop or via config management, and never store the CA key on a public server.

OpenSSL

Let's look at the OpenSSL way first.

Run openssl to create and sign a CA key and certificate:

$ openssl genrsa -out ~/.docker/ca-key.pem 2048
.+++
..........................................................................................................+++
e is 65537 (0x10001)
$ openssl req -x509 -new -nodes -key ~/.docker/ca-key.pem \
    -days 10000 -out ~/.docker/ca.pem -subj '/CN=docker-CA'
$ ls ~/.docker/
ca-key.pem  ca.pem

Then, move the CA certificate to the /etc/docker/ssl directory:

$ sudo cp ~/.docker/ca.pem /etc/docker/ssl

Next, create the ~/.docker/openssl.cnf file.

Put this inside:

[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth

This is a configuration file for OpenSSL.

Once that's done, we need a configuration file for the Docker server. So create the /etc/docker/ssl/openssl.cnf file and put this inside:

[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = docker.local
IP.1 = 172.17.8.101
IP.2 = 127.0.0.1

Add any DNS or IPs that you might use to access the Docker server with. This is critical, as the Golang SSL libraries are very strict.

Next, create and sign a certificate for the client:

$ openssl genrsa -out ~/.docker/key.pem 2048
....................................+++
.............+++
e is 65537 (0x10001)
$ openssl req -new -key ~/.docker/key.pem -out ~/.docker/cert.csr \
    -subj '/CN=docker-client' -config ~/.docker/openssl.cnf
$ openssl x509 -req -in ~/.docker/cert.csr -CA ~/.docker/ca.pem \
    -CAkey ~/.docker/ca-key.pem -CAcreateserial \
    -out ~/.docker/cert.pem -days 365 -extensions v3_req \
    -extfile ~/.docker/openssl.cnf
Signature ok
subject=/CN=docker-client
Getting CA Private Key

Then do the same for the server:

$ sudo openssl genrsa -out /etc/docker/ssl/key.pem 2048
................................................................................+++
....................................+++
e is 65537 (0x10001)
$ sudo openssl req -new -key /etc/docker/ssl/key.pem \
    -out /etc/docker/ssl/cert.csr \
    -subj '/CN=docker-server' -config /etc/docker/ssl/openssl.cnf
$ sudo openssl x509 -req -in /etc/docker/ssl/cert.csr -CA ~/.docker/ca.pem \
    -CAkey ~/.docker/ca-key.pem -CAcreateserial \
    -out /etc/docker/ssl/cert.pem -days 365 -extensions v3_req \
    -extfile /etc/docker/ssl/openssl.cnf
Signature ok
subject=/CN=docker-client
Getting CA Private Key

OMGWTFSSL

If all that looks too complex, you may want to take another approach.

The paulczar/omgwtfssl image is a small (< 10MB) Docker image built specifically for creating certificates for situations like this. Let's see how to use it.

First we'll create our client certificates and use a Docker volume binding to put the CA and certificates into the ~/.docker directory, like so:

$ docker run --rm -v $(pwd)/.docker:/certs \
    paulczar/omgwtfssl
----------------------------
| OMGWTFSSL Cert Generator |
----------------------------

--> Certificate Authority
====> Using existing CA Key ca-key.pem
====> Using existing CA Certificate ca.pem
====> Generating new config file openssl.cnf
====> Generating new SSL KEY key.pem
Generating RSA private key, 2048 bit long modulus
.............+++
..........+++
e is 65537 (0x10001)
====> Generating new SSL CSR key.csr
====> Generating new SSL CERT cert.pem
Signature ok
subject=/CN=example.com
Getting CA Private Key

Next we'll move them and fix the ownership:

$ sudo cp ~/.docker/ca.pem /etc/docker/ssl/ca.pem
$ chown -R $USER ~/.docker

Now we can create the server certificates using the same CA. We do this with a second volume binding to /etc/docker/ssl, like so:


$ docker run --rm -v /etc/docker/ssl:/server \
    -v $(pwd)/.docker:/certs \
    -e SSL_IP=127.0.0.1,172.17.8.101 \
    -e SSL_DNS=docker.local -e SSL_KEY=/server/key.pem \
    -e SSL_CERT=/server/cert.pem paulczar/omgwtfssl
----------------------------
| OMGWTFSSL Cert Generator |
----------------------------

--> Certificate Authority
====> Using existing CA Key ca-key.pem
====> Using existing CA Certificate ca.pem
====> Generating new config file openssl.cnf
====> Generating new SSL KEY /server/key.pem
Generating RSA private key, 2048 bit long modulus
.................................+++
..................+++
e is 65537 (0x10001)
====> Generating new SSL CSR key.csr
====> Generating new SSL CERT /server/cert.pem
Signature ok
subject=/CN=example.com
Getting CA Private Key

Since this is a server certificate we need to pass the IP and DNS that the server may respond to via the -e arguments.

Once this is done, we have our TLS certificates created, owned by the correct user, and in the correct locations.

Using the TLS certificates with Docker

Now we need to tell Docker to use the TLS certificate and verify the client. You can do this by creating a drop-in systemd unit that modifies the existing Docker systemd unit.

Create the file custom.conf in /etc/systemd/system/docker.service.d/ and add the following text:

[Service]
Environment="DOCKER_OPTS=-H=0.0.0.0:2376 -H unix:///var/run/docker.sock --tlsverify --tlscacert=/etc/docker/ssl/ca.pem --tlscert=/etc/docker/ssl/cert.pem --tlskey=/etc/docker/ssl/key.pem"

If you want to restrict local users from using the Docker UNIX socket, remove the second -H argument. If you already have a custom drop-in unit, you can add the -H and --tls* arguments to it.

Reload systemd and the Docker service:

$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

Now, when you try to access Docker via the TCP port you should get a TLS error:

$ docker -H tcp://127.0.0.1:2376 info
Get http://127.0.0.1:2376/v1.21/containers/json: malformed HTTP response "\x15\x03\x01\x00\x02\x02".
* Are you trying to connect to a TLS-enabled daemon without TLS?

This is because the Docker client doesn't know to use TLS to communicate with the server.

So, let's set some environment variables to enable TLS for the client and use the client key we created, like so:

$ export DOCKER_HOST=tcp://127.0.0.1:2376
$ export DOCKER_TLS_VERIFY=1
$ export DOCKER_CERT_PATH=~/.docker

Check it worked by running:

$ docker info
docker info
Containers: 0
Images: 0
Server Version: 1.9.1
Storage Driver: overlay
 Backing Filesystem: extfs
Execution Driver: native-0.2
Logging Driver: json-file
Kernel Version: 4.3.3-coreos
Operating System: CoreOS 899.1.0
CPUs: 1
Total Memory: 997.4 MiB
Name: core-01
ID: RGVQ:VDUC:Z5LU:IE7I:J6UJ:TFBJ:SSCO:EWG2:QKAW:5FY6:EIAV:MROK

If you got a message like that, TLS is set up correctly and working!

Wrap Up

In this post we looked at two ways of generating TLS key and certificate pairs that allow you to secure Docker with authentication and encryption. This allows us to use the Docker API over TCP with a significantly reduced security risk.

This post originally appeared on Paul Czarkowski's blog.

Posted in Docker, Security

triangle square circle

Did you enjoy this post?