Skip to content

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 sectionName permettrait de revenir à une seule route. Pas urgent : la route séparée est plus lisible.