fix(audit-2026-06-11/H-B1): login rate limit + Argon2 concurrency cap

The login handler had no admission control before
`spawn_blocking(verify_password)`. On Pi-class hardware Argon2id is ~50-
100 ms per attempt, so a few dozen concurrent anonymous POSTs to
/auth/login park every blocking worker, wedging the entire admin API
and any other path that uses `spawn_blocking`.

Adds two cheap, in-process guards modeled on EmailRateLimiter:

* `LoginRateLimiter` — two sliding-window token buckets, one per
  `(remote_ip, username)` (burst 5/60s) and one per `username`
  (burst 10/15min). Per-(ip, user) defeats a single attacker pounding
  one account; per-user defeats credential-stuffing distributed across
  many IPs. Username is case-folded so case variants share a bucket.
  Maps GC lazily when they cross a soft size cap.

* Per-process `tokio::sync::Semaphore` — caps concurrent Argon2 verifies
  during login. Default 2 permits (Pi-class), overridable via
  `PICLOUD_LOGIN_ARGON2_PARALLELISM`. Acquired AFTER the bucket check so
  attackers can't queue.

Limit check fires before the DB credentials lookup, so even an unknown
username doesn't cost a query. Denied attempts return 429 + Retry-After.
Real client IP comes from the first X-Forwarded-For entry (Caddy is the
trusted single hop); falls back to "unknown" so the per-user bucket
still gates.

4 unit tests cover bucket burst exhaustion, per-user crossing IPs, case
folding, and per-user independence.

Audit ref: security_audit/08_dos_resource.md (H-2 / H-B1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-11 20:42:51 +02:00
parent bd64a25c97
commit 07ffc0b568
5 changed files with 354 additions and 2 deletions

View File

@@ -115,6 +115,13 @@ pub struct AuthState {
/// at startup; cloned cheaply into every router state. `None` for
/// tests / harnesses that don't wire it.
pub principal_cache: Arc<PrincipalCache>,
/// Audit 2026-06-11 H-B1 — per-`(remote_ip, username)` + per-`username`
/// burst limiter for `/auth/login`. Cheap to clone (Arc).
pub login_rate_limiter: Arc<crate::login_rate_limit::LoginRateLimiter>,
/// Audit 2026-06-11 H-B1 — caps concurrent Argon2 verifies during
/// login so a credential-stuffing flurry can't park every blocking
/// worker. Sized via `PICLOUD_LOGIN_ARGON2_PARALLELISM` (default 2).
pub argon2_login_semaphore: Arc<tokio::sync::Semaphore>,
}
/// Legacy request-extension alias retained so the (only remaining)