Cómo segurizar la API de Docker con TLS

Cómo segurizar la API de Docker con TLS

En este artículo, te muestro como segurizar la API de Docker, esto es mucho muy importante si tenés corriendo tu host de Docker en Internet y tenés la API abierta.

Hace un par de semanas publiqué un pique que mostraba cómo habilitar la API en Docker, también hacia el comentario de que no era recomendable usarlo en producción ya que exponer la API de Docker directamente es un lindo problema de seguridad y un casi seguro dolor de cabeza en el futuro.

Hablé sobre cómo habilitar la API en este artículo:

Pique #19: Cómo exponer la API de Docker
Cómo habilitar el consumo de la API de Docker remotamente.

También les dejo un ejemplo de porque no esta bueno exponer la API así como así:

Threat Alert: Attacker Building Malicious Images Directly on Your Host
New attack exploits a misconfigured Docker API port to build and run a malicious container image on the host, rather than pull it from a public registry.

Con la seguridad en mente, te voy a mostrar cómo segurizar esa API, para que en el caso de que la tengas que exponerla hacia Internet por el motivo que fuera, no esté regalada para que cualquier atacante haga lo que quiera.

Cómo segurizar la API

Creando el certificado para el servidor

Lo que vamos a hacer es configurar unos certificados para poder exponer la API de manera segura usando TLS. La idea es que si el cliente no usa certificados del lado del cliente para autenticarse, no va a poder ejecutar contra la API.

Lo que vamos a usar será openssl para generar los certificados en el host donde corre Docker.

Primero, vamos a generar la llave privada para nuestra CA.

$ openssl genrsa -aes256 -out ca-key.pem 4096

La terminal nos devolverá algo como esto en donde debemos especificar una pass phrase.

Generating RSA private key, 4096 bit long modulus (2 primes)
.....................................................................................................................................................................................++++
.................................................................................................................................................................................................++++
e is 65537 (0x010001)
Enter pass phrase for ca-key.pem:
Verifying - Enter pass phrase for ca-key.pem:

Lo siguiente es generar una CA usando la llave privada que generamos recién:

$ openssl req -new -x509 -days 365 -key ca-key.pem -sha256 \
-out ca.pem

Nos pedirá la pass phrase de nuestra llave privada:

Enter pass phrase for ca-key.pem:

Y lo siguiente que debemos hacer es completar con información para generar un CSR para que se cree esa CA. La mia fue así, pero asegurense de poner vuestra información, sobre todo el Common Name.

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]:UY
State or Province Name (full name) [Some-State]:Montevideo
Locality Name (eg, city) []:Montevideo
Organization Name (eg, company) [Internet Widgits Pty Ltd]:CDUser
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:docker
Email Address []:tunombre@tudominio.com

Ya tenemos la llave privada y el certificate authority (CA), ahora debemos generar una llave del servidor y el CSR de nuestro certificado.

$ openssl genrsa -out server-key.pem 4096

La terminal devolverá algo como esto:

Generating RSA private key, 4096 bit long modulus (2 primes)
........................++++
.................................++++
e is 65537 (0x010001)

Vamos a generar el Certificate Sign Request (CSR)

$ openssl req -subj "/CN=docker" -sha256 -new \
-key server-key.pem -out server.csr

La terminal no nos devolverá nada.

Ahora lo que viene es firmar ese CSR con la CA que hicimos antes:

$ openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem \
-CAkey ca-key.pem -CAcreateserial -out server-cert.pem

El output de esto debería ser algo como esto, donde tendremos que poner el pass phrase de nuestra CA.

Signature ok
subject=CN = docker
Getting CA Private Key
Enter pass phrase for ca-key.pem:

Vamos a copiar los certificados y llaves que generamos recién hacia la carpeta /etc/ssl/certs.

$ mv server-cert.pem ca.pem server-key.pem /etc/ssl/certs/

Creando el certificado para el cliente

Este paso es esencial, ya que este certificado es el que usaremos para consumir la API de Docker de manera "autenticada".

Generemos la llave privada:

$ openssl genrsa -out key.pem 4096

El resultado será algo como esto:

Generating RSA private key, 4096 bit long modulus (2 primes)
................................++++
.........................................................................................................++++
e is 65537 (0x010001)

Lo siguiente es el CSR:

$ openssl req -subj '/CN=client' -new -key key.pem -out client.csr

La terminal no devolverá nada.

Lo siguiente es generar el certificado con esa llave privada que generamos recién, el CSR y que la firme la CA que generamos en el paso anterior:

$ openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem

La devolución será esta, donde, una vez más debemos especificar el pass phrase de nuestra CA.

Signature ok
subject=CN = client
Getting CA Private Key
Enter pass phrase for ca-key.pem:

Listo. Ya estamos prontos para configurar el servicio de Docker y que el mismo use nuestro certificado que creamos en la primera parte.

Configurando el Daemon de Docker para que use TLS

Ahora, vamos a configurar el daemon para que arranque con TLS habilitado y usando nuestros certificados, para eso vamos a editar el archivo docker.service.

$ sudo vim /lib/systemd/system/docker.service

Vamos a buscar la línea que empiece con ExecStart y lo vamos a editar para que quede de esta manera:

ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2376 --tlsverify --tlscacert=/etc/ssl/certs/ca.pem --tlscert=/etc/ssl/certs/server-cert.pem --tlskey=/etc/ssl/certs/server-key.pem

Guardamos, cerramos y hacemos un reload al daemon de Systemd, si Docker está corriendo, vamos a reiniciar o iniciarlo si está parado.

sudo systemctl daemon-reload
sudo systemctl restart docker

Vamos a probar si nos podemos conectar a la API

Ya tenemos todo listo, ahora nos queda probarlo. Para esto, vamos a hacerlo bien sencillo y usaremos curl para pegarle a la API.

La API se expone a través del puerto 2376, así que vamos a tratar de conectarnos sin usar certificado y veremos que no nos va a dejar establecer la conexión. Tratemos de obtener un listado de las imágenes de Docker que tengo en este servidor.

curl --insecure https://docker:2376/images/json
curl: (56) OpenSSL SSL_read: error:14094412:SSL routines:ssl3_read_bytes:sslv3 alert bad certificate, errno 0

Ahora probemos especificando el certificado de cliente y la llave privada.

curl --insecure \
--cert /home/ubuntu/cert.pem \
--key /home/ubuntu/key.pem \
https://docker:2376/images.json

La devolución es este hermoso JSON:

[{"Containers":-1,"Created":1599346280,"Id":"sha256:cab08c2b7b4155d6ef8e6af70503c7d96499ab3f1be427844ed5b42fb5556e39","Labels":{"build_version":"Linuxserver.io version:- 0.6.8-ls78 Build-date:- 2020-09-05T22:46:26+00:00","maintainer":"chbmb"},"ParentId":"","RepoDigests":["linuxserver/calibre-web@sha256:be9512dcc11684a967b7ed998623771fdcef5e0b3ddc3716769338d34d688c09"],"RepoTags":["linuxserver/calibre-web:latest"],"SharedSize":-1,"Size":367511975,"VirtualSize":367511975},{"Containers":-1,"Created":1599037264,"Id":"sha256:ea03cb7b07501e6a8bbf01ee5b02f61697a3db7a3712c166e982613a7cada581","Labels":{"build_version":"Linuxserver.io version:- v3.5.0-ls48 Build-date:- 2020-09-02T04:56:28-04:00","maintainer":"aptalca"},"ParentId":"","RepoDigests":["linuxserver/code-server@sha256:34fdafff2a1789736d50c288676635603426414cbc80939c7e30b4584b72c41b"],"RepoTags":["linuxserver/code-server:latest"],"SharedSize":-1,"Size":480299868,"VirtualSize":480299868},{"Containers":-1,"Created":1598864687,"Id":"sha256:a0a227bf03ddc8b88bbb74b1b84a8a7220c8fa95b122cbde2a7444f32dc30659","Labels":null,"ParentId":"","RepoDigests":["portainer/portainer-ce@sha256:0ab9d25e9ac7b663a51afc6853875b2055d8812fcaf677d0013eba32d0bf0e0d"],"RepoTags":["portainer/portainer-ce:2.0.0"],"SharedSize":-1,"Size":195546824,"VirtualSize":195546824},{"Containers":-1,"Created":1598527421,"Id":"sha256:6b367a5c4fe3e272ae9fb6cf8393856562c99a6eb41d2922c8ab75a3e37d1813","Labels":null,"ParentId":"","RepoDigests":["portainer/agent@sha256:372dd13917381fb98560c44db66e54b5f6b3a2087e6ae9777fdc101485cb8fc7"],"RepoTags":["portainer/agent:latest"],"SharedSize":-1,"Size":89848087,"VirtualSize":89848087},{"Containers":-1,"Created":1592248141,"Id":"sha256:70e63b36b36fbcbb3497d96dc2059180a9aa7669c9c10f6dcb1f82e7df1aac5f","Labels":null,"ParentId":"","RepoDigests":["paulczar/omgwtfssl@sha256:885a3ae5be1e63082147b16efbd4a43bae737626d1a052b336afa76c33b107c3"],"RepoTags":["paulczar/omgwtfssl:latest"],"SharedSize":-1,"Size":10023866,"VirtualSize":10023866}]

Para ir cerrando

Segurizar la API de Docker es mucho muy importante para no dejar la API expuesta a cualquier mal intencionado que encuentre nuestro host en Internet. Espero que les sirva y cualquier duda o consulta pueden dejarla en los comentarios.