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.
pip install django-axes, addaxestoINSTALLED_APPS,axes.middleware.AxesMiddlewareat the end ofMIDDLEWARE, andaxes.backends.AxesStandaloneBackendfirst inAUTHENTICATION_BACKENDS.- Run
python manage.py migrate. - Set
AXES_FAILURE_LIMIT = 5,AXES_COOLOFF_TIME = timedelta(minutes=30), andAXES_LOCKOUT_PARAMETERS = [["username", "ip_address"]]. - Behind a proxy, configure the client IP source so all users do not share the proxy's IP.
- Reset after successful login (
AXES_RESET_ON_SUCCESS = True) and provide a support unlock path (axes_resetcommand or admin). - 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.
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.
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()withrequestfor 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
- Firewall-level bans: Protect Django Login With fail2ban.
- Make passwords insufficient on their own with Add Two-Factor Authentication to the Django Admin.
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.