viernes, 27 de diciembre de 2019

Novedades de seguridad en la nube (Diciembre 2019)

Estos dos meses no he podido escribir en este blog por falta de tiempo, pero han pasado muchas cosas interesantes.

Quizás en seguridad una de las más importantes ha sido la detección de dos librerías con código malicioso en el repositorio oficial de paquetes de python (PyPI). Las dos hacían uso de nombres similares a las librerías originales y una de ellas llevaba más de un año en el repositorio con cientos de descargas. En el blog de Sysdig nos explican los detalles de estas librerías, qué hacían y cómo detectarlas con la herramienta Sysdig Secure.

También en Diciembre hemos tenido una nueva versión de Kubernetes, la 1.17. En el blog oficial nos explican las principales novedades, y si queremos profundizar en algunas de las nuevas características más en detalle, en el blog de Sysdig nos hacen un resumen de las principales características en su tradicional editor's pick.

Y hablando de nuevas versiones, se ha publicado la versión 2.15 de Prometheus. Podéis ver los detalles de los cambios en las release notes del proyecto y os dejo también un pequeño artículo de uno de sus mantenedores en el que explica algunos de los cambios.

Por último, hay que hablar de Falco (aquí podéis ver un vídeo de la última KubeCon en la que explican el proyecto). Este proyecto de Sysdig ha sido donado a la CNCF y está actualmente en fase Sandbox, aunque hay propuestas para pasarlo a Incubating. En Falco han pasado dos cosas interesantes este meses.

La primera es la publicación del Cloud Native Security Hub. Este es un sitio que pretende aglutinar en un mismo sitio las reglas que puede necesitar cualquiera que tenga servicios en la nube y que quiera monitorizarlos y crear alertas de seguridad sobre ellos.

La segunda noticia de Falco es la publicación de la auditoría de seguridad que ha realizado (esta es una de las ventajas de ser parte de la CNCF). Podéis ver los detalles aquí. Esta es similar a la que se hizo de Kubernetes y que comentamos en los primeros artículos de este blog.

Para terminar, comentar que Falco detecta ya el gusano Graboid, que publiqué en el último post. Tras escribir el artículo, hice la pull request en el repositorio y se convirtió en mi primera aportación al proyecto.

viernes, 1 de noviembre de 2019

Detectan el primer gusano que utiliza imágenes y el servicio de Docker para propagarse

La Unidad 42 (un grupo de investigadores de seguridad de Palo Alto Networks), han detectado hace unas semanas el primer gusano que se propaga a través de Docker (aquí el artículo orginal en inglés). Esto parece una contradicción, ya que siempre pensamos que los contenedores son jaulas seguras donde los procesos no pueden salir de los namespaces que gestiona el kernel, etc. Pero esto tiene mucha más gracia y puede ser el comienzo de nuevos tipos de ataques basados en contenedores.

El ataque utiliza el API del demonio de Docker. Normalmente, esta API no es accesible públicamente ya que se accede a través de socket /var/run/docker.sock, pero existe la posibilidad de que escuche en un puerto tcp, con la siguiente opción por ejemplo: -H tcp://0.0.0.0:1234 (esto configuraría el socket del demonio de docker para escuchar cualquier conexión al puerto 1234).

El atacante lo que ha hecho es encontrar una serie de máquinas con este puerto abierto públicamente y sin autenticación. De esta forma, de manera remota puede lanzar, detener y acceder a contenedores en la máquina anfitriona. Y no sólo eso... Montando volúmenes puede acceder al contenido completo del sistema de ficheros. Pero vayamos al ataque en cuestión.

Teniendo la posibilidad de lanzar contenedores de forma remota en las máquinas comprometidas, el atacante subió una imagen al registro de imágenes de Docker Hub, concretamente la pocosow/centos:7.6.1810. Esta imagen contenía tanto un cliente docker para comunicarse con otras máquinas comprometidas, como una serie de scripts que se encargaban de mandar información a las máquinas desde las que se gestionaba el ataque y otros que se dedicaban a encender y apagar los servicios de minado de criptomonedas en otros hosts infectados.


esquema de ataque de docker inseguro
Imagen original de Unit 42

El ataque la verdad es que no ha sido muy efectivo ni malicioso. En primer lugar, porque al explotar una vulnerabilidad poco usual (que es la de tener accesible de forma pública el API del demonio de docker) no ha afectado a muchas máquinas. En el fichero de máquinas infectadas había 2.000 IPs.

En segundo lugar, el sistema de distribución de la imagen infectada ha sido el propio Docker Hub. En el momento en el que el equipo de la Unidad 42 ha detectado el ataque y la imagen comprometida se han puesto en contacto con Docker y han colaborado para eliminar las imágenes que servían para realizar el ataque.

Sin embargo, hay que tener en cuenta algunas cuestiones que sí que pueden ser interesantes. Este es el primer gusano conocido que explota el acceso público al API de Docker. En este caso se utilizaba para minar criptomonedas (y de forma no muy eficiente, la verdad). ¿Qué hubiera pasado si esta vulnerabilidad se utiliza para hacer ransomware? ¿O robo de información o secuestro del servidor? ¿Y si hubieran utilizado otro repositorio privado para distribuir las imágenes infectadas de forma que no se pudieran eliminar tan fácilmente?

En el artículo en el que explican este gusano, la Unidad 42 da algunos consejos para evitar este nuevo tipo de ataques:

  • Nunca exponer la API del demonio Docker sin un mecanismo de autenticación.
  • Usar directamente el socket de Unix (opción por defecto) o conectarse por ssh.
  • Utilizar un firewall con listas blancas de IPs de acceso.
  • Nunca utilizar imágenes de registros o namespaces desconocidos.
  • Controlar frecuentemente los contenedores desconocidos (esto puede hacerse de forma automática con herramientas como Falco).
  • Utilizar soluciones automatizadas como las de Prisma o Twistlock para la detección de contenedores o actividades sospechosas. 

domingo, 20 de octubre de 2019

Definiendo el usuario de un contenedor en Docker

Como ya sabéis, los contenedores ofrecen un entorno en el que, gracias a los namespaces, el sistema de archivos por capas y cgroups el contenedor sea como Las Vegas dentro de nuestro sistema: "Lo que pasa en el contenedor se queda en el contenedor". Un proceso puede volverse loco (con una bomba lógica como la que probamos en otro artículo), pero si la asignación de recursos es correcta, no tiene por qué afectar al resto del sistema. Un atacante puede comprometer un contenedor inseguro, pero gracias al sistema de ficheros por capas, no podrá acceder a las carpetas reales del sistema.

Pero... ¿qué pasa con los usuarios dentro del contenedor? Cuando no se define un usuario de forma específica en el Dockerfile el usuario que utiliza el contenedor es root. Pero a diferencia del sistema de ficheros, no se trata de un 'root especial para el contenedor' que solo tiene permisos de superadministrador dentro del contenedor y no fuera de él. Se trata del mismo root que el sistema anfitrión.

Esto en principio, como el contenedor es un sistema seguro del que un usuario que obtuviera acceso al mismo no puede salir, no pasa nada. Pero esto sabemos que no es así. Hay multitud de situaciones, vulnerabilidades y formas de escapar de un contenedor y conseguir acceso al sistema anfitrión. Y eso significa que si consiguen acceso a un contenedor inseguro con permisos de root y salen fuera de él, lo harán con permisos de root.

Una vez metido el miedo en el cuerpo, vamos a probarlo. Para ello he creado un repositorio en Github con algunos Dockerfiles que iré usando para probar qué ocurre con diferentes configuraciones. Para clonarlo en vuestra máquina local:

git clone https://github.com/daviddetorres/definiendo-usuarios-docker

En el repositorio hay 3 Dockerfiles, de los cuales, nos vamos a centrar primero en los que se llaman Dockerfile_usuariopordefectoDockerfile_conusuario. El primero no define ningún usuario, por lo que el contenedor al iniciarse lo hará con permisos de root. En el segundo he añadido la creación de un usuario dentro del propio Dockerfile. Las líneas que hacen añaden al usuario son las siguientes:

RUN addgroup -S usuariocontenedor && \
adduser -S -s /bin/false -G  usuariocontenedor usuariocontenedor  && \
chown -R usuariocontenedor:usuariocontenedor /app
USER usuariocontenedor

En la primera orden con RUN lo que hacemos es:
  • Crear un grupo de sistema "usuariocontenedor"
  • Crear un usuario de sistema "usuariocontenedor" sin shell y en ese grupo
  • Cambiar el propietario de la carpeta donde hemos copiado la aplicación a ese usuario
Finalmente, con la línea "USER usuariocontenedor" lo que hacemos es indicarle a Docker que cuando se ejecute el contenedor, lo haga con ese usuario. 

Ahora vamos a probarlo. En el repositorio están los comandos para crear dos carpetas en /tmp con un fichero cada una. Una de ellas es propiedad de root son permisos sólo para él. La otra pertenece a un usuario normal. Vamos a ver si desde los contenedores podemos leer estas carpetas montando la carpeta padre de ellas como volumen. 

Cuando iniciamos los dos contenedores y desde el que no tiene definido usuario (y está utilizando el usuario root) ejecutamos la lectura del fichero del superadministrador y ¡SOPRESA! podemos leerlo. Sin embargo, como era de esperar, desde el contenedor en el que hemos definido el usuario no podemos leer ni uno ni otro (ya que son propiedad de otro usuario). 

acceso a carpeta root desde contenedor

Pero no nos vamos a quedar ahí. ¿Qué usuario está usando el contenedor que está creando un usuario nuevo? Vamos a verlo viendo el usuario que ha creado el proceso con un ps

uid del usuario del contenedor con creación de usuario

¿Quién es ese usuario systemd+ que está ejecutando el contenedor? ¿No se supone que he creado un usuario que se llamaba usuariocontenedor? Pues resulta que el uid del usuario que ha creado el contenedor es el 100, porque en el contenedor es el primero que está libre. Sin embargo, en mi máquina donde estoy ejecutando el contenedor, el usuario con uid 100 ya existe... Se trata de un usuario de sistema llamado systemd-timesync (más información sobre este usuario, que sirve para el servicio de sincronización horaria, aquí).

Entonces... Tengo un usuario que he creado en el contenedor con la intención de que no tenga permisos, pero ¿y si coincide con un usuario en la máquina anfitrión que sí que tiene permisos por estar en grupos, sudo, etc.? La alternativa para hacerlo bien es indicar a la hora de crear el usuario un uid específico, y normalmente lo suficientemente alto como para poder asegurar que ese uid no pertenece a usuarios creados en la máquina anfitrión.

Para poder hacer eso, hay que instalar en nuestra imagen basada en Alpine un paquete especial llamado shadow que permite la creación de usuarios con uids altos. El Dockerfile donde se hace esto es el llamado Dockerfile_conusuariouid y la línea que crea el usuario se convierte en los siguiente:

RUN apk add shadow && \
/usr/sbin/groupadd -g 10001 --system usuariocontenedor && \
/usr/sbin/useradd --system -u 10001 -s /bin/false -g  usuariocontenedor usuariocontenedor  && \
chown -R usuariocontenedor:usuariocontenedor /app

Ahora, al hacer un ps del proceso desde la máquina anfitrión, vemos lo siguiente:

uid del usuario del contenedor con creación de usuario con paquete shadow

Ahora el usuario al que pertenece el proceso no lo identifica la máquina anfitrión como un usuario determinado (si fuera así nos indicaría el nombre), sino que aparece únicamente con un uid (10001) que es el que hemos definido en la creación de la imagen.

Como conclusiones, podemos decir que dejar las imágenes con el usuario por defecto si no es estrictamente necesario es una mala idea, ya que estamos dando un acceso de root a todo aquel que se haga con el poder del contenedor gracias a una vulnerabilidad (del código, de las librerías que usamos, de la imagen base, etc.). Esto es especialmente crítico cuando montamos volúmenes.

Otro de los efectos que hemos visto en este ejemplo es que un contenedor puede crear un usuario con un uid que la máquina anfitrión identifique como otro usuario ya creado anteriormente. Si el usuario con el que coincide el del contenedor tiene otros permisos con los que no hemos contado, tenemos también un posible agujero y escalada de privilegios inesperada. Por ellos es recomendable crear siempre los usuarios de los contenedores uids que podamos asegurar que no corresponden con usuarios de sistema o especiales en la máquina anfitrión

miércoles, 9 de octubre de 2019

Probando una bomba YALM en Kubernetes

El próximo martes 15 de octubre se publican nuevas versiones menores de las diferentes versiones de Kubernetes. Son las siguientes:
  • 1.16.2
  • 1.15.5
  • 1.14.8
  • 1.13.12
En estas versiones se resuelve una vulnerabilidad (la CVE-2019-11253) que permite una bomba YALM. ¿Qué es eso de una bomba YALM? Como toda bomba lógica, se trata de un código que mediante recursividad consigue que el sistema agote los recursos que tiene y se bloquee o quede en un estado no recuperable. 

Así que para ver de qué se trataba esta bomba, la hemos probado. En la página del issue Github que lo documenta se encuentra un ejemplo de fichero que si lo guardamos en un fichero 'manifest.yalm' puede provocar la bomba ejecutando la siguiente orden: 

kubectl create -f manifest.yalm

Para probarlo he utilizado kind (Kubernetes in Docker), ya que de esta forma se puede ver el contenedor que crea el nodo del cluster mediante la herramienta csysdig. A la vez, también he puesto un htop en otra consola para ver la utilización de recursos de sistema.

Recursos antes de bomba YALM


Como se puede ver, kind ha creado un contenedor llamado 'kind-control-plane', y en la parte de abajo se ve el uso de memoria y swap de 1.85G y 1.49G respectivamente.

Después de ejecutar la bomba YALM lógica, la memoria ha comenzado a crecer:

Recursos durante la bomba yalm

Como se puede ver el efecto de esta bomba lógica se da en el cliente, que como se ve en la imagen ha comenzado a llenar la memoria RAM y ha comenzado con el swap. 

¿Qué hace la bomba YALM del ejemplo? Vamos a verla:

apiVersion: v1
data:
  a: &a ["web","web","web","web","web","web","web","web","web"]
  b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
  c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
  d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
  e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
  f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
  g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
  h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
  i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
kind: ConfigMap
metadata:
  name: yaml-bomb
  namespace: default

Lo único que hace es crear un ConfigMap de forma recursiva. Este al ser parseado por kubectl hace que este comience a crecer de forma incontrolada. Esto realmente tampoco sería muy peligroso... si no pudieramos atacar directamente al servidor API, pero sí que podemos (depende de los permisos que tenga un usuario, pero supongamos que los suficientes como para crear ConfigMaps).

Tras un par de pruebas en las que he conseguido bloquear totalmente mi máquina y varios reinicios (con lo que he podido comprobar que la bomba funciona perfectamente atacando directamente a la API), he decidido pasar a hacer la prueba de la bomba YALM con Minikube, para tenerlo en una máquina virtual con recursos restringidos.

Así que por un lado entro en la máquina virtual de minikube para hacer un top y ver los recursos que consume sin nada encendido:

minikube ssh

recursos minikube antes de ataque de bomba YALM

En otra terminal abro un proxy al API:

kubectl proxy

Y en una tercera ejecuto una petición directa al servidor API:

curl -X POST http://127.0.0.1:8001/api/v1/namespaces/default/configmaps -H "Content-Type: application/yaml" --data-binary @manifest.yalm   

Pocos segundos después los recursos de la máquina virtual se disparan, y es más, deja de responder a las peticiones más sencillas, por ejemplo de listar los nodos del cluster, dando timeout.

recursos minikube durante ataque de bomba YALM

Este es un ejemplo de como un usuario malintencionado puede comprometer un cluster con una bomba YALM, ya que el servidor API es el encargado de realizar todas las creaciones y destrucciones de los pods en los nodos esclavos. Si el servidos API no responde y no tenemos replicas del mismo en un escenario de alta disponibilidad (con varios nodos maestros), dejaremos de tener acceso al cluster.

De nuevo otro buen motivo para mantener nuestros clusters actualizados con las versiones más actuales.  

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.