domingo, 22 de septiembre de 2019

Descubre cómo de inseguro es tu contenedor de Docker con Trivy

En muchas ocasiones, cuando se están desarrollando microservicios y desplegándolos mediante contenedores de Docker lo que se tiene en cuenta a la hora de elegir la imagen base del Dockerfile es puramente funcional. Se suele buscar que sea una imagen oficial del repositorio de DockerHub que tenga lo que se necesita para que la aplicación corra, y a ser posible que no sea excesivamente pesada.

Sin embargo, durante el paso del desarrollo al despliegue, así como sí que se suelen tener en cuenta cuestiones de seguridad como los puertos expuestos, los volúmenes que se montan, permisos de usuario, etc. No suele prestarse mucha atención a la imagen que se usa como base del contenedor. Aquí es donde hay que tener en cuenta que el contenido del contenedor es parte de la superficie de ataque de un cluster Kubernetes. Por eso es tan importante tener bien controlados los otros puntos de acceso (puertos, versiones antiguas, volúmenes, permisos, etc.) como lo que el desarrollador mete dentro del contenedor. Un contenedor inseguro en el que pueda escalar privilegios y acceder al nodo anfitrión puede comprometer todo el cluster.

Un ejemplo de esto es la vulnerabilidad CVE-2019-5021 que fue reportada en mayo de 2019 por la que la mayoría de las imágenes que usaban la base Alpine en DockerHub venían con usuario root con contraseña vacía. Esto hacía posible una escalada de privilegios haciendo el contenedor inseguro y facilitando la el escape a la máquina anfitrión.


Usando Trivy para encontrar vulnerabilidades en imágenes Docker

Trivy es una herramienta opensource que cuenta con una extensa base de datos de vulnerabilidades en imágenes de Docker basándose en los sistemas y paquetes que tienen instalados. Esto permite escanearlas y generar informes de seguridad.

La compañía de seguridad Aqua Security la adquirió hace poco más de un mes (agosto 2019) aun teniendo anteriormente su propia herramienta de escáner de vulnerabilidades, y ha asegurado que continuará siendo opensource y que de hecho acabará sustituyendo a la propia de la compañía.

Su uso es muy sencillo. Tras instalarlo, no hay más que invocarlo con el nombre de la imagen que se quiere auditar y genera un informe de este tipo: 

Ejemplo de informe de vulnerabilidades en imagen Docker con Trivy

 

Escaneando con Trivy un contenedor Docker con diferentes imágenes base

Para probar el resultado de utilizar una imagen Docker u otra a la hora de hacer un microservicio en un contenedor he creado una pequeña aplicación de ejemplo en Python que abre un "Hola Mundo" en local en el puerto 5000. Para crear el contenedor he creado varios Dockerfile utilizando diferentes imágenes como base. 

Si queréis realizar los pasos que he seguido en este artículo podéis descargaros la aplicación,  los Dockerfile e instrucciones de instalación del repositorio de GitHub de este artículo con el siguiente comando:

git clone https://github.com/daviddetorres/jugandocontrivy.git

Las imágenes base de Docker que he utilizado son todas públicas del repositorio de DockerHub:
  • python:3.5-alpine
  • python:3.5.7-alpine3.10
  • python:3.5-stretch
  • python:3.5-buster

Los resultados de los informes los podéis encontrar en carpeta "informes" del repositorio. Como curiosidad podemos ver que python:3.5-alpine utiliza Alpine v3.9.4, y python:3.5.7-alpine3.10 la v3.10 (como era de esperar). En el primer caso se encuentran 10 vulnerabilidades en la imagen, de las cuales 5 son de tipo alto. Sin embargo, en el caso de la versión v3.10 sólo se detectan 6 y ninguna de tipo alto (4 medio y 2 bajo).

pythonapp-alpine-python35 (alpine 3.9.4)
========================================
Total: 10 (UNKNOWN: 0, LOW: 1, MEDIUM: 4, HIGH: 5, CRITICAL: 0)


pythonapp-alpine310-python357 (alpine 3.10.2)
=============================================
Total: 6 (UNKNOWN: 0, LOW: 2, MEDIUM: 4, HIGH: 0, CRITICAL: 0)


Este es un ejemplo bastante claro como el simple hecho de elegir entre una imagen genérica y otra con de una versión determinada puede generar un contenedor inseguro en producción.

Las otras dos imágenes con las que hemos hecho imágenes no suelen utilizarse tanto, sobre todo por su peso. Aún así, es interesante ver cómo han salido los informes de seguridad de estas imágenes.

En el caso de la imagen con base python:3.5-stretch (Debian v9.11), el resultado ha sido un número sorprendentemente alto de vulnerabilidades. Nada menos que 3.494, de las que 11 son críticas y 606 altas.

pythonapp-stretch-python35 (debian 9.11)
========================================
Total: 3494 (UNKNOWN: 4, LOW: 134, MEDIUM: 2739, HIGH: 606, CRITICAL: 11)


La imagen con base python:3.5-buster (Debian v10.1) ha salido mejor parada, pero aún así es mucho más insegura que las imágenes de Alpine. En este caso se han detectado 1.184 vulnerabilidades en la imagen, de las cuales 7 son críticas y 95 son altas.

pythonapp-buster-python35 (debian 10.1)
=======================================
Total: 1184 (UNKNOWN: 3, LOW: 70, MEDIUM: 1009, HIGH: 95, CRITICAL: 7)


Algunos consejos de seguridad que podemos extraer de este experimento

El primero que podemos extraer es que no todas las imágenes que se utilizan como base en el Dockerfile son iguales. Hemos visto como la diferencia entre utilizar una imagen python:3.5-alpine (sin especificar la versión) y python:3.5.7-alpine3.10 es importante, sobre todo en las 5 vulnerabilidades altas que elimina la segunda opción. 

La segunda conclusión a la que podemos llegar es que cuantos más componentes tenga una imagen de base, más vulnerabilidades puede tener. Esto, en principio hace que Alpine tenga menos que las otras basadas en Debian. Sin embargo debemos hacernos la siguiente pregunta: ¿De verdad para este microservicio necesitamos consola, por ejemplo? Quizás para desarrollo sí, pero para producción no. 

En este sentido, han aparecido imágenes Docker sin distribución (distroless). En otro artículo profundizaré un poco más en ellas, pero la idea es que la imagen sólo contiene las librerías necesarias para el microservicio. Por ejemplo, la máquina Java, Python, etc. pero no el resto de componentes que pueden comprometer la imagen convirtiéndolo en producción en un contenedor inseguro y comprometiendo el cluster. 


lunes, 16 de septiembre de 2019

¿Son tus YAML compatibles con la nueva API de Kubernetes v1.16?

Esta ya casi todo listo para que, en lo que queda de septiembre, se publique Kubernetes v1.16, y en esta ocasión viene con algunos cambios en el API que pueden requerir la modificación de ficheros de configuración de nuestros clusters orquestados con Kubernetes.

La API de Kubernetes va cambiando la versión de los diferentes recursos conforme van madurando y pasando de alphas a betas y de estas a estables, o reorganizándolos en diferentes puntos de entrada. Estos cambios no son de una versión para otra, sino que se anuncian al menos con 3 versiones de antelación en el caso de las beta y se marca una versión de obsolescencia para esos recursos o el cambio en el punto de entrada a la API de los mismos.


¿Qué recursos cambian en el API en Kubernetes v1.16?

Los recursos que cambian de punto de entrada de la API  de esta nueva versión son los siguientes: 
El cambio más visible de cada uno de ellos es el punto del API al que se interroga para acceder a estos recursos. ¿En la práctica qué significa? Pues que en el fichero de yaml de configuración de cada uno de estos recursos, en la línea de donde se define el campo "apiVersion" habrá que poner el nuevo punto de acceso al API. Concretamente:

  • Todos los recursos que se sirvan desde "apps/v1beta1" y "apps/v1beta2" pasarán a servirse desde "apps/v1"
  • NetworkPolicy pasa de estar en "extensions/v1beta1" a "networking.k8s.io/v1"
  • podsecuritypolicies pasa de "extensions/v1beta1" a "policy/v1beta1"

Por ejemplo, un yaml de un deployment en la v1.14 podría tener la siguiente pinta:

apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: ejemplo-nginx
  labels:
    app: nginx
spec:
  replicas: 5
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80

Como veis, en este ejemplo el campo apiVersion tiene valor "apps/v1beta2". Este valor ya se anunció en las notas de versión de v1.14 y v1.15 que en esta versión v1.16 iba a dejar de servirse en ese punto de la API y se debían cambiar por "apps/v1".

Estos cambios de la API en versión beta a versión estable en algunas ocasiones pueden suponer cambios también en la estructura de los ficheros. Campos que cambian de nombre, que desaparecen, que aparecen... Para cada uno de ellos deberemos consultar la documentación correspondiente.


¿Qué tengo que hacer para asegurarme de que todo seguirá funcionando?

Aunque a alguno se le puede pasar por la cabeza quedarse como está, no actualizar a Kubernetes v1.16 no es una opción. Así que (además de otras acciones que deben tenerse en cuenta), habrá que cambiar al menos el campo apiVersion de todos los recursos que estén aún utilizando puntos de entrada al API antiguos.

Esto puede ser suficiente en el caso de ingress. Pero en el caso de los deployments, donde hay algunos campos que cambian, en el blog de Kubernetes recomiendan utilizar el comando kubectl convert. Un ejemplo de como convertir un deployment de app/v1beta2 a app/v1:

kubectl convert -f ./fichero-a-convertir.yaml --output-version apps/v1

Es posible que no tengáis muchos ficheros que cambiar, ya que como he comentado antes este es un cambio que se anunció en v1.14 y v.1.5, pero si no lo hacéis corréis el riesgo de que los recursos configurados en ellos dejen de funcionar.

En el caso de que no podáis cambiarlos, no estéis seguros de que están todos cambiados o simplemente, por requerimientos del sistema debáis mantener estos puntos de entrada en el API durante un tiempo, existe la opción de re-activarlos temporalmente (hasta la v1.18) mediante los siguientes flags en la opción --runtime-config del API Server:
  • apps/v1beta1=true
  • apps/v1beta2=true
  • extensions/v1beta1/daemonsets=true,extensions/v1beta1/deployments=true,extensions/v1beta1/replicasets=true,extensions/v1beta1/networkpolicies=true,extensions/v1beta1/podsecuritypolicies=true



¿Cómo puedo probar la nueva versión del API de Kubernetes v1.16?

Como publicaron en el foro de Kubernetes, hay una versión de MicroK8s con v1.16-beta1. Esto os puede permitir probar configuraciones y despliegues para ver si todo sigue funcionando antes de mandarlo a producción. 

Para instalarlo desde cero: 

snap install microk8s --classic --channel 1.16/beta

O si tenéis ya una instalación de MicroK8s, podéis actualizar la versión:
snap refresh microk8s --channel 1.16/beta




Podéis encontrar más detalles en: 

viernes, 13 de septiembre de 2019

Las 5 vulnerabilidades graves que encontró Kubernetes en la auditoría de seguridad

El pasado 6 de agosto Kubernetes publicó los resultados de la auditoría de seguridad que realizó la empresa Trail of Bits entre los meses de marzo y mayo. Entre los documentos publicados están:
  • Artículo de seguridad en Kubernetes, en el que se analiza la superficie de ataque de Kubernetes, la arquitectura de seguridad, los tipos de ataques según su origen y recomendaciones para administradores y desarrolladores. 
  • Documento de amenazas, donde se describen tanto la metodología como las 17 amenazas encontradas (en otro artículo analizaré algunas de ellas).
  • Informe final de la auditoría de seguridad. Aquí es donde se detallan cada una de las 37 vulnerabilidades de seguridad encontradas. 

La auditoría de seguridad

La auditoría se hizo sobre Kubernetes v1.13.4 y se realizaron tanto inspecciones de código manuales como automáticas, así como despliegues de clusters en local como en la nube. Para ello utilizaron Kubespray, ya que permitía configuraciones consistentes entre el entorno local y el de la nube. 

De entre las 37 vulnerabilidades encontradas, vamos a analizar las 5 de mayor gravedad. El resto se reparte en 17 de nivel medio, 8 de nivel bajo y 7 informativas. En el issue #81146 se listan las diferentes vulnerabilidades, el estado de la resolución y el issue de cada una


El tipo HostPath de PersistentVolumes permite saltarse la directiva PodSecurityPolicy

En Kubernetes, PodSecurityPolicy es uno de los recursos que permite al admission controller decidir si un pod puede crearse por parte de un service account o no dependiendo de la configuración que tenga. Por ejemplo, si en un PodSecurityPolicy no se permiten pods en modo privilegiado, cualquier pod que intente crearse en modo privilegiado desde esa service account dará un error. 

Esto normalmente funciona, pero en la auditoría de seguridad se encontró un caso en el que, aún teniendo restringido la definición del pod el montaje de volúmenes hostPath (este tipo de volumen monta un directorio del nodo anfitrión haciéndolo accesible desde dentro del contenedor), si se hace en lugar de con un volumen normal con un volumen persistente, a través de un PersistentVolumeClaim esta restricción no se tienen en cuenta.

El efecto es que cualquiera podría montar un directorio de la máquina huésped desde el contenedor y tener acceso al sistema de ficheros, consiguiendo escapar del contenedor. En el apéndice C del informe final hay un ejemplo con los yalm y código necesario para la prueba de concepto de la vulnerabilidad. 

A corto plazo la solución de esta vulnerabilidad ha sido documentar que las PodSecurityPolicy no limitan los tipos de volúmenes persistentes, y que estos deben de darse acceso sólo a usuarios confiables y está cerrado para la v1.16.


No se puede revocar un certificado sin revocar todos los de la red

Los diferentes servicios de Kubernetes utilizan certificados X.509 para asegurar la autenticación, autorización y seguridad del transporte de datos entre ellos. Es el servidor API el que ejerce de entidad certificadora, firma y manda los certificados del resto de servicios. 

El problema viene cuando uno de los nodos queda comprometido (por una intrusión, un uso sospechoso de recursos, comportamiento extraño de un contenedor...). En el caso de que se sospeche que el certificado puede haberse visto comprometido no se puede revocar uno de los certificados individualmente, sino que hay que revocar toda la cadena de certificados del sistema, volver a generarlos y volver a mandarlos a los diferentes nodos y servicios. 

La solución pasará por tener una lista de certificados revocados. De momento se está proponiendo la implementación de OCSP stampling. Esto implicaría tener un servidor de certificados en el que se podría revocar individualmente el que se quisiera y que estamparía la fecha y hora del certificado antes de usarse para asegurar que sigue siendo válido. 


Por defecto no se fuerza el uso de TLS en las conexiones HTTPS

En las configuraciones por defecto de Kubernetes, el servidor API no verifica que las conexiones con el kubelet de los nodos utilice TLS. Esto puede provocar que alguien malintencionado registre en el servidor API servicios kubelet maliciosos que reciban configuraciones e información sensible del cluster. 

De momento la solución es utilizar las opciones para forzar la conexión mediante TLS de cada uno de lo servicios. El issue que soluciona esta vulnerabilidad está etiquetado como 'important-longterm', por lo que no parece que a corto plazo vaya a cambiar esta opción insegura por defecto y que tendremos que poner las opciones explícitamente para forzar la comprobación de TLS.


Vulnerabilidad de condición de carrera en los PID que puede dar acceso de root en el host a un proceso en un contenedor

Esta vulnerabilidad es algo más complicada de realizar que las que hemos visto antes. De hecho, requiere que el atacante tenga acceso root a un contenedor del nodo y acceso de usuario a un proceso del propio nodo (fuera de cualquier contenedor). 

El ataque se basa en conseguir mediante los PID del proceso de dentro de dentro del contenedor y de fuera del contenedor, que el de cgroup del proceso dentro del contenedor cambie y tenga acceso a leer y escribir fuera del contenedor (recordad que cgroup es junto con los namespaces uno de los mecanismos que se utilizan del kernel de Linux para aislar los contenedores). 

De momento no hay una solución para esta vulnerabilidad, también teniendo en cuenta que no es el escenario habitual tener procesos en los nodos corriendo fuera de contenedores y que la solución puede no ser sencilla, se ha etiquetado también como 'important-longterm'.


Vulnerabilidad del comando kubectl cp no resuelta correctamente

El comando 'kubectl cp' es una funcionalidad que permite a administradores copiar ficheros de un contenedor a otro. Sin embargo, no se validaba la estructura de los tar que se utilizaban para traspasar los ficheros, de forma que un atacante podría introducir ficheros malintencionados en un contenedor. 

Esto se arregló parcialmente en v1.13 y v1.14 de Kubernetes. Sin embargo, seguían sin comprobarse los links simbólicos. En principio, parece que esta vulnerabilidad está resuelta desde el 30 de abril. 


Conclusiones

La primera conclusión es el compromiso firme de Kubernetes con la seguridad al haber dedicado recursos a una auditoría externa de los 1.5 millones de líneas de código (largos) que tiene. 

Pero no sólo eso, sino que haber publicado el resultado de la auditoría junto con la respuestas para solucionar cada una de las vulnerabilidades detectadas hace que Kubernetes saque pecho con una de sus mayores fortalezas: la gran comunidad de desarrolladores open source que lo respalda.

Seguiremos los adelantos en la resolución de las diferentes vulnerabilidades y en futuros posts intentaré profundizar en los otros documentos de la auditoría de seguridad, ya que son muy interesantes y permiten conocer la estructura interna de Kubernetes con los puntos fuertes y débiles de sus componentes, su superficie de ataque y las posibles amenazas que existen en el sistema.

domingo, 8 de septiembre de 2019

Learner, el nuevo estado de nodo en etcd v3.4

Aunque algunas configuraciones de Kubernetes como K3s utilizan otras bases de datos para guardar el estado, la gran mayoría de los despliegues de Kubernetes utilizan clusters etcd. Por eso es importante echarle un ojo a las novedades de la nueva versión 3.4 que acaba de ser publicada. También podéis leer el resumen que hacen en el blog de Kubernetes.

Entre las novedades que trae, hay una que parece que va a tener recorrido dentro de los futuros desarrollos de etcd y que hay que empezar a conocer. Se trata de un nuevo estado de nodo llamado learner. En este nuevo estado, el nodo no aumenta el quorum del cluster etcd y no puede participar en procesos de elecciones, sin embargo, recibe todos los datos y actualizaciones del líder. Un nodo en este estado learner puede ser promocionado a nodo seguidor y operar de forma normal dentro del cluster etcd cuando esté al día de los datos.

En el propio documento de diseño proponen los siguientes pasos de este estado learner para la futura v3.5:
  • Aumentar la capacidad de reconfiguración y tolerancia a errores de configuración haciendo este estado por defecto al crear un nuevo nodo en el cluster.
  • Promocionar al learner automáticamente a miembro con derecho a voto cuando se cumplan ciertas condiciones definidas por el usuario.
  • Dejarlo "en el banquillo" y promocionar al learner automáticamente cuando el cluster sufra problemas de disponibilidad (cuando caigan otros nodos, o no sean accesibles). 
  • Crear learners de solo lectura. Esto puede descargar al líder de operaciones de red sin aumentar ni complicar el consenso del cluster. 

Podéis encontrar aquí el documento de diseño de este nuevo estado learner con los escenarios que han propiciado su aparición en la v3.4 dándose todos ellos cuando se añade un nuevo nodo al cluster etcd. Las imágenes de los diferentes escenarios que explico a continuación están extraídas de este documento.

Problemas en la creación de un nodo en el cluster etcd antes de v3.4

La motivación crear este nuevo estado de nodo ha sido triple. En primer lugar, podían aparecer problemas de sobrecarga del nodo líder en el cluster cuando se unía un nuevo nodo. Cuando el líder del cluster etcd le manda toda la información del log para que se ponga al día, puede sobrecargar su red y dar problemas de timeout en el latido que manda al resto de nodos seguidores del cluster, haciendo que comience un nuevo proceso de elecciones.

etcd v3.4 nodo learning: sobrecarga de líder


En segundo lugar, podría haber problemas cuando un nuevo miembro se unía al cluster etcd cuando se producía un particionamiento de la red. Dependiendo de si el líder queda aislado y dónde se crea el nuevo nodo, puede llevar a provocar a la pérdida del quorum.

etcd v3.4 nodo learning: particionamiento de red


Finalmente, el caso más grave ocurre cuando un se da de alta en el cluster etcd un nuevo nodo con una configuración incorrecta. El problema es que hasta ahora las configuraciones incorrectas también cambiaban el tamaño del quorum y se podía llegar a dejar inoperativo todo el cluster etcd si se añadían nodos inaccesibles en número suficiente como para que nunca se pueda alcanzar el quorum.

etcd v3.4 nodo learning: fallo en configuración de nuevo nodo


Esto, puede ocurrir de manera accidental, pero puede convertirse en un ataque intencionado si se logra acceso al backend de etcd. El ataque consiste en dar de alta suficientes nodos inaccesibles como para que el cluster etcd pierda el quorum.

Riesgos de seguridad y actualización del cluster etcd a v3.4

Hay que tener en cuenta que tener un acceso inseguro al backend de la base de datos etcd es equivalente a tener acceso root a todo el cluster de Kubernetes, ya que se puede consultar y modificar el estado de los nodos, pods, autorizaciones, accesos, etc. sin tener que pasar por el servidor API de Kubernetes.

Los efectos de dejar inoperativo la base de datos etcd es que el servidor API deja de poder consultar o modificar la base de datos y por lo tanto, queda inoperativo todo el cluster de Kubernetes. Entre las medidas que hay que implementar para corregir un acceso inseguro a la base de datos etcd están:
  • Despliegue del cluster etcd en nodos diferentes a los nodos master de Kubernetes. Esto evita que desde una intrusión en un nodo maestro inseguro pueda accederse directamente a la administración de la base de datos etcd.
  • Uso de certificados TLS y rotación automática y periódica de lo mismos entre el servidor API de Kubernetes y la base de datos etcd.
  • Uso de firewall para que sólo el servidor API de Kubernetes tenga acceso al cluster etcd.

En la página de actualización de etcd de v3.3 a v3.4 explican que ésta es una actualización que puede hacerse sin caía del servicio, siempre y cuando tengamos un despliegue de alta disponibilidad con varios nodos etcd, actualizando uno a uno cada nodo.