lockout

package
v1.0.126 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package lockout provides the login account-lockout limiter and the admin-API rate limiter. It is a self-contained leaf (stdlib only, no Culvert coupling) extracted from the flat package main per ADR-0002.

Index

Constants

View Source
const (
	// MaxAttempts is the number of failures from one IP against one username
	// that triggers a pair lock (tier 1).
	MaxAttempts = 5
	// AccountMaxAttempts is the number of failures against one username
	// across ALL IPs that triggers the account-wide lock (tier 2).
	AccountMaxAttempts = 20
	// Window is the span within which failures accumulate toward a lock.
	Window = 10 * time.Minute
	// Duration is how long a pair/account stays locked once tripped.
	Duration = 15 * time.Minute
	// TrustTTL is how long a successful login keeps its client IP exempt
	// from the tier-2 account lock for that username.
	TrustTTL = 30 * 24 * time.Hour
)
View Source
const (
	// Burst is the max API mutations allowed per window.
	Burst = 60
	// RateWindow is the sliding window width for the API rate limiter.
	RateWindow = 1 * time.Minute
)

Variables

This section is empty.

Functions

func Msg

func Msg(seconds int) string

Msg returns a human-readable lockout error. Exposed in package main as LockoutMsg via the shim alias.

Types

type APIRateLimiter

type APIRateLimiter struct {
	// contains filtered or unexported fields
}

APIRateLimiter limits mutating admin API calls per client IP.

func NewAPIRateLimiter

func NewAPIRateLimiter() *APIRateLimiter

NewAPIRateLimiter returns a ready-to-use APIRateLimiter.

func (*APIRateLimiter) Allow

func (a *APIRateLimiter) Allow(ip string) bool

Allow returns true if the IP is within the rate limit for API mutations.

func (*APIRateLimiter) Cleanup

func (a *APIRateLimiter) Cleanup()

Cleanup removes expired entries.

type LockedEntry added in v1.0.63

type LockedEntry struct {
	Tier             string `json:"tier"`
	Username         string `json:"username"`
	IP               string `json:"ip,omitempty"`
	SecondsRemaining int    `json:"seconds_remaining"`
}

LockedEntry describes one currently-active lockout, for admin visibility. Tier is "account" (tier-2, blocks every IP for Username) or "pair" (tier-1, blocks only IP against Username). IP is empty for account-tier entries.

type LoginLimiter

type LoginLimiter struct {
	// contains filtered or unexported fields
}

LoginLimiter tracks failed login attempts per (client IP, username) pair (tier 1) and per username across IPs (tier 2), with a trusted-IP bypass on tier 2 for IPs that previously logged in successfully.

func NewLoginLimiter

func NewLoginLimiter() *LoginLimiter

NewLoginLimiter returns a ready-to-use LoginLimiter.

func (*LoginLimiter) AttemptsLeft

func (l *LoginLimiter) AttemptsLeft(ip, username string) int

AttemptsLeft returns how many more failures the (ip, username) pair is allowed before the tier-1 lock trips.

func (*LoginLimiter) Check

func (l *LoginLimiter) Check(ip, username string) (locked bool, secondsRemaining int)

Check returns (locked bool, secondsRemaining int) for a login attempt by username from client ip. A locked attempt must not be verified further. The pair lock always applies; the account lock is bypassed for IPs with a live trust grant. Expired entries are lazily deleted.

func (*LoginLimiter) CheckPair added in v1.0.32

func (l *LoginLimiter) CheckPair(ip, username string) (locked bool, secondsRemaining int)

CheckPair reports the tier-1 (IP, username) pair lock ONLY, ignoring the account tier entirely. It is for callers that want pure per-IP rate limiting with no cross-IP aggregation — notably the pre-provisioning setup endpoint, where there is no account to protect yet and an account-wide counter would let a few IPs globally block bootstrap (lockout-as-DoS).

func (*LoginLimiter) Cleanup added in v1.0.22

func (l *LoginLimiter) Cleanup()

Cleanup removes entries that can no longer affect a decision, bounding the maps against an unbounded-memory DoS: the pair and account keys derive from the attacker-controlled username (and client IP) on the unauthenticated login POST, so without a sweep each failed attempt could leak a permanent entry. An entry is removable when its lock has expired (a future Check would delete it anyway) or when it is unlocked and its failure window has elapsed (a future RecordFailure would reset it to a fresh window). Trusted grants past TrustTTL are dropped too (they no longer bypass anything). Called periodically by the shared cleanup janitor. Removing a stale entry is behaviorally identical to the lazy reset the hot paths already perform, so it changes no decision.

func (*LoginLimiter) RecordFailure

func (l *LoginLimiter) RecordFailure(ip, username string) bool

RecordFailure registers one failed attempt from ip against username in both tiers. Returns true when this attempt JUST tripped either lock (so the caller can log the lockout event).

func (*LoginLimiter) RecordPairFailure added in v1.0.32

func (l *LoginLimiter) RecordPairFailure(ip, username string) bool

RecordPairFailure registers one failed attempt against the tier-1 (IP, username) pair ONLY, leaving the account tier untouched. Pair-only companion to CheckPair. Returns true when this attempt JUST tripped the pair lock.

func (*LoginLimiter) RecordSuccess

func (l *LoginLimiter) RecordSuccess(ip, username string)

RecordSuccess clears the pair's failure history and marks ip as trusted for username (TrustTTL, bounded at trustMaxIPs with oldest-first eviction). The tier-2 account counter is deliberately NOT cleared: a victim's successful login must not erase the evidence of a concurrent distributed attack, and the victim doesn't need it cleared — their IP is now trusted.

func (*LoginLimiter) ResetUser added in v1.0.32

func (l *LoginLimiter) ResetUser(username string)

ResetUser removes ALL failure/lock state for username — the tier-2 account entry and every (ip, username) pair — without touching the trusted-IP set (trust is an allowance earned by a successful login, not lock state). This is the explicit unlock primitive: test isolation helpers use it where the old single-tier code abused RecordSuccess as a full reset, and it is the natural hook for a future admin "unlock account" API.

func (*LoginLimiter) Snapshot added in v1.0.63

func (l *LoginLimiter) Snapshot() []LockedEntry

Snapshot returns every currently-active lockout across both tiers, sorted by username then IP for stable display. It is the read side of the explicit unlock primitive (ResetUser) — without it, an operator has no way to see who is locked out short of reading logs or waiting/restarting. It does not mutate limiter state; expired entries are simply omitted.

func (*LoginLimiter) SnapshotAndClear

func (l *LoginLimiter) SnapshotAndClear() func()

SnapshotAndClear captures the current limiter state, replaces it with an empty state, and returns a closure that restores the captured state. It is the exported equivalent of the whitebox snapshot/restore idiom used for test isolation of the package-global limiter: the maps are deep-copied under the mutex and the restore runs under the mutex too, so neither tears against a concurrent reader/writer. Production code never calls this; it exists so package main's test isolation helper does not need access to the unexported maps across the package boundary (ADR-0002 extraction).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL