Automatizando la gestión de los certificados SSL para nuestros contenedores en Docker

A veces instalamos servicios web en nuestros dockers caseros y estos van sin https. Esto es un error bastante serio por varios motivos, el principal es la seguridad, pero también hay otros como posibles problemas de compatibilidad. En este artículo comentaré como tener estos servicios bajo https.

Code. Autor: Markus Spiske
Code

Hoy voy a escribir el que quizá sea el artículo más técnico del blog hasta la fecha. Pero creo que la comunidad de #selfhosted lo agradecerá.

Lo primero de todo es aclarar que no estoy inventando la rueda, tan solo estoy recopilando documentación que he leído y algún pequeño aderezo de mi propia cosecha.

¿Qué vamos a montar?

Nginx es un conocido servidor web que con algunas modificaciones puede utilizarse como un proxy inverso, es decir, una puerta de entrada única hacia varios servicios web. Centrando el tiro en el mundo de Docker, lo que hacemos es tener varios contenedores con diversas aplicaciones web, pero en vez de tener esos contenedores expuestos a internet, lo que se tiene es solo un contenedor llamado nginx-proxy y será este el que pase el tráfico hacia los contenedores de las aplicaciones web.

Esto tiene sus ventajas, tanto de seguridad, como la que nos ocupa hoy aquí: poder gestionar los certificados SSL de manera automatizada con el contenedor de nginx-proxy y acme-companion.

Así que lo que hoy lograremos será que cuando levantes un contenedor con una aplicación web, este sea automáticamente https. Sí, automáticamente, sin que hagas nada. Incluso las renovaciones también serán automágicas.

Pasos previos

Esto no es un tutorial para principiantes que busquen montar todo desde 0. Aquí voy a dar por supuesto que Docker está funcionando sin problemas, y ya tienes alguna aplicación web ejecutándose y expuesta a internet. Si no es así, hay muchos y muy buenos tutoriales en internet en la lengua de Cervantes que te explican paso por paso lo que tendrás que hacer.

Dicho esto, sí hay un par de pasos previos. El primero es altamente recomendable y es segmentar las redes de nuestros contenedores. Es decir, que lo que vaya a tener tráfico desde/hacia Internet sea independiente del resto de contenedores. Para ello tendremos que crear una red en Docker llamada "proxy-network":

docker network create proxy-network

Y el segundo paso es obligatorio o de lo contrario Docker no podrá levantar el contenedor de nginx-proxy. Debes parar todos los contenedores que estén utilizando los puertos 80 y 443. Recuerda que ahora todo ese tráfico debe pasar por el proxy, así que será él el que reciba todas las peticiones y se encargará de redirigirlas a los contenedores implicados en responder.

Instalando nginx-proxy y acme-companion

Vamos a instalar primero el proxy.

docker run -d \ 
    --name nginx-proxy \
    --net proxy-network \
    -p 80:80 \
    -p 443:443 \
    -e TZ="Europe/Madrid" \
    -v ~/docker_volumes/nginx-proxy/certs:/etc/nginx/certs \
    -v ~/docker_volumes/nginx-proxy/vhost:/etc/nginx/vhost.d \
    -v ~/docker_volumes/nginx-proxy/html:/usr/share/nginx/html \
    -v /var/run/docker.sock:/tmp/docker.sock:ro \
    --restart unless-stopped \
    nginxproxy/nginx-proxy:1.5

Algunos comentarios:

  • El huso horario he puesto Europe/Madrid, esto es útil para que la hora en los logs sea la correcta. Aunque cada uno puede poner su timezone.
  • A mi me gusta tener los volúmenes en mi home dentro del directorio docker_volumes, pero esto puedes hacerlo como quieras, si es importante que estos tres volúmenes los tengas montados en el host.
  • La propia documentación de Nginx dice que no se utilice la imagen latest en entornos productivos porque no es una versión estable. Mi recomendación es utilizar una no-menor, por ejemplo la 1.5.

Ahora le llega el turno a acme-companion, que es el contenedor que se encarga de hacer la supervisión de los certificados utilizando Let’s Encrypt.

docker run -d \
    --name acme-companion \
    --net proxy-network \
    -e TZ="Europe/Madrid" \
    -v /var/run/docker.sock:/var/run/docker.sock:ro \
    -v ~/docker_volumes/acme-companion:/etc/acme.sh \
    --volumes-from nginx-proxy \
    --env "DEFAULT_EMAIL=mi@mail.com" \
    --restart unless-stopped \
    nginxproxy/acme-companion:2.3

Comentarios:

  • De nuevo el timezone y el directorio para el volumen a elección de cada uno.
  • Importante que monte los mismos volúmenes del contenedor de "nginx-proxy" (volumes-from).
  • La variable DEFAULT_EMAIL debe contener un email válido, será necesario para la validación inicial y te avisará también de caducidades de certificados que no se renueven.
  • Aquí la versión la estoy cerrando a la 2.3. Acme companion tiene menos actividad, pero prefiero utilizar una versión estable.

Con esto ya lo tendríamos todo hecho, ahora vamos a levantar alguna aplicación de ejemplo.

Iniciando contenedores que reciban tráfico a través del proxy

Voy a poner un par de ejemplos para ver en acción nuestro nuevo sistema. Imaginemos que queremos montar nuestro cloud privado utilizando Nextcloud, para ello haríamos lo siguiente:

docker run -d \
    --name nextcloud \
    --net proxy-network \
    --env "VIRTUAL_HOST=micloud.midominio.com" \
    --env "VIRTUAL_PORT=80" \
    --env "LETSENCRYPT_HOST=micloud.midominio.com" \
    --env "LETSENCRYPT_EMAIL=mi@mail.com" \
    ...
    nextcloud:latest

Comentemos lo anterior:

  • IMPORTANTE: NO se debe especificar el parámetro -p, más adelante la explicación.
  • Como es lógico, el contenedor debe estar en nuestra red "proxy-network", de lo contrario nginx-proxy será incapaz de pasarle tráfico.
  • Las variables de entorno VIRTUAL_HOST y VIRTUAL_PORT las utilizará nginx-proxy para saber que tráfico debe pasar al contenedor y porque puerto. Si en Nextcloud se indica que para levantar el contenedor hay que poner -p 80:80, el VIRTUAL_PORT será 80. Con esto estamos diciéndole al proxy que deberá enviar el tráfico al puerto 80 del contenedor, por tanto no necesitamos mapearlo hacia un puerto exterior. Si esto no lo entiendes, te recomiendo que mires la documentación de Docker sobre como funciona el mapeo de puertos.
  • Y las variables LETSENCRYPT_HOST y LETSENCRYPT_EMAIL las utilizará acme-companion para generar el certificado y exponerlo a las peticiones que gestione nginx-proxy.
  • Como dije más arriba, no estoy entrando en los pasos previos de como configurar tu dominio para redirigirlo a un router y que este a su vez le pase el tráfico al host donde esté Docker.

Hecho esto podríamos acceder a https://micloud.midominio.com y llegaríamos a nuestro contenedor haciendo uso del proxy y con los certificados correctos.

Vamos a poner otro ejemplo con Grafana:

docker run --detach \
    --name grafana \
    --net proxy-network \
    --env "VIRTUAL_HOST=grafana.midominio.com" \
    --env "VIRTUAL_PORT=3000" \
    --env "LETSENCRYPT_HOST=micloud.midominio.com" \
    --env "LETSENCRYPT_EMAIL=mi@mail.com" \
    ...
    grafana/grafana

Fíjate que sigue siendo lo mismo, solo que ahora he cambiado el subdominio y el puerto. Aquí conseguiríamos que nginx-proxy cuando reciba una petición para grafana.midominio.com, se la pase al contenedor grafana al puerto 3000.

Siguientes pasos

Con esta configuración habrás conseguido tener comunicaciones cifradas desde internet hacia tus servicios web, todo ello olvidándote de la gestión tediosa de generar y administrar los certificados. Pero aquí no acaba la cosa, como todo servicio expuesto a internet es susceptible a ataques, no te pienses que tu no. Así que todavía quedaría securizar el proxy pero lo dejaré para el siguiente post, mientras tanto asegúrate de tener correctamente configuradas las aplicaciones que levantes.