From a0987c940fdc725b53d748c7f3fa92a2915e87ef Mon Sep 17 00:00:00 2001 From: Mitchell-Johnson Date: Sun, 27 Sep 2026 18:04:23 -0700 Subject: [PATCH] Twenty login tries before the backoff starts, not five A guest password is typed by hand off a message on a phone. Five tries is not many when a hyphen lands in the wrong place, and being told to wait is worse than the brute-force risk here: past twenty the doubling lockout still applies, the global backstop is unchanged, and the password is ~78 bits. Co-Authored-By: Claude Opus 5 --- internal/auth/auth.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 92eb22e..5bc0a88 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -318,11 +318,10 @@ func (s *Sessions) RevokeOthers(user, keep string) int { // RevokeUser ends every session of user (the account is going away). func (s *Sessions) RevokeUser(user string) { s.RevokeOthers(user, "") } -// Limiter is a single global backoff: after 5 failures, each further failure -// doubles a lockout starting at 30s, capped at 10 minutes. Single-user box. // Limiter locks out a client after repeated failed logins, per client address, with a // global backstop so a botnet can't burn CPU on argon2 indefinitely. A lockout for one -// address never blocks another. +// address never blocks another. Past perClientLock failures, each further failure doubles a +// lockout starting at 30s, capped at 10 minutes. type Limiter struct { mu sync.Mutex clients map[string]*bucket @@ -338,7 +337,11 @@ type bucket struct { var ErrLocked = errors.New("too many failed logins") const ( - perClientLock = 5 // failures before a client is locked + // perClientLock is how many failures one address gets before it is made to wait. Set high + // on purpose: a guest login is a generated password read off a message and typed by hand + // on a phone, and being locked out for fumbling it is worse than the brute-force risk — + // which the doubling backoff past this point, and the global backstop, still cover. + perClientLock = 20 globalLock = 100 // failures (all clients) in the window before everyone waits a minute globalWindow = 5 * time.Minute )