PSA: cuidado con los puertos expuestos en Docker
Docker es una tecnología increíble y prevalece en el flujo de trabajo de casi todos los desarrolladores de software. Es útil para crear entornos idénticos y compartirlos entre desarrollo, pruebas, producción y otros. Es una excelente manera de enviar un entorno de software confiable entre sistemas o incluso a los clientes. Sin embargo, como con cualquier tecnología, uno debe saber cómo estar seguro al usarla.
De forma predeterminada, Docker interactúa con otros contenedores de Docker a través de la red de Docker. Docker Compose hace que sea muy fácil crear redes Docker y vincularlas entre sí, para imitar entornos de producción donde cada servicio está aislado en su propia red y solo interactúa a través de interfaces definidas. Por ejemplo, considere el siguiente sistema:
- Aplicación A
- Base de datos
- API de aplicación
- Aplicación B
- Base de datos
- Almacén de caché
- Interfaz de usuario de la aplicación
Al separar cada uno en su propia red Docker, los sistemas permanecen aislados entre sí. Sin embargo, cuando la aplicación B necesita comunicarse con la aplicación A, las dos redes deben estar vinculadas. Esto se logra diciéndole a Docker que proporcione una interfaz de red al Contenedor de UI de la aplicación que también existe en la red para la Aplicación A. En esta configuración, los Contenedores Docker involucrados permanecen tan seguros como las aplicaciones que se ejecutan dentro de los contenedores. No hay puertos expuestos fuera de Docker Networks.
Ahora considere que la interfaz de usuario de la aplicación *no* está bajo un contenedor Docker, que está completamente configurado para ayudar a los desarrolladores a escribir los proyectos de interfaz de usuario de la aplicación en su sistema local. O considere que el sistema se está utilizando para probar funciones. Esto se puede hacer en una computadora portátil local o en un sistema de escritorio, o incluso se puede hacer en un servidor en la nube. Sin embargo, para lograr esto, se debe exponer un puerto del contenedor al host local.
Docker ofrece varias formas de lograr esto:
- A través de la línea de comandos «docker», hay varias opciones (-p, -P)
- A través de la configuración de Dockerfile usando el comando EXPOSE
- A través de Docker Compose Configuration usando el atributo EXPOSE
Los tres funcionan básicamente de la misma manera: configuran las reglas del firewall del sistema local para exponer el puerto especificado usando el formato: “[ip:] : ”. Esto tiene el efecto de permitir que las cosas fuera del Docker Container accedan a las cosas dentro del Docker Container pero solo en el puerto especificado.
¿Cual es el problema?
Ahora considere que está siendo consciente de sus propios sistemas y está ejecutando un firewall. Por ejemplo, considere un sistema Debian que usa UFW como su administrador de firewall. Para proteger el sistema, ha hecho lo siguiente:
$ sudo apt-get install ufw
$ sudo ufw permitir OpenSSH
$ sudo ufw habilitar
En este punto, espera que el *único* acceso a través del firewall sea el Puerto 22 para el acceso remoto a través de SSH. ¿Esperaría que la exposición de un puerto en Docker pasara por alto la configuración de su firewall? ¿O esperaría que, como administrador del sistema, necesitara agregar una regla de firewall para permitir el acceso al contenedor Docker *si* desea que entidades externas accedan a él?
Desafortunadamente, resulta que Docker se integra con el firewall del sistema de tal manera que exponer un puerto desde un contenedor lo expone a través del firewall al mundo exterior. Además, la forma en que Docker interactúa con los firewalls es esencialmente invisible para la mayoría de las herramientas de firewall, a menos que esté interactuando directamente con las aplicaciones de firewall sin procesar (por ejemplo, IPTables).
En Linux, Docker crea un conjunto de cadenas Netfilter para administrar su red Docker. Cuando se expone un puerto desde un contenedor, las cadenas relacionadas se muñen para permitir el acceso al puerto. De manera predeterminada, esto asigna el puerto a la dirección IPv4 0.0.0.0 y efectivamente hace dos cosas:
- Expone el puerto a través del firewall al mundo exterior.
- Evita que cualquier otro contenedor de Docker en el sistema local pueda exponer el *mismo* puerto.
El número 2 es una molestia pero no una amenaza para la seguridad. Es molesto porque si uno quiere trabajar en múltiples proyectos y utilizar Docker solo para la infraestructura relacionada, entonces las configuraciones deben configurarse para que no entren en conflicto entre sí.
El número 1, sin embargo, es una amenaza para la seguridad, especialmente porque si uno confía en herramientas como UFW para verificar el estado de su firewall, entonces no mostrará que Docker Container se está haciendo visible a través del firewall para el mundo exterior. De acuerdo, hay muchas, muchas herramientas de firewall diferentes, y sería imposible que Docker se integre con todas ellas; sin embargo, los usuarios aún deben tener el control de lo que realmente pasa a través del firewall hacia el mundo exterior y deben tomar una decisión concienzuda para exponer el puerto a través del firewall.
¿Cómo puedo mitigar esta amenaza?
Inicialmente, los desarrolladores de Docker lanzaron la funcionalidad `DOCKER_USER` para permitir a los usuarios mitigar esto; sin embargo, esto requiere que los usuarios sepan cómo administrar su firewall por sí mismos. Esto puede ayudar a mitigar algunos; sin embargo, las reglas de firewall son notoriamente difíciles de acertar, por lo que hay una razón por la cual las personas emplean herramientas como UFW para administrar las reglas de firewall. Por lo tanto, esta es realmente una solución inaceptable. Además, como https://github.com/docker/for-linux/issues/810 ilustra, una interrupción en la funcionalidad DOCKER_USER dejará a los usuarios completamente expuestos nuevamente.
Se puede mitigar un poco esto cambiando la configuración de Docker Daemon para usar una dirección de host local, una en el rango 127.0.0.0/8 o, alternativamente, haciendo lo mismo en cualquiera de los métodos enumerados anteriormente para exponer un puerto. Esto funciona como una mitigación, pero requiere una intervención activa para hacerlo. Si no lo hace, deja el sistema abierto a una amenaza de seguridad.
La mejor manera de ver y confirmar completamente qué puertos están expuestos a otros sistemas es realizar un escaneo de puertos desde otro host. La herramienta de facto para esto es nmap, y está disponible para la mayoría de los sistemas; casi todas las distribuciones de Linux la proporcionan. Los binarios preconstruidos también están disponibles para Windows y MacOS. Se puede realizar un escaneo de puertos exhaustivo usando nmap de la siguiente manera:
$ sudo nmap -p1-65535
Donde ‘ ‘ es la dirección IP del sistema. Ejecutar como un usuario normal (sin sudo, o con una cuenta que no sea root) también funcionará, pero llevará más tiempo.
NOTA: Cuando utilice recursos en la nube o incluso en sus propias redes corporativas, tenga cuidado de conocer las políticas de seguridad aplicables. Muchas organizaciones no aprecian que se realice un escaneo de puertos sin su conocimiento. Esto puede incluso conducir a malos resultados, ya que los sistemas de seguridad pueden identificar el sistema de escaneo como una amenaza y bloquearlo.
Si desea realizar un seguimiento de este problema, síganos en el Rastreador de problemas de Docker en https://github.com/moby/moby/issues/22054