Documentation
¶
Overview ¶
Package lockout implements per-(email, ip) login throttling for the airlock auth path. The mechanism is intentionally application-only — per-IP rate limiting is the operator's reverse-proxy job. See airlock/AGENTS.md and the project README for the threat-model rationale.
Index ¶
- Constants
- Variables
- func NormalizeIP(remoteAddr string) string
- type Policy
- func (p Policy) Check(ctx context.Context, pool *pgxpool.Pool, email, ip string) (Status, error)
- func (p Policy) ClearOnSuccess(ctx context.Context, pool *pgxpool.Pool, email, ip string) error
- func (p Policy) CooldownFor(tier int) time.Duration
- func (p Policy) PadResponse(start time.Time)
- func (p Policy) RecordFailure(ctx context.Context, pool *pgxpool.Pool, email, ip string) error
- type Status
Constants ¶
const UnknownIP = "unknown"
UnknownIP is the bucket assigned to requests whose RemoteAddr cannot be parsed. Aligns with the "fail loud" rule in airlock/AGENTS.md: never silently skip the lockout — pool the unparseable requests instead.
Variables ¶
Functions ¶
func NormalizeIP ¶
NormalizeIP turns an http.Request RemoteAddr into a stable bucket key. IPv4 round-trips to canonical form; IPv6 collapses to its /64 prefix because a single attacker controls their entire /64 trivially.
Types ¶
type Policy ¶
type Policy struct {
WindowMinutes int
Threshold int
TierDelays []time.Duration
PadDuration time.Duration
}
func (Policy) ClearOnSuccess ¶
ClearOnSuccess wipes failure history + lockout state for a successful login. Best-effort: the caller logs errors but does not block the successful login response on this.
func (Policy) PadResponse ¶
PadResponse sleeps so total elapsed time since `start` reaches p.PadDuration. No-op if already past. Used on the auth-failure paths to collapse the timing channel between unknown-email (fast), wrong-password (bcrypt-slow), and lockout (fast) responses.
func (Policy) RecordFailure ¶
RecordFailure logs an auth failure for (email, ip) and applies an escalating lockout when the rolling-window threshold is reached. The whole thing runs in one transaction so concurrent triggers agree on the next tier.