← repoman internals

internal/security

import "git.griefed.de/griefed/repoman/internal/security"
package security // import "git.griefed.de/griefed/repoman/internal/security"

Package security provides the fail2ban-friendly security event log.

The log is a separate file from the application logs so an operator can point
fail2ban at it without filtering through general traffic. Events written here
are also mirrored into the audit_log table by the caller; this package only
handles file output.

Format (one line per event, designed to be parsable by fail2ban regexes and by
awk):

    2026-04-28T08:30:42Z [SECURITY] <event> ip=<ip> [k=v]...

Examples:

    2026-04-28T08:30:42Z [SECURITY] login_failed ip=192.0.2.1 user=amy reason=bad_password
    2026-04-28T08:30:43Z [SECURITY] login_failed ip=192.0.2.1 user=amy reason=bad_password
    2026-04-28T08:30:44Z [SECURITY] account_soft_locked ip=192.0.2.1 user=amy duration=15m
    2026-04-28T08:31:12Z [SECURITY] invalid_password_reset ip=192.0.2.1 reason=token_expired token_id=42

The accompanying fail2ban filter (docs/fail2ban/repoman.conf) uses:

    failregex = ^.*\[SECURITY\] (login_failed|account_soft_locked|invalid_password_reset|invalid_reset_token) ip=<HOST>

so any combination of these events from the same IP within the fail2ban findtime
triggers a ban.

CONSTANTS

const (
	EventLoginFailed         = "login_failed"
	EventLoginSucceeded      = "login_succeeded"
	EventAccountSoftLocked   = "account_soft_locked"
	EventAccountHardLocked   = "account_hard_locked"
	EventAccountUnlocked     = "account_unlocked"
	EventInvalidResetToken   = "invalid_reset_token"
	EventInvalidResetRequest = "invalid_password_reset"
	EventResetTokenIssued    = "reset_token_issued"
	EventResetTokenConsumed  = "reset_token_consumed"
	EventEmergencyReset      = "emergency_reset"
	Event2FAFailed           = "two_factor_failed"
	Event2FASucceeded        = "two_factor_succeeded"
	Event2FAReset            = "two_factor_reset"
)
    Standard event names. Use these constants rather than literals so a rename
    surfaces in code review and the fail2ban filter stays in sync.


TYPES

type Event struct {
	Name string            // e.g. "login_failed"
	IP   string            // remote IP; "unknown" if unavailable
	Tags map[string]string // additional structured fields
}
    Event represents a single security event ready for serialisation.

    Event names should match fail2ban-filter regex tokens; keep them snake_case
    and stable. Adding a new event name means updating the shipped fail2ban
    filter to match if the new event should also trigger bans.

type Logger struct {
	// Has unexported fields.
}
    Logger writes security events to an injected destination. Concurrency-safe:
    writes are serialised under a mutex so partial lines never interleave.

    The destination is supplied by the caller (typically a
    logging.RotatingWriter so events land in a date-rotated security file) —
    the security package itself knows nothing about files or rotation, keeping
    that concern in one place. A nil destination makes every Write a no-op, so
    callers can wire the logger unconditionally and disable it by configuration.

func NewLogger(out io.Writer) *Logger
    NewLogger returns a security Logger writing to out. A nil out yields a
    Logger whose Write methods are no-ops — useful when the operator chose not
    to configure a security log. If out implements io.Closer, Close closes it,
    so the caller need not separately close the rotating writer it passed in.

func (l *Logger) Close() error
    Close closes the destination if it is an io.Closer (e.g. the rotating
    writer). Safe to call on a no-op Logger and safe to call more than once.

func (l *Logger) SetStdoutMirror(mirror io.Writer)
    SetStdoutMirror enables a copy of every event to the given writer (typically
    os.Stdout in dev mode). Useful for debugging without having to tail the
    file.

func (l *Logger) Write(event Event)
    Write emits a single event. Errors are logged via slog rather than returned
    because security-log failures must never break the authentication flow —
    better to lose a log line than to lock everyone out because the log disk is
    full.