Sign-in rate limits are counted per worker, in memory, and reset on every restart #59

Closed
opened 2026-09-06 16:05:54 +00:00 by tiagoagueda · 0 comments
Owner

What happens

allauth's rate limits are on, with sensible values:

login_failed: 10/m/ip,5/300s/key    login: 30/m/ip    signup: 20/m/ip
reset_password: 20/m/ip,5/m/key     reauthenticate: 10/m/user

They are counted in Django's cache. Postulo sets no CACHES at all, so Django falls back
to LocMemCache — a dictionary inside one process. The production image runs

gunicorn ... --workers 3

so there are three independent counters, and which one a request lands on is up to the
load balancer. Ten failed attempts a minute becomes about thirty. Every restart, deploy or
worker recycle empties them.

Verified by reading the settings back from a running configuration: the cache is
django.core.cache.backends.locmem.LocMemCache, and the worker count is in
docker/Dockerfile.

Why it matters

This is the control standing between somebody's job search — employment history, home
address, every application they have made — and an unlimited password guessing rate. The
limits are configured correctly and then not enforced as configured, which is worse than
either having them or not, because the numbers written down are not the numbers in force.

Shape

  • A shared cache when one is available. POSTULO_CACHE_URL, so an operator with Redis
    or Memcached points at it and the counters become instance-wide.
  • A database-backed cache as the default, since a self-hosted instance usually has no
    Redis and always has a database. django.core.cache.backends.db.DatabaseCache plus the
    table its createcachetable makes, created by a migration so nobody has to run a
    command.
  • Or one worker, which is the wrong answer for a different reason.
  • Whatever is chosen, say it in Hardening, and add a test that two "processes" sharing a
    cache share a counter.

While in here: the limits themselves deserve a look. login: 30/m/ip is per address, and
several people behind one address share it.

Classification

Bug, security. Not breaking — a cache table is added, nothing changes shape.

## What happens allauth's rate limits are on, with sensible values: ``` login_failed: 10/m/ip,5/300s/key login: 30/m/ip signup: 20/m/ip reset_password: 20/m/ip,5/m/key reauthenticate: 10/m/user ``` They are counted in Django's cache. Postulo sets no `CACHES` at all, so Django falls back to `LocMemCache` — a dictionary inside one process. The production image runs ``` gunicorn ... --workers 3 ``` so there are **three independent counters**, and which one a request lands on is up to the load balancer. Ten failed attempts a minute becomes about thirty. Every restart, deploy or worker recycle empties them. Verified by reading the settings back from a running configuration: the cache is `django.core.cache.backends.locmem.LocMemCache`, and the worker count is in `docker/Dockerfile`. ## Why it matters This is the control standing between somebody's job search — employment history, home address, every application they have made — and an unlimited password guessing rate. The limits are configured correctly and then not enforced as configured, which is worse than either having them or not, because the numbers written down are not the numbers in force. ## Shape - **A shared cache when one is available.** `POSTULO_CACHE_URL`, so an operator with Redis or Memcached points at it and the counters become instance-wide. - **A database-backed cache as the default**, since a self-hosted instance usually has no Redis and always has a database. `django.core.cache.backends.db.DatabaseCache` plus the table its `createcachetable` makes, created by a migration so nobody has to run a command. - **Or one worker**, which is the wrong answer for a different reason. - Whatever is chosen, say it in *Hardening*, and add a test that two "processes" sharing a cache share a counter. While in here: the limits themselves deserve a look. `login: 30/m/ip` is per address, and several people behind one address share it. ## Classification Bug, security. Not breaking — a cache table is added, nothing changes shape.
tiagoagueda added this to the 0.2.0 milestone 2026-09-06 16:05:54 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#59
No description provided.