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.