Implementación de NGINX como puerta de enlace de API, parte 2: protección de los servicios de backend

NGINX | 20 de enero de 2021

Esta es la segunda entrada de nuestra serie sobre la implementación de NGINX Open Source y NGINX Plus como puerta de enlace de API.

  • La parte 1 proporciona instrucciones de configuración detalladas para varios casos de uso.
  • Publicada originalmente en 2018, esta entrada se ha actualizado para reflejar las prácticas recomendadas actuales para la configuración de API, utilizando bloques anidados de location para enrutar las solicitudes en lugar de reglas de reescritura.

Este artículo amplía esos casos de uso y analiza una serie de medidas de protección que pueden aplicarse para proteger y asegurar los servicios de API de backend en producción:

  • Parte 3 explica cómo implementar NGINX Open Source y NGINX Plus como puerta de enlace de API para servicios gRPC.

Nota: Salvo que se indique lo contrario, toda la información de esta publicación se aplica tanto a NGINX Open Source como a NGINX Plus. Para facilitar la lectura, en el resto del blog se utiliza simplemente «NGINX».

Limitación de solicitudes

A diferencia de los clientes basados en navegador, los clientes de API individuales pueden someter sus API a cargas enormes, hasta el punto de consumir una proporción tan elevada de los recursos del sistema que, en la práctica, impidan el acceso a otros clientes de API. Esta amenaza no procede únicamente de clientes maliciosos: un cliente de API que funcione de forma incorrecta o contenga errores podría entrar en un bucle y saturar el backend. Para evitarlo, aplicamos un límite de solicitudes que garantiza un uso equitativo por parte de cada cliente y protege los recursos de los servicios de backend.

NGINX puede aplicar límites de frecuencia en función de cualquier atributo de la solicitud. Normalmente se utiliza la dirección IP del cliente, pero, cuando la autenticación está habilitada para la API, el ID del cliente autenticado es un atributo más fiable y preciso.

Los límites de frecuencia se definen en el archivo de configuración de nivel superior de la puerta de enlace de API y, posteriormente, pueden aplicarse de forma global, por API o incluso por URI.

Cargando ejemplo de código…

En este ejemplo, la directiva limit_req_zone de la línea 4 define un límite de 10 solicitudes por segundo para cada dirección IP de cliente ($binary_remote_addr), mientras que la de la línea 5 define un límite de 200 solicitudes por segundo para cada ID de cliente autenticado ($http_apikey). Esto ilustra cómo podemos definir varios límites de frecuencia independientemente de dónde se apliquen. Una API puede aplicar varios límites de frecuencia al mismo tiempo o aplicar límites diferentes a distintos recursos.

A continuación, en el siguiente fragmento de configuración usamos la directiva limit_req para aplicar el primer límite de solicitudes en la sección de políticas de la «API de almacén» descrita en la Parte 1<.htmla>. De forma predeterminada, NGINX envía la respuesta 503 (Servicio no disponible) cuando se ha superado el límite de solicitudes. Sin embargo, resulta útil que los clientes de la API sepan explícitamente que han superado dicho límite para que puedan modificar su comportamiento. Para ello, usamos la directiva limit_req_status para enviar en su lugar la respuesta 429 (Demasiadas solicitudes ).

Cargando ejemplo de código…

Puede utilizar parámetros adicionales en la directiva limit_req para ajustar con precisión cómo aplica NGINX los límites de velocidad. Por ejemplo, cuando se supera el límite, es posible poner las solicitudes en cola en lugar de rechazarlas directamente, lo que permite que la frecuencia de las solicitudes descienda por debajo del límite definido. Para obtener más información sobre cómo ajustar los límites de velocidad, consulte Limitación de velocidad con NGINX y NGINX Plus en nuestro blog.

Aplicación de límites a métodos de solicitud específicos

En las API RESTful, el método HTTP (o verbo) es una parte importante de cada llamada a la API y resulta fundamental para definirla. Tomemos como ejemplo el servicio de precios de nuestra API Warehouse:

  • GET /api/warehouse/pricing/item001 devuelve el precio de item001
  • PATCH /api/warehouse/pricing/item001 cambia el precio de item001

Podemos actualizar las definiciones de enrutamiento de URI en la API de almacén para que solo se acepten estos dos métodos HTTP en las solicitudes al servicio de precios (y únicamente el método GET en las solicitudes al servicio de inventario).

Cargando ejemplo de código…

Con esta configuración, las solicitudes al servicio de precios que utilicen métodos distintos de los indicados en la línea 22 (y las solicitudes al servicio de inventario que utilicen un método distinto del indicado en la línea 13) se rechazan y no se envían a los servicios de backend. NGINX envía la respuesta 405 (Método no permitido) para informar al cliente de la API de la naturaleza exacta del error, como se muestra en el siguiente registro de consola. Cuando se requiera una política de seguridad de divulgación mínima, se puede utilizar la directiva error_page para convertir esta respuesta en un error menos informativo, por ejemplo, 400 (Solicitud incorrecta).

$ curl https://api.example.com/api/warehouse/pricing/item001{"sku":"item001","price":179.99}
$ curl -X DELETE https://api.example.com/api/warehouse/pricing/item001
{"status":405,"message":"Method not allowed"}

Aplicación de un control de acceso granular

La parte 1 de esta serie describía cómo proteger las API frente al acceso no autorizado habilitando opciones de autenticación como las claves de API y los tokens web JSON (JWT). Podemos utilizar el identificador autenticado o sus atributos para aplicar un control de acceso detallado.

A continuación, mostramos dos ejemplos de este tipo.

Por supuesto, también pueden aplicarse otros métodos de autenticación a estos casos de uso de ejemplo, como la autenticación básica HTTP y la introspección de tokens de OAuth 2.0.

Control del acceso a recursos específicos

Supongamos que queremos permitir que solo los «clientes de infraestructura» accedan al recurso de auditoría del servicio de inventario de la API Warehouse. Con la autenticación mediante clave de API habilitada, usamos un bloque map para crear una lista de nombres de clientes de infraestructura permitidos, de modo que la variable $is_infrastructure se evalúe como 1 cuando se utilice una clave de API correspondiente.

Cargando ejemplo de código…

En la definición de la API de almacén, añadimos un bloque location para el recurso de auditoría del inventario (líneas 15–20). El bloque if garantiza que solo los clientes de infraestructura puedan acceder al recurso.

Cargando ejemplo de código…

Tenga en cuenta que la directiva location de la línea 15 utiliza el modificador = (signo igual) para establecer una coincidencia exacta con el recurso audit. Las coincidencias exactas tienen prioridad sobre las definiciones predeterminadas de prefijos de ruta utilizadas para los demás recursos. La siguiente traza muestra cómo, con esta configuración, un cliente que no está en la lista de permitidos no puede acceder al recurso audit del inventario. La clave de API mostrada pertenece a client_two (tal como se define en la Parte 1).

$ curl -H "apikey: QzVV6y1EmQFbbxOfRCwyJs35" https://api.example.com/api/warehouse/inventory/audit{"status":403,"message":"Forbidden"}

Control del acceso a métodos específicos

Como se ha indicado anteriormente, el servicio de precios acepta los métodos GET y PATCH, que permiten a los clientes consultar y modificar, respectivamente, el precio de un artículo concreto. (También podríamos permitir los métodos POST y DELETE para ofrecer una gestión completa del ciclo de vida de los datos de precios). En esta sección, ampliamos este caso de uso para controlar qué métodos pueden utilizar determinados usuarios. Cuando la autenticación JWT está habilitada para la API de Warehouse, los permisos de cada cliente se codifican como notificaciones personalizadas. Los JWT emitidos a los administradores autorizados para modificar los datos de precios incluyen la notificación "admin":true. Ahora ampliamos nuestra lógica de control de acceso para que solo los administradores puedan realizar cambios.

Cargando ejemplo de código…

Este bloque map, añadido al final de api_gateway.conf, toma como entrada el método de la solicitud ($request_method) y genera una nueva variable, $admin_permitted_method. Los métodos de solo lectura siempre están permitidos (líneas 62–64), pero el acceso a las operaciones de escritura depende del valor de la declaración admin del JWT (línea 65). Ahora ampliamos la configuración de nuestra API de almacén para garantizar que solo los administradores puedan modificar los precios.

Cargando ejemplo de código…

La API de Warehouse exige que todos los clientes presenten un JWT válido (línea 7). También comprobamos que las operaciones de escritura estén permitidas evaluando la variable $admin_permitted_method (línea 25). Recuerde que la autenticación mediante JWT es exclusiva de NGINX Plus.

Control del tamaño de las solicitudes

Las API HTTP suelen utilizar el cuerpo de la solicitud para incluir instrucciones y datos que debe procesar el servicio de API de backend. Esto se aplica tanto a las API XML/SOAP como a las API JSON/REST. Por consiguiente, el cuerpo de la solicitud puede constituir un vector de ataque contra los servicios de API de backend, que pueden ser vulnerables a ataques de desbordamiento de búfer al procesar cuerpos de solicitud de gran tamaño.

De forma predeterminada, NGINX rechaza las solicitudes cuyo cuerpo supera 1 MB. Este límite puede aumentarse para las API que procesan específicamente cargas útiles de gran tamaño, como imágenes, pero para la mayoría de las API establecemos un valor inferior.

Cargando ejemplo de código…

La directiva client_max_body_size de la línea 7 limita el tamaño del cuerpo de la solicitud. Con esta configuración, podemos comparar el comportamiento de la puerta de enlace de la API al recibir dos solicitudes PATCH diferentes dirigidas al servicio de precios. El primer comando curl envía una pequeña cantidad de datos JSON, mientras que el segundo intenta enviar el contenido de un archivo de gran tamaño (/etc/services).

$ curl -iX PATCH -d '{"price":199.99}' https://api.example.com/api/warehouse/pricing/item001HTTP/1.1 204 No Content
Server: nginx/1.19.5
Connection: keep-alive

$ curl -iX PATCH -d@/etc/services https://api.example.com/api/warehouse/pricing/item001
HTTP/1.1 413 Request Entity Too Large
Server: nginx/1.19.5
Content-Type: application/json
Content-Length: 45
Connection: close

{"status":413,"message":"Payload too large"}

Validación de los cuerpos de las solicitudes

[Editor – El siguiente caso de uso es uno de los varios disponibles para el módulo JavaScript de NGINX. Para consultar la lista completa, véase Casos de uso del módulo JavaScript de NGINX.]

Además de ser vulnerables a ataques de desbordamiento de búfer cuando los cuerpos de las solicitudes son de gran tamaño, los servicios de API de backend pueden verse afectados por cuerpos que contengan datos no válidos o inesperados. En el caso de las aplicaciones que requieren que el cuerpo de la solicitud contenga datos JSON con el formato correcto, podemos utilizar el módulo JavaScript de NGINX<.htmla> para comprobar que los datos JSON se analizan sin errores antes de reenviar la solicitud al servicio de API de backend.

Con el módulo JavaScript instalado, utilizamos la directiva js_import para hacer referencia al archivo que contiene el código JavaScript de la función que valida los datos JSON.

Cargando ejemplo de código…

La directiva «js_set» define una nueva variable, $json_validated, que se evalúa llamando a la función «parseRequestBody».

Cargando ejemplo de código…

La función parseRequestBody intenta analizar el cuerpo de la solicitud mediante el método JSON.parse (línea 6). Si el análisis se realiza correctamente, se devuelve el nombre del grupo de servidores ascendentes previsto para esta solicitud (línea 8). Si no se puede analizar el cuerpo de la solicitud (lo que provoca una excepción), se devuelve la dirección de un servidor local (línea 11). La directiva return asigna un valor a la variable $json_validated para que podamos usarla a fin de determinar adónde enviar la solicitud.

Cargando ejemplo de código…

En la sección de enrutamiento de URI de la API de Warehouse, modificamos la directiva proxy_pass de la línea 22. Esta pasa la solicitud al servicio de API de backend, como en las configuraciones de la API de Warehouse descritas en secciones anteriores, pero ahora utiliza la variable $json_validated como dirección de destino. Si el cuerpo de la solicitud del cliente se ha analizado correctamente como JSON, redirigimos la solicitud al grupo de servidores upstream definido en la línea 15. Sin embargo, si se produce una excepción, utilizamos el valor devuelto 127.0.0.1:10415 para enviar una respuesta de error al cliente.

Cargando ejemplo de código…

Cuando las solicitudes se redirigen mediante proxy a este servidor virtual, NGINX envía la respuesta 415 (Tipo de contenido no compatible) al cliente.

Con esta configuración completa, NGINX solo redirige las solicitudes al servicio de API de backend si contienen cuerpos JSON con el formato correcto.

$ curl -iX POST -d '{"sku":"item002","price":85.00}' https://api.example.com/api/warehouse/pricingHTTP/1.1 201 Created
Server: nginx/1.19.5
Location: /api/warehouse/pricing/item002

$ curl -X POST -d 'item002=85.00' https://api.example.com/api/warehouse/pricing
{"status":415,"message":"Unsupported media type"}

Una nota sobre la variable $request_body Variable

La función de JavaScript parseRequestBody utiliza la variable $request_body para analizar el contenido JSON. Sin embargo, NGINX no rellena esta variable de forma predeterminada, sino que simplemente transmite el cuerpo de la solicitud al backend sin crear copias intermedias. Al utilizar la directiva mirror dentro de la sección de enrutamiento de URI (línea 16), creamos una copia de la solicitud del cliente y, por consiguiente, rellenamos la variable $request_body.

Cargando ejemplo de código…

Las directivas de las líneas 17 y 19 controlan cómo gestiona NGINX internamente el cuerpo de la solicitud. Establecemos client_body_buffer_size con el mismo tamaño que client_max_body_size para que el cuerpo de la solicitud no se escriba en el disco. Esto mejora el rendimiento general al minimizar las operaciones de E/S del disco, aunque a costa de un mayor uso de memoria. Para la mayoría de los casos de uso de puertas de enlace de API con cuerpos de solicitud pequeños, esta es una buena solución de compromiso.

Como se ha mencionado, la directiva mirror crea una copia de la solicitud del cliente. Aparte de rellenar $request_body, no necesitamos esta copia, por lo que la enviamos a una ubicación «sin salida» (/_get_request_body) que definimos en el bloque server de la configuración de nivel superior de la puerta de enlace de API.

Cargando ejemplo de código…

Esta ubicación se limita a enviar la respuesta 204 (Sin contenido). Como esta respuesta está relacionada con una solicitud replicada, se ignora y, por tanto, apenas añade sobrecarga al procesamiento de la solicitud original del cliente.

Resumen

En esta segunda entrada de nuestra serie sobre la implementación de NGINX Open Source y NGINX Plus como puerta de enlace de API, nos centramos en el reto de proteger los servicios de API de backend en un entorno de producción frente a clientes maliciosos o con un comportamiento inadecuado. NGINX utiliza para gestionar el tráfico de las API la misma tecnología que se emplea para hacer funcionar y proteger los sitios con más tráfico de Internet en la actualidad.

Consulta la otra entrada de esta serie:

  • La parte 1 explica cómo configurar NGINX para algunos casos de uso esenciales de una puerta de enlace de API.
  • Parte 3 explica cómo implementar NGINX como puerta de enlace de API para servicios gRPC.

Para probar NGINX Plus como puerta de enlace de API, inicie hoy mismo su prueba gratuita de 30 días o póngase en contacto con nosotros para hablar sobre sus casos de uso. Durante el periodo de prueba, utilice el conjunto completo de archivos de configuración de nuestro repositorio de GitHub Gist.


Compartir
Etiquetas: Security, F5 NGINX, 2021, Tech

Sobre el autor

Liam Crilly
Liam CrillyDirector sénior de Gestión de Producto

Más artículos de Liam Crilly

Artículos relacionados del blog

Únete a F5 NGINX en AppWorld 2024
NGINX | 04/01/2024

Únete a F5 NGINX en AppWorld 2024

Descubre las numerosas razones para asistir al evento insignia de F5, que incluirá un emocionante anuncio sobre la cartera de productos NGINX.

Rotación de certificados SSL/TLS sin reinicios en NGINX Open Source
NGINX | 26/09/2023

Rotación de certificados SSL/TLS sin reinicios en NGINX Open Source

La función de diccionario compartido permite configurar NGINX sin necesidad de reiniciarlo al renovar los certificados SSL/TLS.

Exposición de APIs en Kubernetes
NGINX | 10/03/2022

Exposición de APIs en Kubernetes

La unidad 2 de Microservices March 2022 se centra en cómo exponer tus API en Kubernetes. Aquí tienes enlaces a los recursos que necesitas: la retransmisión en directo del 14 de marzo, artículos de blog y otros recursos de preparación, y el laboratorio práctico «Protege las API de Kubernetes mediante la limitación de solicitudes».

NGINX Sprint 2.0: visión clara, código renovado y nuevos compromisos con el código abierto
NGINX | 23/08/2021

NGINX Sprint 2.0: visión clara, código renovado y nuevos compromisos con el código abierto

Hoy, en NGINX Sprint 2.0, anunciamos cómo respaldaremos nuestra visión del futuro de las aplicaciones modernas con una mayor presencia en la comunidad de código abierto, incluida la participación en el proyecto comunitario Kubernetes, nuevos proyectos de software de código abierto y la arquitectura de referencia para aplicaciones modernas.

Presentamos NGINX Plus R22
NGINX | 09/06/2020

Presentamos NGINX Plus R22

NGINX Plus R22 incorpora nuevas funcionalidades, como la compatibilidad con OCSP para verificar la revocación de certificados en TLS mutuo, el uso de varios proveedores de identidad OIDC en una configuración, el seguimiento en tiempo real de la limitación de solicitudes y conexiones en el panel de NGINX Plus y mejoras en NGINX JavaScript.

Presentamos NGINX Plus R17
NGINX | 11/12/2018

Presentamos NGINX Plus R17

NGINX Plus R17 incorpora compatibilidad con la limitación de velocidad en dos fases y con TLS 1.3, la versión más reciente del protocolo Transport Layer Security. La configuración de OpenID Connect es más sencilla y NGINX ModSecurity WAF es el doble de rápido que antes. También se ha actualizado el módulo JavaScript de NGINX.