Skip to content

Add optional failed-login log for fail2ban et al.#757

Merged
ajslater merged 2 commits into
developfrom
claude/serene-lovelace-a44eac
May 10, 2026
Merged

Add optional failed-login log for fail2ban et al.#757
ajslater merged 2 commits into
developfrom
claude/serene-lovelace-a44eac

Conversation

@ajslater
Copy link
Copy Markdown
Owner

Summary

  • Adds an optional, default-off feature that appends one parseable line per failed credential attempt to a dedicated log file at <log_dir>/failed_logins.log.
  • Configurable via three new keys under [auth] in codex.toml (or matching CODEX_AUTH_* env vars). Disabled deployments pay zero overhead — no extra middleware, no extra sink, no extra signal receiver.
  • Hooks django.contrib.auth.signals.user_login_failed, which fires for both rest_registration's form login and DRF's BasicAuthentication (OPDS), so a single receiver covers both attack vectors.

Why

Codex's OPDS endpoints accept HTTP Basic Auth and /api/v3/auth/login/ accepts username/password — both internet-facing and attractive brute-force targets when a Codex instance is exposed publicly. Operators had no way to feed those failures into a banning tool. This adds a consumer-agnostic line format that fail2ban, CrowdSec, sshguard, or any tail-and-regex tool can use.

How it works

  • Single sink: codex/startup/loguru.py registers a dedicated loguru sink filtered by logger.bind(failed_login=True). The tagged record also passes the default stdout/codex.log filters, so admins still see auth failures in the main log; no format leak because the main format string doesn't render extra.
  • Signal receiver: lives in codex/failed_login_log.py, wired conditionally inside the existing connect_signals().
  • Request-context middleware: rest_registration's default authenticate_by_login_data calls auth.authenticate(...) without passing request=, so the signal would otherwise arrive with request=None and we'd log - for the IP. A small RequestContextMiddleware stashes the active request in a contextvars.ContextVar (works for both sync and ASGI/Granian) and the receiver falls back to it. DRF's BasicAuthentication already propagates request= so OPDS goes through the direct path.
  • XFF trust: defaults to true (matching the project's existing USE_X_FORWARDED_HOST = True posture) but is toggleable via auth.failed_login_log_trust_forwarded_for so direct-exposed deployments can opt out and avoid header-spoofed log poisoning.

Log line format

2026-05-10 12:34:56 | Failed login from 192.168.1.42 user=alice

Example fail2ban failregex:

^\s*\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \| Failed login from <HOST> user=.*$

Files

  • codex/failed_login_log.py (new) — receiver, IP helper, sink filter, middleware
  • tests/test_failed_login_log.py (new) — 9 tests covering IP extraction, signal receiver capture, contextvar fallback, middleware
  • codex/settings/codex.toml.default — three new keys with worked fail2ban regex example
  • codex/settings/config.py, codex/settings/__init__.py — env mappings, typed settings, conditional middleware registration
  • codex/startup/loguru.py — dedicated sink (conditional)
  • codex/signals/django_signals.py — signal wired in existing connect_signals()

Test plan

  • make fix && make lint && make ty clean
  • pytest tests/test_failed_login_log.py -v — all 9 new tests pass (~85% coverage of the new module)
  • make test-python — 227 passed, suite unaffected (feature is default-off)
  • Manual smoke test: set CODEX_AUTH_FAILED_LOGIN_LOG=1, hit /api/v3/auth/login/ with bad creds, hit an OPDS endpoint with bad Basic auth, confirm config/logs/failed_logins.log matches the regex.

🤖 Generated with Claude Code

ajslater and others added 2 commits May 10, 2026 15:40
Adds a default-off feature that appends one parseable line per failed
credential attempt to a dedicated file, so banning tools (fail2ban,
CrowdSec, sshguard) can tail it via regex. Gated end-to-end on
`auth.failed_login_log` so disabled deployments pay zero overhead.

Hooks `django.contrib.auth.signals.user_login_failed`, which fires for
both rest_registration's form login and DRF's BasicAuthentication
(OPDS), covering both vectors with one receiver. rest_registration's
default authenticator drops `request=`, so a small contextvar
middleware stashes the active request to recover the client IP for
form logins. XFF trust is configurable for direct-exposed deployments
that can't trust the header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add env-var references in the Authentication subsection plus a new
"Failed-Login Log" section covering line format, XFF trust trade-off,
and a worked fail2ban filter + jail example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ajslater ajslater merged commit f0cc758 into develop May 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant