Il s'agit du deuxième article de blog de notre série sur le déploiement de NGINX Open Source et NGINX Plus en tant que passerelle API.
La partie 1 fournit des instructions de configuration détaillées pour plusieurs cas d’utilisation.
Cet article étend ces cas d'utilisation et examine une gamme de mesures de protection qui peuvent être appliquées pour protéger et sécuriser les services API back-end en production :
Initialement publié en 2018, cet article a été mis à jour pour refléter les meilleures pratiques actuelles en matière de configuration d'API, en utilisant des blocs d'emplacement
imbriqués pour acheminer les requêtes au lieu de règles de réécriture.
Note : Sauf indication contraire, toutes les informations contenues dans cet article s’appliquent à la fois à NGINX Open Source et à NGINX Plus. Pour faciliter la lecture, le reste du blog fait simplement référence à « NGINX ».
Contrairement aux clients basés sur un navigateur, les clients API individuels sont capables de placer d’énormes charges sur vos API, allant même jusqu’à consommer une proportion si élevée de ressources système que les autres clients API sont effectivement exclus. Les clients malveillants ne sont pas les seuls à représenter cette menace : un client API présentant un comportement incorrect ou des bugs peut entrer dans une boucle qui submerge le backend. Pour nous protéger contre cela, nous appliquons une limite de débit pour garantir une utilisation équitable par chaque client et pour protéger les ressources des services backend.
NGINX peut appliquer des limites de débit en fonction de n’importe quel attribut de la demande. L'adresse IP du client est généralement utilisée, mais lorsque l'authentification est activée pour l'API, l'ID client authentifié est un attribut plus fiable et plus précis.
Les limites de débit elles-mêmes sont définies dans le fichier de configuration de la passerelle API de niveau supérieur et peuvent ensuite être appliquées globalement, par API ou même par URI.
Dans cet exemple, la directive limit_req_zone
de la ligne 4 définit une limite de débit de 10 requêtes par seconde pour chaque adresse IP client ( $binary_remote_addr
), et celle de la ligne 5 définit une limite de 200 requêtes par seconde pour chaque ID client authentifié ( $http_apikey
). Ceci illustre comment nous pouvons définir plusieurs limites de débit indépendamment de l’endroit où elles sont appliquées. Une API peut appliquer plusieurs limites de débit en même temps ou appliquer des limites de débit différentes pour différentes ressources.
Ensuite, dans l’extrait de configuration suivant, nous utilisons la directive limit_req
pour appliquer la première limite de débit dans la section de politique de l’« API Warehouse » décrite dans la partie 1<.htmla>. Par défaut, NGINX envoie le 503
(Service
indisponible)
réponse lorsque la limite de débit a été dépassée. Cependant, il est utile pour les clients API de savoir explicitement qu’ils ont dépassé leur limite de débit, afin qu’ils puissent modifier leur comportement. À cette fin, nous utilisons la directive limit_req_status
pour envoyer le 429
(Trop
de
demandes)
à la place.
Vous pouvez utiliser des paramètres supplémentaires pour la directive limit_req
pour affiner la manière dont NGINX applique les limites de débit. Par exemple, il est possible de mettre les demandes en file d'attente au lieu de les rejeter purement et simplement lorsque la limite est dépassée, laissant ainsi le temps au taux de demandes de tomber sous la limite définie. Pour plus d’informations sur le réglage précis des limites de débit, consultez Limitation de débit avec NGINX et NGINX Plus sur notre blog.
Avec les API RESTful, la méthode HTTP (ou verbe) est une partie importante de chaque appel d'API et très significative pour la définition de l'API. Prenons comme exemple le service de tarification de notre API Warehouse :
GET
/api/warehouse/pricing/item001
renvoie le prix de l'article001PATCH
/api/warehouse/pricing/item001
modifie le prix de l'article001Nous pouvons mettre à jour les définitions de routage URI dans l’API Warehouse pour accepter uniquement ces deux méthodes HTTP dans les requêtes adressées au service de tarification (et uniquement la méthode GET
dans les requêtes adressées au service d’inventaire).
Avec cette configuration en place, les demandes adressées au service de tarification utilisant des méthodes autres que celles répertoriées à la ligne 22 (et au service d'inventaire autre que celui de la ligne 13) sont rejetées et ne sont pas transmises aux services backend. NGINX envoie le 405
(Méthode
non
autorisée)
réponse pour informer le client API de la nature précise de l'erreur, comme indiqué dans la trace de console suivante. Lorsqu'une politique de sécurité de divulgation minimale est requise, la directive error_page
peut être utilisée pour convertir cette réponse en une erreur moins informative, par exemple400
(Mauvaise
demande)
.
$ 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":"Méthode non autorisée"}
La partie 1 de cette série décrit comment protéger les API contre les accès non autorisés en activant des options d'authentification telles que les clés API et les jetons Web JSON (JWT) . Nous pouvons utiliser l’ID authentifié, ou les attributs de l’ID authentifié, pour effectuer un contrôle d’accès précis.
Nous montrons ici deux exemples de ce type.
Bien entendu, d’autres méthodes d’authentification sont applicables à ces exemples de cas d’utilisation, telles que l’authentification HTTP de base et l’introspection de jetons OAuth 2.0 .
Disons que nous souhaitons autoriser uniquement les « clients d’infrastructure » à accéder à la ressource d’audit du service d’inventaire de l’API Warehouse. Avec l'authentification par clé API activée, nous utilisons un bloc de mappage
pour créer une liste blanche de noms de clients d'infrastructure afin que la variable $is_infrastructure
soit évaluée à1
lorsqu'une clé API correspondante est utilisée.
Dans la définition de l’API Warehouse, nous ajoutons un bloc d’emplacement
pour la ressource d’audit d’inventaire ( lignes 15 à 20 ). Le bloc if
garantit que seuls les clients de l'infrastructure peuvent accéder à la ressource.
Notez que la directive d'emplacement
sur la ligne 15 utilise le modificateur =
(signe égal) pour établir une correspondance exacte sur la ressource d'audit . Les correspondances exactes ont priorité sur les définitions de préfixe de chemin par défaut utilisées pour les autres ressources. La trace suivante montre comment, avec cette configuration en place, un client qui ne figure pas sur la liste blanche ne peut pas accéder à la ressource d'audit d'inventaire. La clé API affichée appartient à client_two (comme défini dans la partie 1 ).
$ curl -H "clé API : QzVV6y1EmQFbbxOfRCwyJs35" https://api.example.com/api/warehouse/inventory/audit {"status":403,"message":"Interdit"}
Comme défini ci-dessus , le service de tarification accepte les méthodes GET
et PATCH
, qui permettent respectivement aux clients d'obtenir et de modifier le prix d'un article spécifique. (Nous pourrions également choisir d'autoriser les méthodes POST
et DELETE
, pour fournir une gestion complète du cycle de vie des données de tarification.) Dans cette section, nous développons ce cas d'utilisation pour contrôler les méthodes que des utilisateurs spécifiques peuvent utiliser. Avec l’authentification JWT activée pour l’API Warehouse, les autorisations de chaque client sont codées sous forme de revendications personnalisées. Les JWT émis aux administrateurs autorisés à apporter des modifications aux données de tarification incluent la revendication « admin » : true
. Nous étendons désormais notre logique de contrôle d’accès afin que seuls les administrateurs puissent apporter des modifications.
Ce bloc de carte
, ajouté au bas de api_gateway.conf , prend la méthode de requête ( $request_method
) comme entrée et produit une nouvelle variable, $admin_permitted_method
. Les méthodes en lecture seule sont toujours autorisées (lignes 62 à 64), mais l’accès aux opérations d’écriture dépend de la valeur de la revendication d’administration dans le JWT (ligne 65). Nous étendons désormais notre configuration API Warehouse pour garantir que seuls les administrateurs peuvent apporter des modifications de prix.
L'API Warehouse exige que tous les clients présentent un JWT valide (ligne 7). Nous vérifions également que les opérations d'écriture sont autorisées en évaluant la variable $admin_permitted_method
(ligne 25). Notez à nouveau que l’authentification JWT est exclusive à NGINX Plus.
Les API HTTP utilisent généralement le corps de la requête pour contenir des instructions et des données que le service API backend doit traiter. Ceci est vrai pour les API XML/SOAP ainsi que pour les API JSON/REST. Par conséquent, le corps de la requête peut constituer un vecteur d’attaque pour les services API back-end, qui peuvent être vulnérables aux attaques par dépassement de mémoire tampon lors du traitement de corps de requête très volumineux.
Par défaut, NGINX rejette les requêtes dont le corps est supérieur à 1 Mo. Cette valeur peut être augmentée pour les API qui traitent spécifiquement de charges utiles volumineuses telles que le traitement d'images, mais pour la plupart des API, nous définissons une valeur inférieure.
La directive client_max_body_size
sur la ligne 7 limite la taille du corps de la requête. Avec cette configuration en place, nous pouvons comparer le comportement de la passerelle API lors de la réception de deux demandes PATCH
différentes au service de tarification. La première commande curl
envoie un petit morceau de données JSON, tandis que la deuxième commande tente d'envoyer le contenu d'un gros fichier ( /etc/services ).
$ curl -iX PATCH -d '{"price":199.99}' https://api.example.com/api/warehouse/pricing/item001 HTTP/1.1 204 Aucun contenu Serveur : nginx/1.19.5 Connexion : keep-alive $ curl -iX PATCH -d@/etc/services https://api.example.com/api/warehouse/pricing/item001 HTTP/1.1 413 Entité de requête trop grande Serveur : nginx/1.19.5 Type de contenu : application/json Longueur du contenu : 45 Connexion : fermer {"status":413,"message":"Charge utile trop importante"}
[ Éditeur – Le cas d’utilisation suivant est l’un des nombreux cas d’utilisation du module JavaScript NGINX. Pour une liste complète, voir Cas d'utilisation du module JavaScript NGINX . ]
En plus d'être vulnérables aux attaques par dépassement de mémoire tampon avec des corps de requête volumineux, les services API back-end peuvent être sensibles aux corps contenant des données non valides ou inattendues. Pour les applications qui nécessitent un JSON correctement formaté dans le corps de la requête, nous pouvons utiliser le module JavaScript NGINX<.htmla> pour vérifier que les données JSON sont analysées sans erreur avant de les transmettre au service API backend.
Avec le module JavaScript installé , nous utilisons la directive js_import
pour référencer le fichier contenant le code JavaScript de la fonction qui valide les données JSON.
La directive js_set
définit une nouvelle variable, $json_validated
, qui est évaluée en appelant la fonction parseRequestBody
.
La fonction parseRequestBody
tente d'analyser le corps de la requête à l'aide de la méthode JSON.parse
(ligne 6). Si l'analyse réussit, le nom du groupe en amont prévu pour cette demande est renvoyé (ligne 8). Si le corps de la requête ne peut pas être analysé (provoquant une exception), une adresse de serveur local est renvoyée (ligne 11). La directive return
remplit la variable $json_validated
afin que nous puissions l'utiliser pour déterminer où envoyer la requête.
Dans la section de routage URI de l’API Warehouse, nous modifions la directive proxy_pass
à la ligne 22. Il transmet la demande au service API backend comme dans les configurations API Warehouse décrites dans les sections précédentes, mais utilise désormais la variable $json_validated
comme adresse de destination. Si le corps du client a été analysé avec succès en tant que JSON, nous utilisons un proxy vers le groupe en amont défini à la ligne 15. Si toutefois une exception se produisait, nous utiliserions la valeur renvoyée de 127.0.0.1:10415
pour envoyer une réponse d'erreur au client.
Lorsque les requêtes sont transmises par proxy à ce serveur virtuel, NGINX envoie le 415
(
Type de média
non pris en charge )
réponse au client.
Avec cette configuration complète en place, NGINX envoie les requêtes au service API backend uniquement si elles ont des corps JSON correctement formatés.
$ curl -iX POST -d '{"sku":"item002","price":85.00}' https://api.example.com/api/warehouse/pricing HTTP/1.1 201 Serveur créé : nginx/1.19.5 Emplacement : /api/warehouse/pricing/item002 $ curl -X POST -d 'item002=85.00' https://api.example.com/api/warehouse/pricing {"status":415,"message":"Type de média non pris en charge"}
$request_body
La fonction JavaScript parseRequestBody
utilise la variable $request_body
pour effectuer l'analyse JSON. Cependant, NGINX ne renseigne pas cette variable par défaut et transmet simplement le corps de la requête au backend sans effectuer de copies intermédiaires. En utilisant la directive mirror
dans la section de routage URI (ligne 16), nous créons une copie de la demande du client et remplissons par conséquent la variable $request_body
.
Les directives des lignes 17 et 19 contrôlent la manière dont NGINX gère le corps de la requête en interne. Nous définissons client_body_buffer_size
sur la même taille que client_max_body_size
afin que le corps de la requête ne soit pas écrit sur le disque. Cela améliore les performances globales en minimisant les opérations d’E/S sur disque, mais au détriment d’une utilisation supplémentaire de la mémoire. Pour la plupart des cas d’utilisation de passerelle API avec de petits corps de requête, il s’agit d’un bon compromis.
Comme mentionné, la directive miroir
crée une copie de la demande du client. Hormis le remplissage de $request_body
, nous n’avons pas besoin de cette copie, nous l’envoyons donc vers un emplacement « sans issue » ( /_get_request_body
) que nous définissons dans le bloc serveur
dans la configuration de la passerelle API de niveau supérieur.
Cet emplacement ne fait rien de plus que d'envoyer le 204
(Pas de
contenu)
réponse. Étant donné que cette réponse est liée à une demande en miroir, elle est ignorée et ajoute donc une surcharge négligeable au traitement de la demande client d’origine.
Dans ce deuxième article de blog de notre série sur le déploiement de NGINX Open Source et NGINX Plus en tant que passerelle API, nous nous sommes concentrés sur le défi de la protection des services API back-end dans un environnement de production contre les clients malveillants et mal intentionnés. NGINX utilise la même technologie pour gérer le trafic API que celle utilisée pour alimenter et protéger les sites les plus fréquentés sur Internet aujourd'hui .
Découvrez l’autre article de cette série :
Pour essayer NGINX Plus en tant que passerelle API, démarrez votre essai gratuit de 30 jours dès aujourd'hui ou contactez-nous pour discuter de vos cas d'utilisation . Pendant votre essai, utilisez l'ensemble complet des fichiers de configuration de notre référentiel GitHub Gist .
« Cet article de blog peut faire référence à des produits qui ne sont plus disponibles et/ou qui ne sont plus pris en charge. Pour obtenir les informations les plus récentes sur les produits et solutions F5 NGINX disponibles, explorez notre famille de produits NGINX . NGINX fait désormais partie de F5. Tous les liens NGINX.com précédents redirigeront vers un contenu NGINX similaire sur F5.com."