martes, octubre 16, 2018

Docker Distroless Images: Cómo crear imágenes sin SO sistema ni shell de comandos (1 de 2)

Al par que escribimos este artículo nos encanta Docker, por eso hemos hablado aquí de sus aplicaciones y bondades más de una vez, como por ejemplo "Cómo montar con Docker un entorno de pentesting parte 1" y parte 2 y nuestro querido WPM (WordPress in Paranoid Mode) montado sobre una instalación Docker, WPM en Docker parte 1 y parte 2, además de que nos animamos a escribir el libro de "Docker: SecDevOps" que tenéis disponible en 0xWord. Esta vez vamos a hablaros de las imágenes sin sistema operativo, una forma más limpia y segura de desplegar tus aplicaciones con Docker.

Figura 1: Docker Distroless Images:
Cómo crear imágenes sin SO sistema ni shell de comandos

Las imágenes son la base fundamental de un contenedor, de hecho, un contenedor es una “imagen viva”, por lo que la optimización de la misma es crucial, no sólo por motivos de recursos, sino también, como veremos, por motivos de seguridad.

Introducción

Como ya sabemos, el fichero Dockerfile es clave ya que éste será utilizado por Docker usar para la creación de imágenes. Por eso aquí se centran las buenas prácticas a la hora de conseguir optimizar, tanto en tamaño como en seguridad, nuestra imagen base.

Figura 2: Imagen Docker de WordPress in Paranoid Mode en GitHub

Por ejemplo, el orden de las instrucciones dentro de este fichero es realmente importante a la hora de aprovechar la caché de las capas que Docker mantiene. Así como el número de instrucciones dentro del fichero Dockerfile, ya que Docker crea una capa para cada instrucción. Por eso, si las colocamos de forma ordenada y optimizada (cuantas menos instrucciones mejor) dentro del Dockerfile, esto afectará positivamente tanto a la ejecución como al tamaño final de la imagen. En este enlace puedes encontrar más información y buenas prácticas.

Figura 3: Capas Docker y contenedores

Otra buena práctica es la exclusión de ficheros innecesarios dentro del contexto de Docker cuando creamos una imagen. Para ello se utiliza el fichero .dockerignore, en el cual indicamos los ficheros que no necesitamos incluir en dicho contexto. Por supuesto, evitar instalar paquetes que no necesitemos es otra buena práctica básica a la hora de crear una imagen Docker. En los repositorios oficiales de Docker puedes encontrar ejemplos “ejemplares” (valga la redundancia) de ficheros Dockerfile totalmente optimizados, como por ejemplo este para la imagen de Go o este otro para Ruby.

¿Y qué tiene que ver esto con la seguridad?

Pues si conseguimos imágenes pequeñas y especializadas, centradas en sólo una función o aplicación, se reducen los vectores de ataque, así como el tráfico de red y por lo tanto el riesgo. Un atacante en nuestro sistema lo primero que intentará conseguir es una shell, para desde aquí ejecutar lo que le interese, pivotar o ejecutar movimientos laterales (para obtener la máxima información posible del sistema), exfiltración de datos, buscar persistencia, etcétera. Es decir, las técnicas habituales de las que tantas veces han escrito nuestros compañeros Chema Alonso y Palbo González, y que tenéis descritas en el libro de Ethical Hacking.

Además, de esta forma también conseguimos reducir los CVEs, al no tener sistema evitamos vulnerabilidades, excepto claro está, aquellas relacionadas con el intérprete o el runtime. Esto también reduce de manera drástica las actualizaciones del sistema y, por tanto, el mantenimiento completo de toda la arquitectura montada. Y es aquí donde entra en juego las imágenes sin sistema o “Distroless Images”.

Figura 4: Google Container Tools "Distroless"

El proyecto Google Container Tools nos ofrece imágenes Docker centradas en lenguajes de programación como Java, Python, Go, Node.js, .Net, Rust o Lenguaje D, pero sin contener ningún sistema operativo.  Sólo contienen el intérprete o runtime con las dependencias necesarias para poder ejecutar la aplicación, además de las librerías necesarias para el uso de SSL. Es más, estas no contienen ni siquiera una shell del sistema (ya hemos eliminado el primer vector de ataque que hemos descrito antes, acceso a una shell). A día de hoy, estas son las imágenes disponibles:
· gcr.io/distroless/python2.7
· gcr.io/distroless/python3
· gcr.io/distroless/nodejs
· gcr.io/distroless/java
· gcr.io/distroless/java/jetty
· gcr.io/distroless/cc
(incluye versión mínima de glibc para los lenguajes D o Rust)· gcr.io/distroless/dotnet
Alpine es la distribución GNU/Linux más utilizada a la hora de conseguir crear imágenes de un tamaño lo más reducido posible, llegando incluso a menos de 5MB. Si comparamos las imágenes de la versión oficial por ejemplo de Python con las ofrecidas por Google Container Tools vemos que existen una diferencia de unos casi 30MB entre ellas (que no es poco):

Figura 5: Comparación entre Pyhton Distrolesss y Pyhton oficial Alpine

Pero ahora vamos a ver un ejemplo práctico comparativo entre un contenedor corriendo una aplicación Java vulnerable sobre una imagen Alpine vs otra imagen Distroless.

Un ejemplo Vulnerable App en Distroless Docker

La aplicación vulnerable será https://github.com/tuxotron/vulnj, una aplicación Java construida con Spring Boot. Tiene un parámetro “secreto”, cmd, que nos permite la ejecución de comandos en el sistema.

Figura 6: Aplicación Vulnerable Java en GitHub

Por otro lado hemos creado dos imágenes Docker: tuxotron/vulnj:alpine y tuxotron/vulnj:distroless. La primera cómo su etiqueta indica está creado sobre una imagen Alpine y la segunda sobre gcr.io/distroless/java. Para hacer las pruebas vamos a desplegar ambos contenedores en un entorno kubernetes (minikube), crearemos también un secreto con las credenciales súper secretas que usa la aplicación.

Lo primero es tener instalado minikube y arrancado tal y cómo se explica en este enlace: (https://kubernetes.io/docs/tasks/tools/install-minikube/) Por cierto, si tienes minikube en ejecución dentro de una máquina virtual, puedes ejecutarlo con el siguiente comando, así evitarás el error de "VBoxManage not found":
sudo minikube --vm-driver none start
Comprobemos el estado:
$ minikube status 
minikube: Running
cluster: Running
kubectl: Correctly Configured: pointing to minikube-vm at 192.168.99.100
A continuación, crearemos el secreto, dos deployments (uno para cada contenedor) y dos servicios (uno para cada deployment):
$ kubectl create -f kube/secret.yaml \
-f kube/alpine-deployment.yaml \
-f kube/distroless-deployment.yaml \
-f kube/alpine-service.yaml \
-f kube/distroless-service.yaml
secret/mysecret created
deployment.apps/vulnj-alpine-deployment created
deployment.apps/vulnj-distroless-deployment created
service/vulnj-alpine-service created
service/vulnj-distroless-service created
Esto debería habernos creado 2 pods, 2 deployments, 2 servicios y el secreto. Para ver la dirección a las que kubernetes ha asignado a nuestros servicios:
$minikube service list
Figura 7: Comprobación de las direcciones IP asignadas por kubernetes a los servicios

Cómo podemos ver, al servicio de Alpine le asignado la dirección: http://192.168.99.100:32446 y al Distroless http://192.168.99.100:31735.

Una vez que están montados los dos servicios toca hacer las pruebas de seguridad e intentar explotar las vulnerabilidades de nuestra Aplicación Vulnerable en ambos entornos, en el entorno Alpine y en el entorno Distroless. Si te animas, ves probando tú antes de leer la segunda parte del artículo.

Autores:

Fran Ramírez, (@cyberhadesblog) miembro del equipo de Crazy Ideas en CDO en Telefónica, co-autor del libro "Microhistorias: Anécdotas y Curiosidades de la historia de la informática (y los hackers)", del libro "Docker: SecDevOps" y del blog Cyberhades.

Rafael Troncoso (@tuxotron) es DevOps Tech Lead en USCIS/DHS, co-autor del libro "Microhistorias: Anécdotas y Curiosidades de la historia de la informática (y los hackers)", del libro "Docker: SecDevOps" y del blog Cyberhades.

No hay comentarios:

Publicar un comentario