En el artículo Habilita el API remoto de Docker explicaba cómo configurar el acceso remoto al API de Docker. El problema es que de esta forma no hay manera de restringir el acceso.

En este artículo protegemos el acceso usando TLS de manera que sólo se permitan conexiones que presenten un certificado firmado por una CA de confianza.

Seguiremos las instrucciones oficiales de Docker Protect the Docker daemon socket.

Creamos una CA, claves para el cliente y el servidor con OpenSSL

Primero, en la máquina host del Docker daemon, generamos las claves públicas y privadas de la CA (Certification Authority, la entidad certificadora):

# openssl genrsa -aes256 -out ca-key.pem 4096
Generating RSA private key, 4096 bit long modulus
............................................................................................................................................................................................................................................++
..........................++
e is 65537 (0x10001)
Enter pass phrase for ca-key.pem:
Verifying - Enter pass phrase for ca-key.pem:
#

Y a continuación:

# openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
Enter pass phrase for ca-key.pem:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:ES
State or Province Name (full name) [Some-State]:Barcelona
Locality Name (eg, city) []:Barcelona
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Ameisin
Organizational Unit Name (eg, section) []:DevOps
Common Name (e.g. server FQDN or YOUR name) []:192.168.1.20
Email Address []: {REDACTED}
#

Ahora que tenemos una CA, podemos crear la clave para el servidor y la petición de firmado del certificado (certificate signing request, CSR). Por favor, verifica que Common Name (es decir, el FQDN o YOUR Name) coincide con el nombre del host que vas a usar para conectar a Docker.

# openssl genrsa -out server-key.pem 4096
Generating RSA private key, 4096 bit long modulus
............................................................................................................................................................................................................................................................................................................................++
...............................................................++
e is 65537 (0x10001)
# openssl req -subj "/CN=192.168.1.20" -sha256 -new -key server-key.pem -out server.csr
#

A continuación vamos a firmar la clave pública con nuestra CA.

Como las conexiones TLS pueden realizarse usando la dirección IP o un nombre DNS, deben especificarse durante la creación del certificado.

# echo subjectAltName = IP:192.168.1.20,IP:127.0.0.1 > extfile.cnf
# openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
>   -CAcreateserial -out server-cert.pem -extfile extfile.cnf
Signature ok
subject=/CN=192.168.1.20
Getting CA Private Key
Enter pass phrase for ca-key.pem:
#

Autenticación del cliente

Para autenticar al cliente, crearemos una clave de cliente y una petición de firmado del certificado.

Para simplificar, los siguientes dos pasos pueden realizarse desde la máquina donde se encuentra el Docker daemon.

# openssl genrsa -out key.pem 4096
Generating RSA private key, 4096 bit long modulus
...............................................................................++
................................................................................................................................................................................++
e is 65537 (0x10001)
# openssl req -subj '/CN=client' -new -key key.pem -out client.csr
#

Para que la clave permita autenticar al cliente, creamos un fichero de configuración de extensiones:

# echo extendedKeyUsage = clientAuth > extfile.cnf
#

Ahora firmamos la clave:

# openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \
>   -CAcreateserial -out cert.pem -extfile extfile.cnf
Signature ok
subject=/CN=client
Getting CA Private Key
Enter pass phrase for ca-key.pem:
#

Después de haber generado cert.pem y server-cert.pem podemos eliminar las peticiones de firmado:

# ls
ca-key.pem  ca.srl    client.csr  extfile.cnf  server-cert.pem	server-key.pem
ca.pem	    cert.pem  key.pem     server.csr
# rm -v client.csr server.csr
removed ‘client.csr’
removed ‘server.csr’
#

Protección de las claves

Con una máscara umask por defecto de 022 las claves secretas que hemos generado dan a todo el mundo acceso de lectura y de escritura a tu usuario y tu grupo.

Para proteger las claves de daños accidentales, vamos a eliminar los permisos de escritura sobre ellas. Para hacerlas de sólo lectura para tu usuario, usamos:

# chmod -v 0400 ca-key.pem key.pem server-key.pem
mode of ‘ca-key.pem’ changed from 0644 (rw-r--r--) to 0400 (r--------)
mode of ‘key.pem’ changed from 0644 (rw-r--r--) to 0400 (r--------)
mode of ‘server-key.pem’ changed from 0644 (rw-r--r--) to 0400 (r--------)
#

Los certificados pueden ser leídos por todo el mundo, pero para evitar daños accidentales, mejor eliminamos los permisos de escritura:

# chmod -v 0444 ca.pem server-cert.pem cert.pem
mode of ‘ca.pem’ changed from 0644 (rw-r--r--) to 0444 (r--r--r--)
mode of ‘server-cert.pem’ changed from 0644 (rw-r--r--) to 0444 (r--r--r--)
mode of ‘cert.pem’ changed from 0644 (rw-r--r--) to 0444 (r--r--r--)
#

Configurando el API de acceso remoto de forma segura

Para hacer que el Docker daemon sólo acepte conexiones de clientes que proporcionen un certificado de confianza de tu CA.

Para ello, modificamos las opciones de arranque del daemon de Docker:

# nano /lib/systemd/system/docker.service

Modificamos la línea:

ExecStart=/usr/bin/dockerd -H fd:// -H=0.0.0.0:2375

De manera que quede como (lo he dividido en varias líneas por claridad):

ExecStart=/usr/bin/dockerd --tlsverify 		\
         --tlscacert=/root/ca.pem 		\
         --tlscert=/root/server-cert.pem 	\
         --tlskey=/root/server-key.pem 		\
         -H=0.0.0.0:2376 			\
         -H fd://

A continuación, recargamos la configuración y reinciamos el servicio:

# systemctl daemon-reload
# systemctl restart docker

Los primeros intentos de arrancar el daemon han fallado; ha sido necesario especificar la ruta completa a los certificados y las claves para conseguir que el servicio arrancara.

Finalmente, comprobamos que podemos acceder usando el certificado con curl:

# curl https://192.168.1.20:2376/version --cert /root/cert.pem --key /root/key.pem --cacert /root/ca.pem
{"Version":"17.05.0-ce","ApiVersion":"1.29","MinAPIVersion":"1.12","GitCommit":"89658be","GoVersion":"go1.7.5","Os":"linux","Arch":"amd64","KernelVersion":"3.16.0-4-amd64","BuildTime":"2017-05-04T22:04:27.257991431+00:00"}
#

A diferencia de lo que pasaba antes, cuando se intenta acceder a https://192.168.1.9:2376/version desde otro equipo (sin usar el certificado), obtenemos un error:

This site can’t be reached
192.168.1.9 refused to connect.