Lock Out Brute-Force Logins With django-axes

Protect Django logins from password guessing with django-axes: install the backend and middleware, choose lockout parameters by user and IP, get real client IPs behind proxies, store attempts in cache or database, and unlock users safely.

Problem statement

Django's login views do not limit failed attempts. Attackers can try thousands of passwords against the admin or user accounts, slowly from many IPs (credential stuffing with leaked passwords) or quickly from one. django-axes records failed login attempts and locks out further attempts after a threshold, by username, IP address, or both, with configurable cool-off periods. It integrates with Django's authentication system, so it covers the admin, LoginView, and most authentication packages.

How django-axes worksLogin attemptadmin or site formAxes backendchecks lockout firstFailure recordeduser + IPThreshold hitlocked for cool-offFailures are counted per user and IP; further tries are refused until the cool-off ends.
  1. pip install django-axes, add axes to INSTALLED_APPS, axes.middleware.AxesMiddleware at the end of MIDDLEWARE, and axes.backends.AxesStandaloneBackend first in AUTHENTICATION_BACKENDS.
  2. Run python manage.py migrate.
  3. Set AXES_FAILURE_LIMIT = 5, AXES_COOLOFF_TIME = timedelta(minutes=30), and AXES_LOCKOUT_PARAMETERS = [["username", "ip_address"]].
  4. Behind a proxy, configure the client IP source so all users do not share the proxy's IP.
  5. Reset after successful login (AXES_RESET_ON_SUCCESS = True) and provide a support unlock path (axes_reset command or admin).
  6. Combine with 2FA and edge rate limits for defence in depth.

Step-by-step solution

1. Install and configure

INSTALLED_APPS += ["axes"]

AUTHENTICATION_BACKENDS = [
    "axes.backends.AxesStandaloneBackend",
    "django.contrib.auth.backends.ModelBackend",
]

MIDDLEWARE += ["axes.middleware.AxesMiddleware"]   # last

from datetime import timedelta
AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = timedelta(minutes=30)
AXES_LOCKOUT_PARAMETERS = [["username", "ip_address"]]
AXES_RESET_ON_SUCCESS = True
AXES_LOCKOUT_TEMPLATE = "accounts/locked_out.html"
python manage.py migrate
python manage.py check

The backend must come first so it can refuse authentication for locked-out combinations before checking passwords.

2. Choose lockout parameters

  • ["username", "ip_address"] (combined) locks a username only from the offending IP; attackers cannot lock real users out from elsewhere, but distributed attacks on one account continue from new IPs.
  • "username" alone stops distributed guessing on one account but lets attackers lock out any user deliberately (denial of service).
  • "ip_address" alone stops one IP from trying many accounts but punishes shared IPs.

A common balance is two rules: [["username", "ip_address"], "ip_address"] with a higher limit for IP-only, plus 2FA so a guessed password alone is not enough.

what to lock onStopsRiskusername + IPone IP guessing one account!distributed attacks continueusername only✓distributed attacks on an account✕attackers can lock users outIP onlyone IP trying many accounts!shared IPs punishedCombine a user-plus-IP rule with an IP rule, and add 2FAdjango-deployment.com
Two-factor authentication makes the remaining gaps far less dangerous.

3. Get the real client IP

Behind Nginx, a load balancer, or Cloudflare, REMOTE_ADDR is the proxy's address, and every user would share one lockout counter. Axes 7+ uses AXES_CLIENT_IP_CALLABLE or the django-ipware integration; configure it to read the header your proxy sets, and trust only your proxies:

AXES_IPWARE_PROXY_COUNT = 1
AXES_IPWARE_META_PRECEDENCE_ORDER = ["HTTP_X_FORWARDED_FOR", "REMOTE_ADDR"]

Behind Cloudflare, prefer HTTP_CF_CONNECTING_IP. Check the settings names for your axes version, as they changed between major releases. Then test: two different clients must produce different IPs in the AccessAttempt records.

4. Storage and performance

The default handler stores attempts in the database (AccessAttempt, AccessFailureLog). Under heavy attack, the cache handler (AXES_HANDLER = "axes.handlers.cache.AxesCacheHandler") with Redis avoids database writes for every failure. Prune old records periodically.

5. Unlocking and user experience

Show a lockout page that explains the cool-off and offers password reset. Support staff can unlock with:

python manage.py axes_reset_username alice
python manage.py axes_reset_ip 203.0.113.7

or through the admin's Access Attempts screen.

brute-force protection that worksAxes backend first in AUTH_BACKENDSmiddleware lastReal client IP from trusted proxynot the proxy addressBalanced lockout parametersuser+IP and IP rulesCache handler under heavy attackfewer DB writesLockout page and unlock proceduresupport can help usersThe most common mistake is locking everyone behind the proxy IPdjango-deployment.com
Test from two networks to confirm lockouts are per client, not global.

Verification check: five wrong passwords for a test user lock that user from your IP (lockout page shown), the same user can still log in from another network, the AccessAttempt record shows your real IP, and axes_reset_username restores access.

Explanation

django-axes plugs into Django's authentication pipeline: its backend checks whether the current username and IP combination is locked before other backends verify credentials, and its signal handlers record failures and successes. Lockout state is computed from recorded failures within the cool-off window. Because authentication in Django goes through authenticate(), the admin, built-in login views, and most third-party auth flows are covered without per-view changes. The quality of the protection depends on correct client IPs and sensible lockout parameters.

Edge cases / notes

  • APIs with token or JWT login endpoints must call authenticate() with request for axes to see them; check your auth package.
  • django-allauth works with axes; make sure the axes backend precedes allauth's backend.
  • fail2ban can complement axes by banning IPs at the firewall based on axes or Nginx logs.
  • Password reset endpoints need their own rate limits; axes covers login attempts.

Next steps

FAQ

What does django-axes do?

It records failed logins and locks out further attempts after a threshold, by username, IP, or both.

Why does axes lock out all my users at once?

It sees the proxy's IP for everyone. Configure it to read the real client IP from trusted proxy headers.

Can attackers lock out real users with axes?

If you lock by username only, yes. Combine username with IP, or use challenges and 2FA.

How do I unlock a user?

Run python manage.py axes_reset_username <name> or remove their attempts in the admin.

Learning path · step 8 of 15

Hardening a Django deployment

Keep going