Add optional failed-login log for fail2ban et al.#757
Merged
Conversation
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>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
<log_dir>/failed_logins.log.[auth]incodex.toml(or matchingCODEX_AUTH_*env vars). Disabled deployments pay zero overhead — no extra middleware, no extra sink, no extra signal receiver.django.contrib.auth.signals.user_login_failed, which fires for both rest_registration's form login and DRF'sBasicAuthentication(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
codex/startup/loguru.pyregisters a dedicated loguru sink filtered bylogger.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 renderextra.codex/failed_login_log.py, wired conditionally inside the existingconnect_signals().rest_registration's defaultauthenticate_by_login_datacallsauth.authenticate(...)without passingrequest=, so the signal would otherwise arrive withrequest=Noneand we'd log-for the IP. A smallRequestContextMiddlewarestashes the active request in acontextvars.ContextVar(works for both sync and ASGI/Granian) and the receiver falls back to it. DRF'sBasicAuthenticationalready propagatesrequest=so OPDS goes through the direct path.USE_X_FORWARDED_HOST = Trueposture) but is toggleable viaauth.failed_login_log_trust_forwarded_forso direct-exposed deployments can opt out and avoid header-spoofed log poisoning.Log line format
Example fail2ban
failregex:Files
codex/failed_login_log.py(new) — receiver, IP helper, sink filter, middlewaretests/test_failed_login_log.py(new) — 9 tests covering IP extraction, signal receiver capture, contextvar fallback, middlewarecodex/settings/codex.toml.default— three new keys with worked fail2ban regex examplecodex/settings/config.py,codex/settings/__init__.py— env mappings, typed settings, conditional middleware registrationcodex/startup/loguru.py— dedicated sink (conditional)codex/signals/django_signals.py— signal wired in existingconnect_signals()Test plan
make fix && make lint && make tycleanpytest 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)CODEX_AUTH_FAILED_LOGIN_LOG=1, hit/api/v3/auth/login/with bad creds, hit an OPDS endpoint with bad Basic auth, confirmconfig/logs/failed_logins.logmatches the regex.🤖 Generated with Claude Code