Securizando tus arquitecturas de microservicios con Istio

Ya hemos hablado en otras ocasiones de Istio y de todo lo que nos puede ofrecer en el desarrollo de nuestras arquitecturas distribuidas. En Paradigma estamos apostando fuerte por el producto y hoy nos centraremos en una de las funcionalidades “clave” que nos ofrece este service mesh: la seguridad.

El token JWT

JWT (JSON Web Token) es un estándar abierto cuyo objetivo es facilitar el intercambio de claims (datos que representan a usuarios finales) entre las partes de un sistema de manera segura.

Fue publicado por el grupo de trabajo JOSE (Javascript Object Signing and Encryption) en 2015. Hasta ese momento existía un vacío dentro del mundo de las APIS y los microservicios a la hora de transferir tokens de usuario.

En el pasado, muchas compañías optaron por definir sus tokens propietarios, aunque en la actualidad JWT es el que se ha impuesto.

Veamos qué ventajas ofrece el uso de JWT:

  • Permite firma y cifrado de datos.
  • El formato es fácilmente entendible y “descifrable”, al contrario que otras formas de codificación de datos, como puede ser ASN.1.
  • Permite ser propagado para servir como autenticación en las llamadas entre servicios.
  • La validación del token se puede hacer “offline”, al contrario que los tokens opacos, que requieren de una conexión con el servicio que emitió el token para validarlos. Los tokens JWT no necesitan ser validados contactando con el emisor, sino que bastará con tener la clave pública que firmó el token. Esta característica hace que las latencias en sistemas distribuidos sean menores.

JWT no es perfecto, también existen desventajas en su uso. Por poner un ejemplo, los tokens no son fáciles de revocar ya que su validación, al ser offline, no depende de un punto único como en el caso de OAuth2.

La representación de un token JWT se compone de tres partes diferenciadas separadas por un punto (.). Cada parte tiene una codificación base 64 que al decodificar nos da un JSON para las dos primeras partes y los datos criptográficos para la tercera parte.

La cabecera contiene el tipo de token y algoritmos criptográficos para su verificación y/o descifrado. El payload son los datos del usuario (claims) y la tercera parte contiene la firma o cifrado de los datos que aseguran su integridad y/o confidencialidad.

Seguridad en las arquitecturas de microservicios

Con la proliferación del API management, el patrón gateway y las arquitecturas de microservicios, cada vez se está extendiendo más una “arquitectura tipo” en la que se usa una autenticación “fuerte” (OAuth 2.0/openid connect) entre el dispositivo del usuario final y el gateway y una autenticación “menos fuerte” entre el gateway y backend/microservicios.

Este tipo de autenticación más liviana es la que se suele implementar mediante JSON Web Token.

En estas arquitecturas el API Gateway, o Edge Service, es el encargado de validar el token OAuth contactando con el Identity Provider (IdP) y, una vez validado, genera un token JWT basado en las credenciales OAuth.

Por su parte, los microservicios reciben el token JWT generado por el gateway, normalmente en la cabecera de seguridad estándar “Authorization”. Los microservicios son los encargados de validar el token y propagarlo al resto de microservicios del sistema, si fuera necesario.

Hay que tener especial atención en el proceso de propagación, ya que si el token le llega a un sistema externo podríamos estar ante un problema grave de seguridad.

NOTA: Existen Idp’s que implementan su token OAuth (access_token) mediante JWT. Esta aproximación tiene una ventaja y es que el token puede ser validado de manera online, contactando con el IdP, o de manera offline validando la firma del JWT. Este tipo de tokens abre la posibilidad de usar el token JWT generado por el IdP en todo el sistema, haciendo innecesaria la traslación de tokens en el gateway.

Seguridad con Istio

Istio tiene varias características que nos ayudan a gestionar la seguridad de nuestros sistemas distribuidos.

Por un lado permite definir la seguridad entre componentes, esto lo consigue mediante el uso de SSL con autenticación mutua en las conexiones.

Para este cometido, Istio despliega un componente llamado “istio-auth”, que no es más que una autoridad de certificación (CA) encargada de emitir certificados destinados a ser instalados en los componentes proxy de nuestro plano de datos. La documentación de Istio denomina a este tipo de seguridad “Service-to-service authorization”.

Por otro lado, tenemos la autorización de usuario final o “endUser-to-Service authorization” que se encarga de autenticar y autorizar a los usuarios finales del sistema. En esta parte de la seguridad nos adentraremos en los siguientes apartados.

Seguridad endUser con Istio

La seguridad de usuario final ha sido implementada por Istio según los estándares definidos por el grupo de trabajo JOSE, entre los que se incluye el mencionado JWT o JWK (Json Web Key). Esta funcionalidad se añadió de manera estable en la versión 0.7.x del producto, Istio sacó su primera release (1.0.0) en Julio de este año.

Cuando un proxy envoy reciba una petición, pasará a validar el JWT con la clave pública configurada. Si el token es válido, pasará el control al microservicio añadiendo una cabecera con la información del contexto de seguridad que contiene el token JWT (“sec-istio-auth-userinfo”).

En esta cabecera encontraremos el “payload” del JWT original que contiene los datos del usuario, Istio obvia el resto del JWT.

¿Por qué hace esto Istio? Una vez el proxy ha validado el token, no tiene sentido que el microservicio lo vuelva a validar, así que el proxy envoy transfiere el contexto de seguridad al microservicio por si tiene que hacer alguna acción adicional como una autorización avanzada, generar trazas de monitorización, etc.

Para obtener el comportamiento descrito arriba, Istio disponibiliza, dentro del cluster Kubernetes, una serie de definiciones de objetos (object types). En la versión 0.7, que es la que hemos usado para el post, tendremos que trabajar con los siguientes objetos:

  • EndUserAuthenticationPolicySpec: crea una politica de autenticación de usuario final, en ella se configura parámetros tales como el emisor del token JWT, la clave pública* para validar el token, las cabeceras http donde buscar el token o si se debe propagar el token JWT entre el proxy y el microservicio.

* A fecha de publicación de este post Istio solo permite la obtención del fichero de claves públicas por protocolos http o https.

Despliegue de ejemplo en Kubernetes

Para la prueba de concepto usaremos uno de los ejemplos de arquitectura que venimos usando en nuestra serie de posts dedicados a service mesh. Como una imagen vale más que mil palabras, os dejamos aquí el diseño de arquitectura:

Lo primero que tenemos que hacer es instalar en nuestro cluster kubernetes el software de Istio. No vamos a entrar en el detalle de su instalación ya que se ha abordado en posts anteriores.

Instalaremos el software en un cluster que hayamos creado anteriormente, nosotros hemos usado GKE, aunque Istio se puede instalar en otros sabores de Kubernetes.

Una vez instalado Istio podremos validar que el plano de control ha sido desplegado:

Ahora instalaremos nuestra arquitectura de microservicios. Para aplicar el plano de datos a nuestros ficheros descriptores de Kubernetes debemos ejecutar, en local, el comando istioctl que viene con la instalación de Istio sobre nuestros descriptores del sistema.

Este comando se encargará de modificar los ficheros descriptores insertando todo lo necesario para que los componente proxy intercepten las comunicaciones.

kubectl apply -f <($ISTIO_DIR/bin/istioctl kube-inject -f k8s/msa.yml)

El fichero msa.yml contiene los microservicios y publica una IP + puerto para acceder al servicio books, por lo que ya podríamos probar nuestra arquitectura.

Como se aprecia en la imagen, pese a no haber añadido una cabecera de seguridad, la petición se ha procesado sin errores. Este comportamiento es correcto ya que no hemos configurado todavía las políticas de seguridad.

Para habilitar la seguridad de nuestros servicios primero vamos hacer deploy de un servidor web que contenga la clave pública con la que se firmarán los tokens.

Usaremos criptografía asimétrica para firmar y verificar los tokens, para ello contamos con una clave en formato DER generada mediante openssl. El formato del fichero de claves debe seguir el estándar JWK por lo que debemos transformarla.

Pem-jwk es una librería nodejs sencilla de usar y que nos ayudará con la transformación. En el repositorio Git podéis ver el script de transformación. El fichero de claves obtenido es como el siguiente:

{
	"keys": [{
		"use": "sig",
		"alg": "RS256",
		"kid": "istio-kid",
		"kty": "RSA",
		"n": 
"4f5wg5l2hKsTeNem_V41fGnJm6gOdrj8ym3rFkEU_wT8RDtnSgFEZOQpHEgQ7JL38xUfU0
Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0icqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20
ZZuNlX2BrClciHhCPUIIZOQn_MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR-1DcKJzQBSTAGnpYVaqpsARap-nwRipr3
nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKARdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy
7w",
		"e": "AQAB"
	}]
}

Una vez tenemos nuestro fichero de claves, necesitamos un servidor web para albergarlo, desplegaremos un contenedor nginx para este cometido.

kubectl apply -f k8s/keyrepo.yml

Por último, tendremos que crear la política de autenticación en Istio y asociarla a los servicios que desplegamos con anterioridad.

apiVersion: config.istio.io/v1alpha2
kind: EndUserAuthenticationPolicySpec
metadata:
  labels:
    app: books
  name: books-jwt-auth
  namespace: default
spec:
  jwts:
  - issuer: test
    jwks_uri: "http://key-repo:8080/"
    forwardJwt: true
---
apiVersion: config.istio.io/v1alpha2
kind: EndUserAuthenticationPolicySpecBinding
metadata:
  name: books-jwt-auth-binding
  namespace: default
spec:
  policies:
  - name: books-jwt-auth
    namespace: default
  services:
  - name: books
    namespace: default
  - name: stars
    namespace: default
---

La configuración del objeto EndUserAuthenticationPolicySpec contiene:

  • El emisor del JWT, en nuestro caso hemos establecido “test”.
  • Url http desde la que envoy podrá descargar el fichero de claves “http://key-repo:8080/“.
  • Un flag para que envoy transfiera el JWT al microservicio.

Aquí hay que entender que Envoy reenvía el token a su “servicio sidecar”, pero no tiene la capacidad de reenviarlo entre los distintos servicios. Por eso, si nuestro objetivo es que el JWT llegue a todos los microservicios debemos propagar el token en nuestro código.

En la definición del objeto EndUserAuthenticationPolicySpecBinding establecemos los servicios a los que se les aplicará esta política de autenticación, en nuestro caso “books” y “stars”.

Esto quiere decir que los dos microservicios necesitarán un JWT válido para que Envoy le pase la petición a la lógica de negocio. Desplegamos los objetos de seguridad:

kubectl apply -f k8s/enduser-security.yml

Para listar los objetos lo haríamos con el cliente de Kubernetes, como con cualquier otro objeto:

Ahora pasamos a comprobar que se han aplicado las políticas a los servicios. En primer lugar realizaremos una llamada sin la cabecera authorization y más tarde una llamada con un token JWT válido.

Como se puede ver en la imagen, una vez aplicadas las políticas, el proxy envoy rechaza las peticiones que no tienen la cabecera “Authorization”, mientras que las peticiones que vienen autenticadas con un token JWT válido son admitidas y transferidas a la lógica de negocio.

Podemos ver el contenido del JWT usado en la petición en la siguiente imagen:

El código de los microservicios y todos los ficheros referenciados en este post los podéis encontrar en nuestro repositorio de Github.

Conclusión

Con este post nos hemos introducido en la seguridad según Istio, una de las muchas funcionalidades que nos ofrece este service mesh. Esta feature no se detalla mucho en la documentación del producto por lo que esperamos que os sea de ayuda.

Istio se está convirtiendo en un gran producto, las últimas versiones le han dado estabilidad y las personas que están detrás de él se han empeñado en hacer más fácil la vida de los desarrolladores.

La seguridad en las arquitecturas de microservicios es otra de las funcionalidades que hasta ahora se “diluía” con la lógica de negocio en el código base de nuestros servicios, pero puede que esto deje de suceder si empiezas a utilizar este service mesh que le va como anillo al dedo a nuestro querido Kubernetes.

Escribe un comentario