Skip to content

Rate limit : freiner le brute force sans toucher à l'application

Contexte : ADR 039, issue #129.

Un rate limit (limite de débit) compte les requêtes d'un client sur une fenêtre de temps, et refuse celles qui dépassent. Cette page explique où il est posé dans ce projet, comment une requête le traverse, et ce qu'il ne protège pas.

En trois phrases

POST /login et POST /users/ étaient publics et sans limite : 30 essais de mot de passe d'affilée passaient tous. Envoy compte maintenant les requêtes de chaque IP sur ces deux routes, et répond 429 au-delà de 10 par minute. L'application n'a pas changé d'une ligne.

Le vocabulaire

Terme Ce que c'est
Brute force essayer des mots de passe en masse jusqu'à tomber sur le bon
429 Too Many Requests le code HTTP qui dit « trop de requêtes, réessaie plus tard »
HTTPRoute ressource Gateway API qui dit quelle URL va vers quel Service
BackendTrafficPolicy ressource Envoy Gateway qui règle le trafic d'une route : délais, rate limit…
targetRefs le champ d'une policy qui dit à quelle route elle s'applique
Rate limit Local compteur tenu dans Envoy lui-même, sans service externe
Rate limit Global compteur partagé dans un Redis, pour plusieurs Envoy

Le trajet d'une requête

flowchart TD
    C["Client"] --> N["NLB AWS"]
    N --> E["Envoy<br/>(proxy du Gateway)"]
    E -->|"POST /login<br/>POST /users/"| RA["route fastapi-auth<br/>+ policy rate limit"]
    E -->|"tout le reste"| RF["route fastapi<br/>+ policy idle 3 s"]
    RA -->|"≤ 10/min pour cette IP"| API["API FastAPI"]
    RA -->|"> 10/min"| R429["429<br/>l'API n'est jamais appelée"]
    RF --> API

Les deux routes portent le même host. Envoy choisit la plus précise : un chemin exact (/login) l'emporte sur un préfixe (/).

Comment le fichier devient une règle dans Envoy

Trois outils se passent le relais. Aucun ne « lit » le fichier seul.

  1. Kustomize assemble k8s/base/ et l'overlay de l'env. Les deux manifests (httproute-auth.yaml, backendtrafficpolicy-auth-ratelimit.yaml) sont listés dans k8s/base/kustomization.yaml. Un fichier absent de cette liste est ignoré.
  2. ArgoCD applique le résultat dans le namespace de l'env (fastapi-dev, fastapi-staging, fastapi-prod).
  3. Envoy Gateway voit la policy, suit son targetRefs jusqu'à la route fastapi-auth, et traduit le tout en configuration Envoy.

Chaque env a sa propre policy, donc son propre compteur : 10 essais sur dev ne consomment rien sur prod.

Trois pièges rencontrés en le posant

1. Une limite par IP suppose de voir l'IP

Derrière un load balancer, Envoy peut ne voir que l'IP d'un nœud. Tous les clients partageraient alors un seul compteur, et un attaquant bloquerait tout le monde.

Ici, le Service du Gateway est en externalTrafficPolicy: Local, qui préserve l'IP source. Vérifié, pas supposé : le log d'accès d'Envoy montre l'IP publique du poste dans downstream_remote_address.

2. Une policy ne vise pas une règle de route, en v1.4.0

Le plan de départ nommait une règle auth dans la route existante, et la policy visait cette règle (sectionName). Le dry-run serveur l'a refusé : Envoy Gateway v1.4.0 ne le supporte pas encore. D'où la route dédiée fastapi-auth.

Un kubectl apply --dry-run=server interroge la validation réelle de l'API server. Il attrape ce genre de refus avant le merge, là où kubectl kustomize seul ne voit rien.

3. Une nouvelle route n'hérite pas des policies de l'ancienne

fastapi-upstream-idle (le délai d'inactivité de 3 s de #172) vise la route fastapi. Elle ne couvre pas fastapi-auth. Sans recopier ce délai, les 503 UC seraient revenus sur /login, et seulement là.

Le vérifier soi-même

# 30 logins d'affilée : 10 réponses de l'API (403), puis des 429
for i in $(seq 1 30); do
  curl -s -o /dev/null -w '%{http_code} ' -X POST https://api-dev.devopsyouss.com/login \
    -d 'username=nobody@example.com&password=wrong'
done; echo

# Statut côté Envoy Gateway : la policy doit être Accepted
kubectl -n fastapi-dev get btp fastapi-auth-ratelimit \
  -o jsonpath='{.status.ancestors[*].conditions[*].type}={.status.ancestors[*].conditions[*].status}{"\n"}'

Le compteur se recharge au fil de la minute : attendre une minute avant de rejouer.

Ce que ça ne protège pas

Limite Pourquoi Complément
Un attaquant avec beaucoup d'IP chaque IP a ses 10 essais verrou par compte, côté app
Plusieurs clients derrière une IP (NAT) ils partagent un compteur limite plus haute ou autre critère
Envoy à plusieurs replicas un compteur par replica, soit N × 10 diviser la valeur, ou passer en Global

Le verrou par compte a son propre piège : il permet de bloquer le compte d'un autre en ratant exprès son mot de passe. Un délai qui augmente à chaque échec vaut mieux qu'un blocage définitif.