Malachi. Auth. LockoutManager
(malachi v0.22.7)
View Source
Account-lockout facade over the replicated lockout store (Malachi.Auth.LockoutServer, a dedicated
ra cluster). Applies progressive lockout after repeated failed logins, keyed by {username, ip}.
Replaces the old node-local ETS store: lockouts now replicate across the cluster, so brute-force
protection is cluster-wide (an attacker cannot spread attempts across nodes to dodge the limit) and
survives a restart (a restart cannot reset a lockout). The pure lockout logic lives in
Malachi.Auth.LockoutRegistry; this module reads the lockout config, routes reads to the local replica
and writes through the Raft log, and performs the observable side effects (metrics, logging, audit) for a
newly applied or cleared lockout.
Progressive Lockout
- 1st lockout (5 failures): 5 minutes
- 2nd lockout (10 failures): 15 minutes
- 3rd lockout (15 failures): 45 minutes
- 4th lockout (20 failures): 2 hours
- 5th+ lockout (25+ failures): 6 hours (maximum)
Process role and blocking
Reads (locked?/get_failed_attempts/list_locked_accounts) are direct local queries, no consensus,
no GenServer round-trip. The hot-path writes (record_failed_attempt/record_successful_auth) run in
a background task on Malachi.TaskSupervisor, so the auth path never blocks on a consensus round-trip
or an ra leader election, and no single process funnels every write; ra serializes the concurrent writes
itself, and the rate/connection limiters bound the task volume. unlock_account is a synchronous admin
action (the operator wants the result). The GenServer owns only the periodic cleanup timer.
Malachi.Application forms the ra lockout cluster before this process starts.
Summary
Functions
Returns a specification to start this module under a supervisor.
Returns the number of failed attempts for a username + IP.
Lists all currently locked accounts as info maps (username, ip, locked_until, ...).
Checks if an account is locked.
Records a failed authentication attempt, applying a lockout once the limit is reached.
Records a successful authentication, clearing failed attempts and any lockout for the username + IP.
Starts the lockout manager, which owns the periodic cleanup timer for the replicated store.
Unlocks an account manually (administrative action).
Types
@type ip() :: :inet.ip_address() | String.t()
An IP as the acceptor supplies it (tuple) or already formatted (string).
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec get_failed_attempts(String.t(), ip()) :: non_neg_integer()
Returns the number of failed attempts for a username + IP.
@spec list_locked_accounts() :: [map()]
Lists all currently locked accounts as info maps (username, ip, locked_until, ...).
@spec locked?(String.t(), ip()) :: :not_locked | {:locked, non_neg_integer()}
Checks if an account is locked.
Returns :not_locked, or {:locked, time_remaining_ms} when the account is locked. A direct read of the
local replica (no consensus round-trip).
Records a failed authentication attempt, applying a lockout once the limit is reached.
Non-blocking: the consensus write runs in a background task (see the module doc), so the auth path is
never coupled to ra latency. Returns :ok immediately.
Records a successful authentication, clearing failed attempts and any lockout for the username + IP.
Non-blocking (background task). Skips the write entirely on the common case (a clean login with no prior
failures to reset). Returns :ok immediately.
@spec start_link(keyword()) :: GenServer.on_start()
Starts the lockout manager, which owns the periodic cleanup timer for the replicated store.
@spec unlock_account(String.t(), ip() | :all) :: :ok | {:ok, non_neg_integer()} | {:error, term()}
Unlocks an account manually (administrative action).
ip is a specific IP or :all to unlock every IP for the user. Synchronous, returns :ok (single IP)
or {:ok, cleared_count} (:all) once the change is committed, or {:error, reason} if the store is
unreachable.