[Blog](https://console.f1b10.testing.getox.in/blog) [Guides](https://console.f1b10.testing.getox.in/blog#c-guides) # Fail2Ban for Django: Stop Brute-Force Logins on Your VPS Use Fail2Ban to stop brute-force logins on Django: ban IPs from Nginx logs, catch failed Django admin logins with a signal, and test every filter safely. Saurav Sharma Jun 4, 2025 7 min read On this page - [1. What Is Fail2Ban?](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-1-what-is-fail2ban) - [2. Why Use Fail2Ban with Django?](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-2-why-use-fail2ban-with-django) - [3. Setting Up Fail2Ban Step-by-Step](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-3-setting-up-fail2ban-step-by-step) - [How do I protect the Django admin login with Fail2Ban?](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#admin-login) - [4. Testing Your Guard](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-4-testing-your-guard) - [5. Extra Security Tips](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-5-extra-security-tips) - [Key takeaways](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#key-takeaways) - [Frequently asked questions](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#faq) - [6. You Did It!](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-6-you-did-it) Fail2Ban protects a Django site by reading a log of failed logins and banning the IP address behind them at the firewall for a while. For an API login that answers 401, the Nginx access log is enough. The Django admin answers a failed login with a normal 200 page, so for the admin you log failed logins from Django itself and point Fail2Ban at that file. This guide sets up both, plus the SSH jail every server should have, and shows how to test a filter before you trust it. **In this guide:** - [What Fail2Ban is](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-1-what-is-fail2ban) - [Setting up Fail2Ban for Django, step by step](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-3-setting-up-fail2ban-step-by-step) - [Protecting the Django admin login](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#admin-login) - [Testing your filters and jails](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-4-testing-your-guard) - [Frequently asked questions](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#faq) Imagine your Django server is a cozy castle. Brave knights (your users) come and go, but naughty robots (bad bots) sometimes knock repeatedly on secret doors (login pages) trying to sneak in. We need **Fail2Ban**, a friendly guard dog, to watch the castle logs and chase away any robot that knocks too many times! --- ## 1. What Is Fail2Ban? - **Guard Dog**: Monitors log files like a watchful puppy. - **Jails**: Special zones around each door (service) where the guard listens. - **Filters**: Magic spells (regular expressions) that spot naughty knocks (bad actions). - **Actions**: When a robot misbehaves too much, the guard slams the door shut by adding a firewall rule. --- ## 2. Why Use Fail2Ban with Django? 1. **Protect the Admin Tower** The `/admin/` page is a treasure room. Don't let robots guess passwords! 2. **Guard the Login Gate** Your API login endpoint (`/auth/login/`) needs extra eyes. 3. **Stop Crawler Floods** Bots triggering hundreds of 404 pages can overwhelm the castle. 4. **Layered Security** Complements your Django `settings.py` security (SSL redirect, HSTS, X-Frame-Options). Django itself does not throttle login attempts. Its [security docs](https://docs.djangoproject.com/en/stable/topics/security/) suggest a plugin or a web server module for that, and Fail2Ban is the web-server-side guard: it stops the robot before it ever reaches Django. --- ## 3. Setting Up Fail2Ban Step-by-Step ### Step 3.1: Install Your Guard Shell ```lang-bash sudo apt update sudo apt install -y ufw fail2ban ``` - **UFW**: The castle's drawbridge controller (firewall). - **Fail2Ban**: The guard dog watching your logs. ### Step 3.2: Configure the Drawbridge (UFW) Shell ```lang-bash sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow OpenSSH # Let knights in via SSH sudo ufw allow 'Nginx Full' # Let visitors reach your website (HTTP/HTTPS) sudo ufw enable ``` ### Step 3.3: Create the Jail Configuration Create `/etc/fail2ban/jail.local` and add: INI ```lang-ini [DEFAULT] bantime = 3600 # Ban duration: 1 hour findtime = 600 # Look back: 10 minutes maxretry = 5 # After 5 bad tries, ban [sshd] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log [django-login] enabled = true port = http,https filter = django-login logpath = /var/log/nginx/access.log ``` Never edit `jail.conf`: package updates overwrite it. Your settings go in `jail.local` or a file in `/etc/fail2ban/jail.d/`, and they win over the defaults. - **`bantime`**: How long the misbehaver is kicked out. - **`findtime`**: Time window to count failures. - **`maxretry`**: Number of strikes before the ban. - **`ignoreip`** (optional, in `[DEFAULT]`): Addresses never banned, like `127.0.0.1/8 ::1` and your own office IP, so you can't lock yourself out. If your server has no `/var/log/auth.log` (Debian 12 and later ship without rsyslog, so SSH logs go only to the systemd journal), replace the `logpath` line of `[sshd]` with `backend = systemd`. ### Step 3.4: Write Your Magic Spell (Filter) Create `/etc/fail2ban/filter.d/django-login.conf`: INI ```lang-ini [Definition] # Spot HTTP 401 responses on /auth/login/ failregex = ^ -.*POST /auth/login/.*" 401 ignoreregex = ``` This tells Fail2Ban: "Whenever you see a 401 Unauthorized on a login POST, count it!" This works for an API login, like one built with Django REST Framework, that answers a wrong password with 401. It does not catch the Django admin, which answers a wrong password with a normal 200 page. The next section covers the admin. ### Step 3.5: Start the Guard Dog Shell ```lang-bash sudo systemctl restart fail2ban ``` --- ## How do I protect the Django admin login with Fail2Ban? The admin tower needs a different spell. A failed admin login and a page view look the same in the Nginx log, so let Django write down every failed login, with the visitor's IP, and let Fail2Ban read that. 1. **Log failed logins from Django.** Django sends the [`user_login_failed` signal](https://docs.djangoproject.com/en/stable/ref/contrib/auth/#django.contrib.auth.signals.user_login_failed) on every failed `authenticate()`, the admin's included. Listen for it: Import the module in your app's `AppConfig.ready()` so the receiver is connected. Trust `X-Real-IP` only when Gunicorn listens on `127.0.0.1`, so that Nginx is the only one who can set it. Python ```python # yourapp/signals.py import logging from django.contrib.auth.signals import user_login_failed from django.dispatch import receiver logger = logging.getLogger("auth.failed") @receiver(user_login_failed) def log_failed_login(sender, credentials, request=None, **kwargs): if request is None: return # Behind Nginx every request comes from 127.0.0.1; Nginx's proxy_params # sends the real address as X-Real-IP. ip = request.META.get("HTTP_X_REAL_IP") or request.META.get("REMOTE_ADDR") logger.warning("login failed from %s", ip) ``` 2. **Send those lines to their own file**, in `settings.py`: Create `/var/log/django` and give it to the user your app runs as, so Django can write there. Python ```python LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": {"plain": {"format": "%(asctime)s %(message)s"}}, "handlers": { "failed_logins": { "class": "logging.FileHandler", "filename": "/var/log/django/failed-logins.log", "formatter": "plain", }, }, "loggers": {"auth.failed": {"handlers": ["failed_logins"], "level": "WARNING"}}, } ``` 3. **Write the filter**, `/etc/fail2ban/filter.d/django-auth.conf`: INI ```ini [Definition] failregex = login failed from $ ignoreregex = ``` 4. **Add the jail** to `jail.local` and restart Fail2Ban: INI ```ini [django-auth] enabled = true port = http,https filter = django-auth logpath = /var/log/django/failed-logins.log maxretry = 5 findtime = 10m bantime = 1h ``` Now every wrong password, on the admin or on any login form that calls `authenticate()`, counts as a strike. --- ## 4. Testing Your Guard 1. **Check All Jails** Shell ```lang-bash sudo fail2ban-client status ``` 2. **Inspect the Django-Login Jail** Shell ```lang-bash sudo fail2ban-client status django-login ``` 3. **Simulate Robot Knocks** After 5 "bad" tries, your IP should appear in the banned list! Shell ```lang-bash for i in {1..6}; do curl -s -o /dev/null -w "%{http_code}" \ -X POST https://your-domain.com/auth/login/ \ -H "Content-Type: application/json" \ -d '{"username":"bad","password":"bad"}' done ``` Test a filter against your real log before you wait for robots. `fail2ban-regex` reads the log and prints how many lines matched: Shell ```bash sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/django-login.conf sudo fail2ban-regex /var/log/django/failed-logins.log /etc/fail2ban/filter.d/django-auth.conf ``` Did you ban yourself while testing? Let yourself back in (use your own IP): Shell ```bash sudo fail2ban-client set django-login unbanip 203.0.113.7 ``` --- ## 5. Extra Security Tips - **Protect Your Logs**: Only root can edit `/var/log/*` so robots can't erase evidence. - **Combine with Django Security**: In `settings.py`, enable: - `SECURE_SSL_REDIRECT = True` - HSTS (`SECURE_HSTS_SECONDS`) - `X_FRAME_OPTIONS = "DENY"` - **Add More Jails**: - `nginx-http-auth` (blocks repeated 401 basic-auth failures) - `nginx-botsearch` (blocks bots hunting for admin pages and scripts that answer 404) - `nginx-limit-req` (stops traffic floods) - **Tweak Thresholds**: Adjust `bantime`, `findtime`, `maxretry` based on real traffic. - **Email Alerts**: Use `action_mwl` to notify you on each ban (it needs a working mail setup on the server). - **Per-account lockouts**: Fail2Ban bans addresses. A package like [django-axes](https://django-axes.readthedocs.io/) locks an account after repeated failures, even when the guesses come from many addresses. The two work well together. --- ## Key takeaways - Fail2Ban reads logs and bans IPs at the firewall; Django does not throttle logins on its own. - An API login that answers 401 can be caught from the Nginx access log. - The Django admin answers a failed login with 200, so log failures with the `user_login_failed` signal and watch that file. - Test every filter with `fail2ban-regex`, and set `ignoreip` so you can't ban yourself. - Keep the `sshd` jail on: SSH is the door robots knock on most. ## Frequently asked questions ### Does Fail2Ban work with the Django admin? Yes, but not from the Nginx log alone. A failed admin login returns a normal 200 page, so it looks like any page view. Log failed logins with Django's `user_login_failed` signal, including the visitor's IP, write them to a file, and point a Fail2Ban filter and jail at that file. ### Should I use django-axes or Fail2Ban? Both, if you can. Fail2Ban blocks an address at the firewall, so a robot stops reaching your app at all. django-axes works inside Django and locks an account after repeated failures, even when the guesses come from many addresses. One protects the server, the other protects each account. ### How do I unban an IP in Fail2Ban? Run `sudo fail2ban-client set unbanip `, for example `sudo fail2ban-client set sshd unbanip 203.0.113.7`. To see which addresses a jail holds right now, run `sudo fail2ban-client status `. Add your own address to `ignoreip` so it never gets banned again. ### How do I test a Fail2Ban filter? Run `fail2ban-regex` with the log file and the filter file, like `sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/django-login.conf`. It prints how many lines matched and which ones were missed. Fix the regex until every failed login matches and normal requests do not, then restart Fail2Ban so the jail uses the new filter. --- ## 6. You Did It! 🎉 You now have a trusty Fail2Ban guard watching your Django castle. No more pesky robots breaking in! Keep learning, keep coding, and enjoy your safe server. Next step: the guard dog is one part of a production server. My [Django and Vue.js VPS deployment guide](https://console.f1b10.testing.getox.in/blog/deployment-guide-for-django-vuejs-project-on-vps) covers the rest by hand, from SSH keys to Celery. If you'd rather not build it yourself, see [deploying Django without Docker](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker): when ox prepares [a server you own](https://console.f1b10.testing.getox.in/features#server), it turns on UFW and Fail2Ban's SSH jail for you (a Django login jail stays yours to add, as above), and failed-login warnings your app prints show up in [ox's searchable logs](https://console.f1b10.testing.getox.in/docs/operate/logs#filter). *Updated Oct 8, 2026: added the Django admin setup, filter testing, unbanning, a FAQ, and fixed a jail name.* First published on [blog.sorv.dev](https://blog.sorv.dev/fail2ban-magic-keeping-your-django-castle-safe). New to running apps on your own server? [How ox self-hosts your apps](https://console.f1b10.testing.getox.in/self-hosting) shows what ox sets up on your server. On this page - [1. What Is Fail2Ban?](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-1-what-is-fail2ban) - [2. Why Use Fail2Ban with Django?](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-2-why-use-fail2ban-with-django) - [3. Setting Up Fail2Ban Step-by-Step](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-3-setting-up-fail2ban-step-by-step) - [Step 3.1: Install Your Guard](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-step-31-install-your-guard) - [Step 3.2: Configure the Drawbridge (UFW)](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-step-32-configure-the-drawbridge-ufw) - [Step 3.3: Create the Jail Configuration](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-step-33-create-the-jail-configuration) - [Step 3.4: Write Your Magic Spell (Filter)](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-step-34-write-your-magic-spell-filter) - [Step 3.5: Start the Guard Dog](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-step-35-start-the-guard-dog) - [How do I protect the Django admin login with Fail2Ban?](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#admin-login) - [4. Testing Your Guard](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-4-testing-your-guard) - [5. Extra Security Tips](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-5-extra-security-tips) - [Key takeaways](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#key-takeaways) - [Frequently asked questions](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#faq) - [Does Fail2Ban work with the Django admin?](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#faq-admin) - [Should I use django-axes or Fail2Ban?](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#faq-axes) - [How do I unban an IP in Fail2Ban?](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#faq-unban) - [How do I test a Fail2Ban filter?](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#faq-test) - [6. You Did It!](https://console.f1b10.testing.getox.in/blog/fail2ban-magic-keeping-your-django-castle-safe#heading-6-you-did-it) Written by Saurav Sharma Builds ox. Writes about running apps on servers you own, without Docker and without a platform bill. [Older post Deploy Django with Uvicorn and Nginx: A Beginner's Guide](https://console.f1b10.testing.getox.in/blog/comprehensive-django-deployment-guide-for-beginners) [Newer post Deploy Django and Vue.js on a VPS: Nginx, Gunicorn, Celery](https://console.f1b10.testing.getox.in/blog/deployment-guide-for-django-vuejs-project-on-vps) ## Keep reading - Guides [How to Self-Host Web Apps on Your Own VPS (2026 Guide)](https://console.f1b10.testing.getox.in/blog/self-host-web-apps-on-your-own-vps) What you need to self-host a web app on a VPS: server, domain, HTTPS, database, backups. Four ways to set it up, their trade-offs, and how ox does it. Saurav Sharma Oct 15, 2026 8 min read - Guides [A FastAPI App on Your Own Server with One Config File](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps) Put a FastAPI app with uvicorn, Postgres and Alembic on your own Ubuntu server, with HTTPS and no Docker, using one small ox.toml file you can copy. Saurav Sharma Oct 8, 2026 7 min read - Guides [Deploy Django Without Docker: Gunicorn, systemd and Caddy](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker) Deploy Django without Docker on your Ubuntu VPS: gunicorn under systemd, Caddy for HTTPS and PostgreSQL. Every step by hand, then the same deploy with ox. Saurav Sharma Oct 8, 2026 9 min read ## Deploy to a server you own ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free for your first project. [Sign up with GitHub](https://console.f1b10.testing.getox.in/login/github) [Read the docs](https://console.f1b10.testing.getox.in/docs)