Aller au contenu
Infrastructure · 4 min de lecture

Limitez le débit en périphérie avant que la requête ne coûte

L'affirmation L'endroit où rejeter le trafic abusif est l'endroit le moins cher de votre pile pour le rejeter, et c'est le mandataire inverse en périphérie — avant que la requête n...

A Rédigé par Administrator
Limitez le débit en périphérie avant que la requête ne coûte

L'affirmation

L'endroit où rejeter le trafic abusif est l'endroit le moins cher de votre pile pour le rejeter, et c'est le mandataire inverse en périphérie — avant que la requête n'atteigne votre application, n'ouvre une connexion de base de données ou n'occupe un processus. Les équipes intègrent couramment la limitation de débit dans l'application, où chaque requête rejetée a déjà coûté presque autant qu'une acceptée. Rejeter en périphérie fait qu'un déluge de dix mille requêtes par seconde ne vous coûte presque rien, parce que Nginx dit non avant même que votre code coûteux ne s'exécute.

Pourquoi la couche compte plus que l'algorithme

Une requête qui atteint votre application a déjà consommé les ressources coûteuses : un processus est occupé, une connexion de base de données peut être retirée, de la mémoire est allouée. La rejeter là vous protège d'une mauvaise logique mais pas de la charge — dix mille requêtes rejetées mobilisent tout de même l'attention de dix mille processus. Rejeter la même requête au mandataire inverse coûte une recherche de hachage et l'incrément d'un compteur. La même défense à une couche différente, c'est l'écart entre une attaque dont vous vous moquez et une attaque qui épuise votre bassin de connexions pendant que vous êtes occupé à la rejeter.

La configuration

Nginx met en œuvre un limiteur à seau percé en quelques lignes. Définissez les zones selon ce que vous protégez :

limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=login:10m   rate=5r/m;

server {
  location / {
    limit_req zone=general burst=60 nodelay;
  }
  location = /login {
    limit_req zone=login burst=3 nodelay;
    limit_req_status 429;
  }
}

Deux paramètres portent la conception. rate est l'allocation soutenue ; burst est le nombre de requêtes qui peuvent s'empiler au-dessus avant rejet, ce qui absorbe les rafales légitimes comme une page qui émet plusieurs requêtes au chargement. nodelay sert la rafale immédiatement plutôt que de la lisser, ce que vous voulez pour du trafic interactif. La taille de zone 10m contient environ 160 000 adresses clientes distinctes, amplement pour un hôte unique.

Fixez des limites différentes selon les points d'accès

Une limite globale unique est le mauvais modèle, car vos points d'accès ont des coûts et des profils d'abus radicalement différents. La route de connexion mérite une limite stricte mesurée en requêtes par minute, car c'est là qu'atterrissent les attaques par bourrage d'identifiants et qu'aucun utilisateur légitime ne se connecte dix fois par seconde. Un point de recherche qui frappe la base mérite une limite plus serrée qu'une ressource statique. Une API publique mérite des limites par clé, pas par IP, car une intégration légitime derrière une seule adresse d'entreprise ne devrait pas être bridée comme un utilisateur abusif. Les zones ci-dessus sont le mécanisme ; ajuster la limite de chaque route à son coût et à son risque réels est le vrai travail.

L'en-tête qui rend les limites utilisables

Une limite qui rejette sans explication génère des billets de la part de clients légitimes incapables de distinguer le bridage d'un bogue. Renvoyez les en-têtes standards pour que les clients bien élevés se retirent correctement :

RateLimit-Limit: 30
RateLimit-Remaining: 12
RateLimit-Reset: 18
Retry-After: 18

Retry-After sur une réponse 429 dit au client exactement combien de temps attendre, et tout consommateur d'API compétent l'honorera. Cela transforme la limitation d'une boîte noire adversariale en un contrat auquel le client peut coopérer, ce qui réduit radicalement le fardeau de soutien lié au fait d'avoir des limites.

Le piège : limiter par IP derrière un mandataire

Si votre Nginx siège derrière un RDC ou un répartiteur, $binary_remote_addr est l'adresse de cet intermédiaire, pas du client — toutes les requêtes semblent donc venir d'une poignée d'IP de mandataire, et vous limitez soit tous vos utilisateurs comme un seul, soit désactivez la limite dans la confusion. Vous devez lire la vraie adresse cliente dans l'en-tête réacheminé, et ne faire confiance à cet en-tête que de la part de vos mandataires connus :

set_real_ip_from 10.0.0.0/8;      # seulement vos plages RDC/repartiteur
real_ip_header X-Forwarded-For;

Faire confiance à X-Forwarded-For de sources arbitraires est une vulnérabilité en soi, car un attaquant peut alors falsifier l'en-tête pour contourner la limite ou incriminer une autre adresse. Ne vous y fiez que des plages précises qu'utilise votre infrastructure.

Là où la périphérie ne suffit pas

La limitation en périphérie gère le volume et l'abus simple, mais certaines limites sont des règles d'affaires que seule l'application connaît : cinq tentatives de paiement échouées par compte par jour, un courriel de réinitialisation par adresse par quinze minutes, un quota par locataire sur un rapport coûteux. Ces règles s'appliquent correctement dans l'application, car elles dépendent d'une identité et d'un état que le mandataire ne voit pas. La bonne architecture, ce sont les deux couches : la périphérie déleste le volume à bas prix pour que l'application ne voie jamais le déluge, et l'application applique les règles qui exigent de savoir qui est l'utilisateur. Commencez par la périphérie, car c'est le moins cher et cela attrape le plus, puis ajoutez les règles applicatives là où une vraie limite d'affaires les réclame.

#nginx #rate limiting #security #scaling

À lire aussi