Seguridad y microservicios Java

Publicado por José Moreno el

JavaMicroserviciosContenedoresSeguridadDocker

Hace unos días llegaron a mis manos dos interesantes artículos relacionados con Java y seguridad. El primero de ellos describe un proceso de ejecución remota de código en Spring Boot. — ¡Joder, joder, joder! — fue mi primera reacción — Si se puede hacer un cat /etc/passwd se podrían hacer cosas muy malas! — . Y comencé a darle vueltas a las implicaciones de todo esto, principalmente en las implementaciones de microservicios construidos en Java, los contenedores, … y evidentemente la seguridad debería ser un innegociable en la construcción de cualquier sistema.

El segundo de ellos habla del proceso de creación de los Java policy files basado en el principio de mínimo privilegio, y tirando del hilo de este artículo sobre la manera que tenemos en Java para protegernos de la ejecución arbitraria de código. Pero vamos por pasos.

TL;TR

Creo que esto puede haber quedado un poco largo, de modo que te dejo aquí un resumen, por si no quieres seguir leyendo:

Estamos poniendo en producción, y accesibles al mundo, JVMs sin seguridad. ¡Securízalas que no cuesta tanto!

Y ahora veamos qué se puede hacer.

First things first

Java es un lenguaje muy completo, lleno de poderosas herramientas que nos permiten afrontar casi cualquier problema con garantías de éxito, y éstas son usadas de manera intensiva por gran cantidad de librerías y productos, la mayoría de los cuales usamos en nuestro día a día. Y, como toda herramienta, en sí mismas no son responsables de lo que se hace con ellas. Se pueden usar para mucho bien, pero también para mucho mal.

Minimiza los riesgos de tu sistema

¿Y qué podemos hacer nosotros? ¿Cual debería ser nuestra responsabilidad? Principalmente minimizar los riesgos de nuestro sistema.

Primeras medidas a tomar

Ejecución de código remoto… uhhhmmm… tras la primera sensación de danger-danger empiezo a pensar en como minimizar el impacto de esto. — Bueno — me digo — si ejecutamos el proceso Java con un usuario con un conjunto limitado de permisos para lo que realmente necesita, seguro que se reduce considerablemente el impacto

Ejecuta la JVM con un usuario específico con unos permisos limitados

La seguridad de la máquina virtual Java

Hasta el día que empezamos a implementar microservicios solíamos construir las aplicaciones Java para ser ejecutadas sobre servidores de aplicaciones, por tanto responsabilidades sobre la seguridad, autenticación, autorización, etc., eran gestionadas por la infraestructura Java EE, lo que hacía que no nos tuviéramos que preocupar de muchas cosas. Y en el peor de los casos ni siquiera éramos muy conscientes.

Cosas que pasan a nuestras espaldas

Como decía más arriba, Java tiene alguna que otra herramienta muy poderosa , que es utilizada intensivamente por muchos frameworks y librerías de uso habitual. El riesgo llega cuando estas herramientas pueden utilizarse para otros usos no tan “interesantes” para nosotros.

Pero trabajemos mejor con un ejemplo. Imaginemos que tenemos que modelar una clase Employee, la cual, por requisitos de negocio necesitamos que sea inmutable y en la que todos los atributos son privados menos uno, el nombre, el que exponemos por medio de un accessor. Nos quedaría un código como el siguiente:

class final InmutableEmployee {
    private final int    id;
    private final String name; 
    private final Float  salary;

    private InmutableEmployee(final int id, 
                              final String name, 
                              final Float salary) {
        this.id     = id;
        this.name   = name;
        this.salary = salary;
    }

    // this is the only accessor of the class  
    public String name() { 
        return name; 
    }

    // rest of the code
}

¿Que estamos haciendo? Confiar en las características del lenguaje de programación y en entorno de ejecución. Lo normal. Si dice que es privado, es privado, y si dice que es final es final ¿o no?.

Bueno, vamos a escribir dos pequeños métodos, a ver qué pasa …

public static Object giveMe(final Object object, 
                            final String attribute) {
    try {
        Field theInmutableFielfd =
            object.getClass().getDeclaredField(attribute);
        theInmutableFielfd.setAccessible(true);
        return theInmutableFielfd.get(object);
    } catch (NoSuchFieldException | 
             IllegalArgumentException | 
             IllegalAccessException e) {
        return null;
    }
}

public static void hackIt(final Object object, 
                          final String attribute, 
                          final Object newValue) {
    try {
        Field theInmutableFielfd = 
            object.getClass().getDeclaredField(attribute);
        theInmutableFielfd.setAccessible(true);
        theInmutableFielfd.set(object, newValue);
    } catch (NoSuchFieldException | 
             IllegalArgumentException | 
             IllegalAccessException e) {}
}

Pues lo que pasa es que de repente podemos hacer cosas como …

InmutableEmployee employee = new InmutableEmployee(1, "John Doe", 100f);

// salary field should be hidden, but...
System.out.printf("The salary of %1$s is %2$s\n", 
                  employee.name(), 
                  giveMe(employee, "salary"));
// output: The salary of John Doe is 100.0 bitcoins
//   OMG!! hidden fields are accesibles

// salary should be final, so inmutable but...
hackIt(employee, "salary", 50f); // hack it!
System.out.printf("The salary of John Doe now is 50.0 bitcoins!\n",
                  employee.name(), 
                  giveMe(employee, "salary"));
// output: The salary of John Doe now is 50.0 bitcoins!
//   OMG!! an inmutable hidden field have been changed!!

Puedes acceder al código completo del ejemplo en mi GitHub.

Es decir, podemos acceder a atributos privados e incluso podemos modificar el valor de atributos finales. Y esto … no está bien. Es un serio problema de seguridad. Sí, ya sé, nosotros tenemos controlado nuestro código. Tenemos análisis de código y sabemos que no hacemos este tipo de cosas en nuestro codebase. Ok. Pero, ¿puedes decir lo mismos de todas las dependencias de tu proyecto? ¿Y de las que usas transitívamente en runtime?

Sandboxear la JVM

Todo el comportamiento anterior es posible porque utilizamos la máquina virtual sin un security manager. Pero la JVM puede ejecutarse en modo sandbox. Pero en ejecución standalone por defecto no se tiene security manager, por tanto el código puede hacer cualquier cosa. Esto quiere decir que cuando exponemos nuestros microservicios Java arrancando sin un security manager estamos dejando que el código haga cualquier cosa. Y ahora une esto con lo que comentábamos al principio del artículo sobre la ejecución arbitraria de código… ummmmhhh … no mola nada.

Por tanto, si queremos evitar esta situación deberíamos ejecutar siempre las máquinas virtuales con el security manager activado.

java -Djava.security.manager <...>

Con esto arrancamos la máquina virtual con las políticas de seguridad por defecto, definidas en el fichero $JAVA_HOME/lib/security/java.policy (o $JAVA_HOME/lib/securiy/default.policy en Java 9).

Arranca la JVM con el security manager activado y con tus políticas de seguridad

Y ahora al ejecutar tenemos una maravillosa excepción que nos indica que hemos intentado hacer algo para lo que no tenemos permiso.

$ java -Djava.security.manager HackInmutable

Exception in thread "main" java.security.AccessControlException: access denied ("java.lang.reflect.ReflectPermission" "suppressAccessChecks")

Con este simple paso hemos conseguido ejecutar la máquina virtual en modo seguro y con ello evitar el comportamiento peligroso que no queríamos. Pero claro, con esto no permitimos que ninguna clase pueda trabajar con reflexión omitiendo los controles de acceso. Y, como dijimos al principio, hay muchos frameworks y librerías que utilizan estas técnicas, y es más que probable que queramos permitirlo para estos casos, para lo cual lo que haremos será, arrancar la máquina virtual con un security manager y pasarle un fichero con las security policies, en el cual se describirán permisos.

java -Djava.security.manager -Djava.security.policy=<policy file>

En un caso como el que estamos hablando, para permitir que un conjunto de librerías, como por ejemplo, Spring o Hibernate, puedan acceder a los atributos privados de una clase deberíamos habilitar los permisos,  permission java.lang.reflect.ReflectionPermission "suppessAccessChecks", pero sólo para estas librerías.

Displaimer: Este artículo no es un tutorial sobre la sintaxis de los ficheros de políticas de seguridad. Recomiendo la documentación oficial o leer algún artículo que profundice en estos temas.

Llegados a este punto ya estaríamos ejecutando nuestra aplicación Java sobre una máquina virtual sobre la que tenemos control de las políticas de seguridad que aplicamos en ella, y tenemos cierta certeza (toda la que se puede tener) de que estamos protegidos ante la ejecución arbitraria de código.

Contenedores, contenedores everywhere

Ahora resulta que vas y me dices que sí, que muy bien todo, pero que estás distribuyendo y ejecutando tu aplicación con contenedores, por lo que todo esto que hemos comentado hasta ahora no te afecta. Sí, bueno, es cierto, estás ejecutando tu código en un contenedor, y un contenedor ya de por sí se ejecuta en un sandbox en el kernel del anfitrión. Entonces, ¿qué podría salir mal?

Pues que dentro del contenedor en el que se encuentre tu aplicación se ejecute código inseguro. Sí, esto sólo va a afectar al contenedor no al kernel del anfitrión. Bueno … a ver … en el kernel del anfitrión los contenedores se suelen ejecutar como root, por lo que, si se descubriera una vulnerabilidad en en el motor de contenedores, el código inyectado en el contenedor podría llegar a ejecutarse como root en el kernel del anfitrión. Uhhhmmmm … esto ya no mola.

Por otro lado el código de la aplicación podría acceder a información y configuración del contenedor, tanto en lectura como en escritura, que puede ser información de configuración de nuestros sistemas, por tanto información sensible, a la que no se debería acceder de manera descontrolada, y mucho menos permitir que se difundiera.

Ejecuta tus aplicaciones Java dentro de un contenedor con un usuario específico y con el security manager activado

Por tanto, ¿qué es lo que deberíamos hacer? Pues simplemente configurar en el contenedor los dos aspectos abordados anteriormente:

  • Ejecutar el proceso Java con un usuario específico.
  • Arrancar la JVM con el security manager activado y una política de seguridad específica de la aplicación.

Es decir, en el Dockerfile con el que construimos el contenedor configuraremos el usuario y los parámetros de arranque de la JVM en el entrypoint, como por ejemplo:

FROM ...

# resto de configuración del contenedor

USER java

ENTRYPOINT ["java", \
            "-Djava.security.manager", \
            "-Djava.security.policy=my_security.policy", \
       # resto de configuración del entrypoint, como classpath 
       # propiedades o Main-Class
           ]

Con estos dos sencillos pasos hemos conseguido que el proceso Java que se ejecuta en el contenedor disponga de las mismas medidas de seguridad que teníamos cuando lo ejecutábamos fuera de un contenedor. Fácil, ¿verdad?

Unas consideraciones finales

Añadido a los valores intrínsecos de seguridad que estamos añadiendo a la aplicación, este proceso de activación del security manager nos va a servir para descubrir qué cosas están haciendo nuestras dependencias, de las que no éramos conscientes. O detectar que existen determinadas librerías que no son muy sensibles con la seguridad.

Evidentemente todo esto no es gratis, como muy bien me indicó Miguel Ángel Pastor (infinitas gracias Miguel). Tiene unos costes, tanto en el proceso de desarrollo (configurar correctamente todo, reconfiguración de políticas de seguridad con nuevas funcionalidades de la aplicación, …) como de rendimiento en tiempo de ejecución, ya que todos estos controles se comen sus ciclos de proceso. Por tanto, llegados a este punto, habría que balancear muy bien entre rendimiento y seguridad, estimar los riesgos de seguridad vs. performance, hacer un análisis de costes/beneficios, etc. para tomar la decisión que más se ajuste a nuestras necesidades. Pero bajo mi punto de vista…

La seguridad deberá ser innegociable.

Este artículo fue publicado por primera vez en Medium el 10 de Febrero de 2017.

Si te ha gustado, ¡síguenos en Twitter para estar al día de nuevos posts!