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.
- Kustomize assemble
k8s/base/et l'overlay de l'env. Les deux manifests (httproute-auth.yaml,backendtrafficpolicy-auth-ratelimit.yaml) sont listés dansk8s/base/kustomization.yaml. Un fichier absent de cette liste est ignoré. - ArgoCD applique le résultat dans le namespace de l'env (
fastapi-dev,fastapi-staging,fastapi-prod). - Envoy Gateway voit la policy, suit son
targetRefsjusqu'à la routefastapi-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.