Skip to main content

2.1 Host

Información

Cuando hablamos de Host nos referimos a cualquier máquina que esté corriendo el demonio de Docker y sea la capa que se interpone entre el demonio y la propia infraestructura.

En muchos entornos productivos, se suelen usar herramientas que permiten la orquestación de contenedores de una forma más sencilla, por ejemplo Kubernetes, Docker Swarm, Openshift... Cada una de esas herramientas tiene su propio enfoque de orquestación de contenedores y sus propias recomendaciones de seguridad y de buenas prácticas. Nosotros en este capítulo queremos centrarnos en un entorno simple y puro, donde solo contamos con el sistema operativo del host y el demonio de Docker para orquestar nuestros contenedores.

Muchas de las recomendaciones y estrategias que veremos a continuación son fácilmente portables a sistemas de orquestación de contenedores.

2.1.1 Bastionado

Aunque el bastionado es un proceso ciertamente complejo, podemos plantear al menos dos escenarios bien diferenciados a la hora de trabajar con Docker en entornos de desarrollo modernos (Agile, DevOps…).

Un primer entorno podría ser local. La máquina que se utiliza para las actividades de desarrollo de software, QA, etc.. donde esa máquina suele utilizarse como ordenador personal/trabajo con entorno gráfico, acceso a aplicaciones de mensajería como Slack, etc... y que no se dedica al despliegue de aplicaciones para recibir tráfico de fuera. Este entorno que se suele denominar local es donde más volatilidad y dinamismo tenemos con respecto a los contenedores, ya que los equipos suelen trabajar con diferentes contenedores y relaciones entre ellos, especialmente si se usan microservicios o se desarrollan librerías.

Otro entorno podría ser aquella(s) máquina(s) donde desplegamos los contenedores que esperamos sean estables a lo largo del tiempo y que soporten tráfico de internet o realicen un trabajo productivo definido. Este tipo de entornos suelen ser máquinas específicas que se usan solo para esto y que normalmente suelen ir evolucionando y modificando su estado a lo largo del tiempo, a través de trabajos y pipelines de integración continua o despliegue continuo. En entornos de trabajo moderno solemos encontrarnos al menos con 2 entornos staging y live. Siendo live el más crítico para nuestros objetivos empresariales y de seguridad.

Progresión de entornos: local, development, staging y finalmente liveProgresión de entornos: local, development, staging y finalmente live

En muchas ocasiones, no prestamos la suficiente atención al entorno local que por su naturaleza distinta para cada individuo, hace que sea difícil de armonizar y bastionar desde un punto de vista IT / CISO. Un primer consejo sería el hacer uso de máquinas virtuales dentro de nuestro entorno local que nos permitan aislar mucho más el impacto de seguridad que pueda tener una mala configuración de Docker en nuestra máquina host.

Muestra como los contenedores y docker se ejecutan dentro de una máquina virtual que vive dentro de la máquina hostMuestra como los contenedores y docker se ejecutan dentro de una máquina virtual que vive dentro de la máquina host

Además sumariamos esta ventaja adicional, ya que podríamos desplegar esos contenedores en nuestra máquina virtual, compartiendo el mismo sistema operativo de nuestro entorno de live o staging, evitando así posibles limitaciones de Docker con MacOs o Windows. Obviamente este enfoque nos obliga a tener una máquina con recursos suficientes, y también cierto manejo de máquinas virtuales y su ciclo de vida, así como los volúmenes compartidos o los recursos entre host <-> virtual host <-> docker <-> container. Este tipo de enfoque es especialmente interesante cuando queremos tener un entorno más de laboratorio para explorar imágenes de terceros o cuando no confiemos en el contenido de las mismas

Independientemente de usar máquinas virtuales o no, deberemos seguir ciertas recomendaciones.

Actualizaciones

Es fundamental mantener nuestro host actualizado tanto el sistema operativo como el demonio de Docker. Históricamente la actualización del demonio de Docker podría arrastrar caídas de servicio ya que los contenedores se paran cuando paramos Docker (para su actualización). Un modo sencillo para evitarlo es hacer uso de la capacidad live-restore que no para los contenedores al cerrarse el demonio y que reconecta con ellos una vez se levanta el demonio de nuevo.

dockered --live-restore

Antivirus

Cada vez es más común en el mundo empresarial el uso de antivirus. En la propia documentación de Docker se menciona que los escaneos sobre las carpetas que usan los contenedores pueden generar errores inesperados. La solución en este caso sería excluir los directorios de Docker pero hacer escáneres cuando los contenedores no están corriendo, usando cron jobs o una planificación adecuada.

2.1.2 Gestión de disco

Particiones específicas para los contenedores

Aunque Docker cuenta con un sistema bastante eficiente de gestión de las imágenes usando chunks y hashes, es muy habitual que nuestros contenedores ocupen cada vez más y más espacio en disco. Una buena recomendación sería mover la ubicación por defecto que usa Docker para la permanencia en disco var/lib/docker a un disco o partición independiente donde podamos hacer un buen mantenimiento y monitoreo del espacio.

Purgar Docker regularmente

Por la naturaleza de Docker, y de los contenedores efímeros, es común ocupar el espacio en disco con elementos que ya no son útiles. Una forma de purgar es usando el comando prune de docker y sus diversas variaciones.

Si todos los contenedores y sus elementos relacionados (volúmenes, redes, imágenes..) son prescindibles podemos hacer:

# Paramos todos los contenedores
docker kill $(docker ps -q)
# Borramos todos los contenedores
docker rm $(docker ps -a -q)
# Purgamos el sistema completamente (incluyendo volúmenes)
docker system prune --volumes

2.1.3 El poder de root

Si profundizamos en los pilares de Docker (capítulo 1.2) es fácil llegar a la conclusión de que en realidad al compartir el kernel si somos root en el contenedor también lo somos en el host.

Siendo la premisa siempre evitar la ejecución de un contenedor como root.

Repeat after me: “friends don’t let friends run containers as root!” Node.js Docker Cheat Sheet

Root siempre es Root

Si no hacemos nada de forma proactiva o utilizamos un orquestador de contenedores bien configurado, es bastante probable que estemos lanzando los contenedores como root:

docker run -it busybox
/ # id
uid=0(root) gid=0(root) groups=10(wheel)

Contenedores Rootless

Si hacemos uso de Linux namespaces y la documentación oficial de Docker vemos que ya existen formas para poder ejecutar un contenedor sin ser root. En algunas ocasiones las imágenes que usamos ya definen este comportamiento usando la instrucción USER (capítulo 3.1.2), pero otras veces no y por ello necesitamos hacer uso del argumento --user del demonio de Docker, como veremos a continuación.

Modo non-root (con el usuario nobody):

docker run -it --user nobody busybox
/ # id
uid=65534(nobody) gid=65534(nobody)

De todas formas si utilizamos un orquestador como Kubernetes, encontramos que ya existen mecanismos que nos permiten gestionar este tipo de casos, simplemente ajustando las especificaciones de seguridad:

spec:
securityContext:
runAsNonRoot: true

Demonio de Docker Rootless

Desde la versión 19 de Docker, podemos hacer uso del modo rootless a nivel del demonio y desde la 20 se considera estable. Este modo tiene algunas limitaciones, pero tiene algunas ventajas que no podemos dejar escapar sin más.

Así pues, la forma de instalarlo sería casi idéntica al demonio de Docker convencional (ejecutalo como usuario sin privilegios) aunque tendremos que instalar previamente otras dependencias como newuidmap y newgidmap:

curl -sSL https://get.docker.com/rootless | sh

Lo que nos ofrece esta versión del demonio de Docker, es poder ejecutar el demonio bajo un usuario sin privilegios, lógicamente este comportamiento también se extiende a los contenedores que maneja el demonio.

Ahora bien, tiene algunas desventajas que aparecen al perder los privilegios. Como cabría esperarse no se podrá usar --net=host, algunos drivers de almacenamiento dejarán de funcionar y además perderemos capacidades de exponer puertos privilegiados (< 1024) entre otras..

Pero no deja de ser una buena opción si no te limita la pérdida de privilegios, especialmente en entornos productivos.

2.1.4 Monitorizar

Aún no siendo un entorno productivo, es siempre importante monitorizar nuestros contenedores para detectar anomalías que podrían alertarnos de potenciales vulnerabilidades.

Si contamos con pocos contenedores o no necesitamos una visión a largo plazo siempre podemos hacer uso del comando stats de Docker para sacar las métricas de nuestros contenedores

En el capítulo 4 hablamos sobre herramientas que pueden ayudarnos a gestionar múltiples aspectos:

  • Con Dockprom (capítulo 4.9) podemos monitorizar nuestros contenedores fácilmente y de una forma visual con Grafana pero además contamos con la posibilidad de incluir alertas
  • Con K6 (capítulo 4.10) podemos fácilmente detectar problemas a la hora de escalar nuestros contenedores, especialmente con servicios web. Para entender la fragilidad a la que nos encontramos, expuestos cuando no hacemos pruebas de carga las vulnerabilidades como CWE-1333: Inefficient Regular Expression Complexity basadas en una Expresión regular deficiente, pueden provocar una denegación de servicio
  • Revisar los logs de nuestros contenedores es también importante ya sea usando los drivers de Docker o alguna solución más compleja.

2.1.5 Parches de seguridad

Security is a process, not a product. Bruce Schneier

Cuando damos el paso de usar contenedores, añadimos más capas a nuestra infraestructura. Ya que la virtualización es un ladrillo más en la pared que vamos construyendo.

Esto supone un reto a la hora de parchear y actualizar ya que cada parte (Sistema operativo, sistema de orquestación, imágenes, librerías...) deberá ser actualizado de forma frecuente.

Aunque es un proceso que parece sencillo, está intrínsecamente lleno de retos especialmente si estamos manteniendo sistemas arcaicos que hace uso de interfaces antiguas o obsoletas.

Esto se ha convertido en un problema cada vez más importante y ganado importancia en el OWASP top 10 (A09-2017 Using Components with Known Vulnerabilities y A06-2021 Vulnerable and Outdated Components) ya que el tiempo pasa y las vulnerabilidades descubiertas siguen creciendo en nuestra contra.

consejo

La mejor forma de abordar este tema es darle el peso que se merece y crear un plan sostenible en el tiempo que permita de una forma automatizada nos ayude con la tediosa tarea de actualizar y parchear de una manera frecuente a medida que las vulnerabilidades van apareciendo en nuestro extenso sistema de dependencias.