ADR 039 — Rate limit par IP sur le login et l'inscription, posé dans Envoy Gateway (2026-09-24)¶
Statut¶
Accepté, écrit le 2026-09-24. Issue #129, Sprint 8 (Tests & Signaux).
Cette ADR lève le risque que l'ADR 028 avait accepté pour la démo (« écriture publique assumée », mitigation renvoyée à #129).
Contexte¶
POST /login et POST /users/ (inscription) sont publics : ils doivent l'être, c'est
par eux qu'on entre. Jusqu'ici, rien ne limitait leur débit.
Mesure du 2026-09-18 : 30 logins d'affilée sur le même compte, 30 réponses 403,
aucun blocage ni ralentissement. Combiné à la fuite d'emails (#229, corrigée) et à
l'énumération à l'inscription (#232, ouverte), le brute force n'avait aucun frein.
Décision¶
1. Une limite par IP source, sur ces deux routes seulement¶
10 requêtes par minute et par IP, sur POST /login et POST /users/. Au-delà,
Envoy répond 429 Too Many Requests sans joindre l'API.
Les autres routes ne sont pas limitées. Un tir de charge (runbook §2) part d'une seule IP : une limite globale le casserait, et elle ne protégerait rien de plus.
10 par minute laisse de la marge à un humain qui se trompe, et au gate e2e-staging,
qui fait quelques inscriptions et logins par passage.
2. Dans Envoy Gateway, pas dans l'application¶
Aucune ligne de code applicatif. La limite est une BackendTrafficPolicy, ressource
native d'Envoy Gateway, livrée par ArgoCD comme le reste. Elle protège l'API avant
même qu'une requête l'atteigne.
3. Mode Local, pas Global¶
Local compte dans Envoy lui-même. Global exige un Redis en plus, une pièce de plus
à monter, surveiller et sécuriser à chaque montage du cluster.
Contrepartie : le compteur est tenu par replica Envoy. À 1 replica (aujourd'hui), la limite est exacte. À N replicas, elle devient N × 10.
4. Une route dédiée, fastapi-auth¶
Envoy Gateway v1.4.0 refuse sectionName sur une BackendTrafficPolicy (dry-run
serveur : « this policy does not yet support the sectionName field »). Une policy ne
peut donc viser qu'une route entière, pas une règle de route.
D'où une HTTPRoute séparée, qui ne porte que les deux routes à protéger. Elle partage
le host et le listener de la route fastapi. Un match Exact l'emporte sur le
PathPrefix /, donc seules ces deux requêtes passent par elle.
5. Le délai d'inactivité de 3 s est recopié¶
fastapi-upstream-idle (#172) vise la route fastapi, pas fastapi-auth. La nouvelle
policy porte donc aussi connectionIdleTimeout: 3s. Sans lui, les 503 UC de #172
reviendraient sur /login. Les deux valeurs se lisent ensemble.
Pourquoi la limite par IP fonctionne ici¶
Une limite par IP n'a de sens que si Envoy voit l'IP du client. Derrière un load balancer, ce n'est pas acquis : il peut remplacer l'IP d'origine par celle d'un nœud, et tous les clients partageraient alors un seul compteur.
Vérifié le 2026-09-24 dans le log d'accès d'Envoy : downstream_remote_address porte
l'IP publique du poste. Le Service du Gateway tourne en externalTrafficPolicy: Local,
qui préserve l'IP source derrière le NLB.
Validation¶
Prouvé d'abord sur dev seul (!433), avant d'étendre aux trois environnements :
| Test | Résultat |
|---|---|
| 30 logins d'affilée sur dev | 10 × 403 (réponse de l'API), puis 20 × 429 |
15 × GET /posts/public |
15 × 200, non limité |
| 12 logins sur prod, non encore couverte | 12 × 403, aucun 429 |
| Statut route et policy | Accepted=True |
Conséquences¶
Ce que ça ferme¶
Le brute force depuis une machine : 14 400 essais par jour au plus, contre un nombre illimité avant.
Ce que ça ne ferme pas¶
- Un attaquant avec beaucoup d'IP (botnet) n'est pas freiné : 1 000 IP font 1 000 × 10 essais par minute. Le complément est un verrou par compte, côté application. Cadré en commentaire de #129, à ouvrir en issue séparée.
- Plusieurs utilisateurs derrière une même IP (NAT d'entreprise) partagent un compteur. Sans effet sur ce projet de démo, à revoir sur une vraie base d'utilisateurs.
- L'énumération d'emails (#232) reste possible, dans la limite de 10 par minute.
À surveiller¶
- Passer Envoy à plusieurs replicas multiplie la limite. Le jour où ça arrive,
diviser la valeur ou passer en
Global. - Une version d'Envoy Gateway qui accepte
sectionNamepermettrait de revenir à une seule route. Pas urgent : la route séparée est plus lisible.