Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Autenticación entre microservicios: ¿en serio es tan complicado?

By Joshua FoxJun 28, 202210 min read

Esta página también está disponible en English, Deutsch, Français, Italiano, 日本語 y Português.

Repasamos varios métodos para autenticar microservicios entre sí, desde el más simple pero menos seguro y mantenible hasta llegar a las arquitecturas recomendadas.

authentication-microservices

Una arquitectura de microservicios se basa en servicios que se invocan entre sí. Pero ¿cómo se mantiene segura, de modo que solo tus propios servicios puedan llamar a cada uno de ellos? ¿Cómo se autentican?

Ya sabes cómo se hace en una app de navegador: el usuario ingresa sus credenciales, el navegador las envía al servicio y este responde con un token de sesión que autentica al usuario durante un tiempo determinado. Pero en un microservicio no hay ninguna persona que ingrese una contraseña ni una clave de autenticación multifactor.

Aquí voy a explicar cómo hacerlo, centrándome en servidores que corren sobre servicios de Google Cloud Platform como Cloud Run o Google Kubernetes Engine (GKE). Los servicios cliente pueden estar en GCP, on-premises o en AWS.

Lo que me motivó a escribir este artículo es que API Gateway y Cloud Endpoints son tecnologías que evolucionan rápido, con sólidas capacidades de autenticación, pero también con limitaciones cuando se las compara entre sí y con otros servicios de la competencia. Hay muchas formas de autenticar microservicios entre sí. Voy a empezar por la más simple pero menos segura y mantenible, e ir avanzando hasta las arquitecturas recomendadas.

Para que quede más claro, me enfocaré en microservicios donde tú controlas ambos extremos, pero los mismos principios aplican cuando el servicio cliente es externo a tu organización.

Lo básico: encabezados, claves y proxies

Aunque hay maneras más simples y otras más complejas de hacerlo, la autenticación entre servicios necesita un diseño muy cuidadoso. El esquema básico es este:

  • El cliente usa una clave secreta para firmar un token.
  • El formato estándar para transmitirlo es JSON Web Token.
  • El token va en un encabezado HTTP Authorization de la siguiente forma:
Authorization: Bearer <JWT>

donde es el token codificado en base64.

  • El servidor valida ese token consultando un servicio. También puede ocurrir que un proxy inverso reciba la solicitud y, antes de reenviarla al servidor real, valide el token consultando ese servicio.
  • En GCP, ese servicio lo provee la propia plataforma.

En este artículo verás varias formas de hacerlo, empezando por las simples e inseguras y avanzando hacia soluciones más completas.

Demasiado simple: "API Keys" autogestionadas

Una solución básica, muy usada por quienes no conocen el abanico completo de tecnologías, es parecida a lo que hacemos los humanos al iniciar sesión con usuario y contraseña: guardar una cadena secreta en el servicio cliente, una "API Key", que sirve como credencial, y luego validarla del lado del servidor.

APIKEY=Microservice1:78eb9a45897f

Limitaciones

Esto no es seguro.

Filtración de claves

Las claves se filtran de más maneras de las que te imaginas.

Para evitarlo, no deberías guardar secretos en Git ni en otro repositorio de código fuente, ya que es muy fácil exponerlos sin querer. En su lugar, usa un servicio de gestión de secretos como Google Cloud Secret Manager o Hashicorp Vault. Aun así, queda el mismo problema: el servicio cliente necesitará credenciales para acceder al gestor de secretos.

Gestión de claves

Tendrás que desarrollar una base de datos del lado del servidor donde guardar estas claves y una capa para validar que la clave recibida sea correcta. Tampoco vas a querer que las claves se filtren desde esa capa, así que el cliente no debería enviar la API key, sino solo un hash, que el servidor compara contra un hash almacenado de la clave. Mantener todo esto sale caro. Y además es inseguro, porque no tienes la experiencia ni los recursos para tapar todos los huecos posibles. Los sistemas de seguridad conviene dejarlos en manos de expertos siempre que se pueda.

Rotación

Como las filtraciones son inevitables, la mejor práctica es rotar la clave con frecuencia: crear una nueva e invalidar la anterior tras cierto período. Esto exige automatizar un mecanismo del lado del cliente que solicite una clave nueva (¡autenticando esa solicitud con la clave antigua!). Y del lado del servidor, necesitas un mecanismo para generar claves nuevas a demanda y para invalidar la antigua en una fecha posterior específica.

Y este tipo de cosas siempre termina siendo más complicado de lo que parece al principio: por ejemplo, seguramente quieras imponer un número máximo de versiones válidas de una clave en cualquier momento, ya que tener dos versiones válidas es parte necesaria de la rotación, pero cien versiones es una filtración esperando a ocurrir.

Claves de cuenta de servicio del proveedor de nube

¿Para qué implementar tú mismo el mecanismo de hashing, validación, rotación y expiración? Una mejor opción es crear una cuenta de servicio y descargar un archivo de clave desde Google Cloud Identity and Access Management (IAM). Puedes crear una clave desde la página de Service Account.

authentication-between-microservices

authentication-and-authorization-in-microservices

Descarga el JSON y no olvides definir la fecha de expiración: una nueva funcionalidad de Google Cloud. El JSON se ve así. (Tranquilo, ya censuré bien el texto 😁, ¡y además ya deshabilité la clave!)

{
"type": "service_account",
"project_id": "myproject",
"private_key_id": "ded9d97108b…..5cfd179e95e0e1",
"private_key": " — — -BEGIN PRIVATE KEY — — -\nMIIEvKIBADABNBg….QDA6woGjE4Q — — -END PRIVATE KEY — — -\n",
"client_email": "[email protected]",
"client_id": "106482...4210366919",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/kubeflowpipeline%40joshua-playground.iam.gserviceaccount.com"
}

Limitación

Suena bien, ¿no? Pero (como ya te imaginas) tampoco es tan seguro y conveniente como nos gustaría.

Igual que las API keys caseras, el archivo de clave de la Service Account puede filtrarse, así que hay que rotarlo. Google te ayuda a que las claves expiren y ofrece APIs para obtener una nueva, pero aun así eres tú quien tiene que aprovechar esas capacidades.

Más adelante explicaremos cómo evitar tener un archivo de clave por completo, integrando la Service Account directamente en tus aplicaciones cliente. Pero primero veamos qué hacer con la Service Account, ya sea a través del archivo de clave o de la variante integrada.

Autenticación en el código de la aplicación

Para autenticarse, el servicio cliente usa su Service Account.

Esto se hace con OpenID Connect (OIDC), que transmite JSON Web Tokens (JWT) firmados. Estos tokens son válidos solo por un breve período (horas, no semanas), lo que reduce el riesgo en caso de filtración.

Puedes hacerlo a nivel de tu propio código, con bibliotecas tanto del lado del cliente como del servidor.

  • Primero, el cliente crea y usa una biblioteca de software junto con la clave de la Service Account para firmar un token JWT de solicitud de acceso.
  • Luego usa ese token para pedir otro distinto, un token de acceso, al servidor de autenticación de Google. El servidor de autenticación de Google verifica que la Service Account efectivamente firmó ese JWT de solicitud de acceso y devuelve el token de acceso que lo certifica.
  • El cliente usa este token de acceso para llamar a tu microservicio.
  • Tu microservicio usa una biblioteca de software para verificar que el token de acceso esté efectivamente validado y firmado por el servicio de Google.

El flujo es similar a este, salvo que el servicio que se invoca es tu propio microservicio en lugar de una API de Google.

authorization-in-microservices

Limitación

Acoplamiento con tu servidor

Esta solución implica meter código dentro de tu servidor. Como puedes tener varios microservicios con las mismas necesidades, eso significa mantener y garantizar la seguridad de esta capa de autenticación en varias bases de código.

Más adelante mostraremos cómo evitar insertar este código en tu aplicación. Pero primero, veamos cómo dejar de usar archivos de clave por completo.

Service Accounts integradas: GCP

Si vas a desplegar el servicio cliente en GCP, no uses una clave de Service Account. En su lugar, lanza el servicio con la Service Account ya integrada.

Por ejemplo, con una instancia de Google Compute Engine (GCE), puedes usar

gcloud compute instances create [INSTANCE_NAME] --service-account [SERVICE_ACCOUNT_EMAIL] ...

para especificar la Service Account; y de manera similar para Cloud Run y otros servicios de GCP desde los que tu microservicio cliente pueda invocar otros microservicios.

Ahora ya no hay que preocuparse por filtraciones de archivos de clave, porque sencillamente no existen. En su lugar, el servidor de metadatos genera un token de instancia firmado que verifica la identidad de la cuenta de servicio. (Y la solicitud al servidor de metadatos nunca sale de la instancia física en la que corre la VM.)

En Kubernetes

Kubernetes tiene su propio sistema de cuentas de servicio que participan en un esquema de autenticación específico de Kubernetes. Esto está separado de la capa de IAM de GCP, así que si tu servicio cliente está en Google Kubernetes Engine, usa Workload Identity para asignar una Service Account de IAM de GCP a tu capa de Kubernetes. Workload Identity intercepta y enruta de forma transparente todas las llamadas de GKE a las APIs de GCP, enriqueciéndolas con el token de acceso.

Si tu cliente está en Elastic Kubernetes Service de AWS, también puedes asignar un rol de IAM para participar en los flujos de GCP, como se ve en la siguiente sección.

Roles integrados: AWS y Workload Identity Federation

Si el servicio cliente está en AWS, no puedes lanzarlo con una Service Account de GCP, pero sí con la variante de AWS: el Role. Lanzas tu Lambda con un execution role, o tu instancia EC2 con un role (envuelto en un "Instance Profile").

GCP no puede confiar directamente en ese rol, así que se usa Workload Identity Federation (WIF) para tender un puente entre AWS y GCP (artículo).

Aquí el flujo es el siguiente:

  • Primero, el servicio cliente en AWS usa su rol para firmar un token (token 1).
  • Usa el token 1 para solicitar otro (token 2) firmado por AWS IAM.
  • Usa el token 2 para pedirle a GCP WIF que firme un token de acceso (token 3). Google WIF está preconfigurado para confiar en el rol de AWS dado, y como AWS ya certificó que la solicitud proviene de ese rol, WIF firma y devuelve el token de acceso (token 3).
  • El servicio cliente ahora usa el token 3 igual que lo haría un servicio cliente basado en GCP con un token de acceso; el flujo continúa de la misma manera a partir de aquí.

microservices-authentication-and-authorization

Parece complicado, pero te permite no andar mandando esas cadenas secretas y tan fáciles de filtrar por toda internet, en este caso a otra nube.

Autenticación desde Google a un workload de AWS

Este artículo trata sobre todo de servicios que corren en Google, pero gtoken merece una breve mención. Hace lo inverso de Workload Identity Federation: autentica un workload de GKE para consultar APIs de AWS, otorgándole una identidad temporal de AWS a la invocación.

Proxy de autenticación: API Gateway

Aun así, como mencionamos antes, estamos limitados por el hecho de que es tu propio código de aplicación el que ejecuta el paso final: validar que la firma realmente provenga del principal autorizado por Google. Es mejor usar sistemas probados y productizados, creados por expertos en seguridad, siempre que se pueda.

Por eso, échale un vistazo a API Gateway, una capa robusta y configurable de autenticación service-to-service que no tienes que mantener tú.

Funciona como un proxy. Expone una dirección pública y se ubica entre el cliente y tu servicio serverless en Cloud Run, Cloud Functions y App Engine. Se encarga de recibir el token e invocar a los servicios de Google para autenticar la solicitud, antes de pasarla a tu backend serverless. Para asegurar el enlace entre el API Gateway y el backend, Google inserta un encabezado especial que controla y que ningún atacante puede agregar.

Limitación

Sin embargo, API Gateway no funciona con GKE, ya que está fuertemente integrado con las interfaces que exponen los servicios serverless gestionados por Google.

Proxy de autenticación: Cloud Endpoint

Entonces, ¿cómo te autenticas en GKE asegurando además el enlace desde la capa de autenticación hasta tu servicio backend? Para eso se usa Extensible Service Proxy con Google Cloud Endpoints: un servicio algo más antiguo en el que se basa API Gateway y que este extiende.

ESP (ya en su v2) es un contenedor que expone una dirección pública y autentica las solicitudes. Para usarlo con GKE, despliégalo como un pod en tu clúster. (Por cierto, aunque la documentación dice que solo se admiten los clústeres más nuevos VPC-native/IP alias, también funciona con los clústeres anteriores basados en routes.)

El enlace entre ESPv2 y tus servicios de Kubernetes dentro del clúster también necesita estar asegurado. Puedes hacerlo a nivel de la red del clúster, no exponiendo ninguna dirección pública salvo la del ESP, o puedes usar soluciones más sofisticadas como mutual TLS o Istio Security.

Para más seguridad todavía, despliega ESPv2 como sidecar, de modo que el proxy y tu aplicación (Kubernetes Deployment) convivan en el espacio seguro de "localhost" de un mismo pod. (Aunque este no es el modo de despliegue principal de ESPv2, está soportado en esta receta YAML publicada en la cuenta oficial de GitHub de Google Cloud.)

Para cerrar: ¡asegura tus microservicios!

No puedes dejar que cualquiera invoque tus APIs. Antes esto se resolvía con perímetros de red o, en la nube, con VPCs. Pero las arquitecturas modernas integran cuentas de nube distintas, distintos proveedores de nube y sistemas que no son cloud. E incluso dentro de la VPC conviene tener otra capa de seguridad apuntada exactamente al enlace específico entre cliente y servidor: cada extremo tiene que confiar en el otro.

Para lograrlo, el desafío es:

  • Autenticarse sin tener archivos sensibles dando vueltas en lugares donde puedan filtrarse.
  • Delegar la autenticación a servicios de confianza; no acoplar la autenticación con el código a nivel de aplicación.

En este artículo describí algunas formas de hacerlo, sumando progresivamente seguridad y mantenibilidad, aunque también exigiendo conocimiento en más tecnologías. Vale mucho la pena aprenderlas: ¡resulta mucho más barato que ser víctima de un hackeo!