BreakGlass.RateLimiter (BreakGlassEx v0.1.0)

Copy Markdown View Source

GenServer that owns the :break_glass_rate_limit ETS table and serialises all write operations to prevent TOCTOU races.

Tracks failed authentication attempts per IP address. When the attempt count reaches :max_attempts, the IP is locked out for :lockout_seconds.

Configuration

  • :max_attempts — number of failures before lockout (default: 5)
  • :lockout_seconds — lockout window duration in seconds (default: 900)

ETS Table

Table name: :break_glass_rate_limit

Record format: {ip :: String.t(), attempt_count :: non_neg_integer(), locked_at :: integer() | nil}

  • locked_at is nil until the attempt count reaches :max_attempts.
  • Once set, locked_at is never reset by subsequent failures — the lockout clock does not slide.
  • reset_attempts/1 deletes the row entirely, removing both the count and lockout.

Hot-path reads (locked_out?/1, remaining_attempts/1) query ETS directly without a GenServer round-trip.

Summary

Functions

Returns a specification to start this module under a supervisor.

Returns true if ip is currently locked out (lockout window still active), false otherwise.

Records a failed authentication attempt for ip.

Returns the number of remaining authentication attempts for ip.

Resets the failed attempt counter and any lockout for ip by deleting the ETS row entirely.

Starts the RateLimiter GenServer and registers it under Elixir.BreakGlass.RateLimiter.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

locked_out?(ip)

@spec locked_out?(ip :: String.t()) :: boolean()

Returns true if ip is currently locked out (lockout window still active), false otherwise.

Reads ETS directly — no GenServer round-trip.

record_failed_attempt(ip)

@spec record_failed_attempt(ip :: String.t()) :: :ok

Records a failed authentication attempt for ip.

If the resulting attempt count reaches :max_attempts, a lockout timestamp is stored and a Logger.warning is emitted with the IP and count.

Returns :ok.

remaining_attempts(ip)

@spec remaining_attempts(ip :: String.t()) :: non_neg_integer()

Returns the number of remaining authentication attempts for ip.

Reads ETS directly — no GenServer round-trip.

Returns the configured :max_attempts (default 5) when no failures have been recorded for ip.

reset_attempts(ip)

@spec reset_attempts(ip :: String.t()) :: :ok

Resets the failed attempt counter and any lockout for ip by deleting the ETS row entirely.

Returns :ok.

start_link(opts \\ [])

@spec start_link(keyword()) :: GenServer.on_start()

Starts the RateLimiter GenServer and registers it under Elixir.BreakGlass.RateLimiter.