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:
@@ -488,12 +488,23 @@ pub async fn build_app(
|
||||
authz: authz.clone(),
|
||||
};
|
||||
|
||||
// Audit 2026-06-11 H-B1 — login DoS defenses. PICLOUD_LOGIN_ARGON2_PARALLELISM
|
||||
// overrides the per-process Argon2-during-login concurrency cap.
|
||||
let argon2_login_parallelism = std::env::var("PICLOUD_LOGIN_ARGON2_PARALLELISM")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.filter(|n| *n > 0)
|
||||
.unwrap_or(2);
|
||||
let auth_state = AuthState {
|
||||
users: auth.users.clone(),
|
||||
sessions: auth.sessions.clone(),
|
||||
keys: auth.keys.clone(),
|
||||
ttl: auth.ttl,
|
||||
principal_cache: Arc::new(picloud_manager_core::auth_middleware::PrincipalCache::new()),
|
||||
login_rate_limiter: Arc::new(
|
||||
picloud_manager_core::login_rate_limit::LoginRateLimiter::new(),
|
||||
),
|
||||
argon2_login_semaphore: Arc::new(tokio::sync::Semaphore::new(argon2_login_parallelism)),
|
||||
};
|
||||
let admins_state = AdminsState {
|
||||
users: auth.users.clone(),
|
||||
|
||||
Reference in New Issue
Block a user