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 )