BlogGuides

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.

On this page

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:

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 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
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
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
[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
[Definition]
# Spot HTTP 401 responses on /auth/login/
failregex = ^<HOST> -.*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
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 on every failed authenticate(), the admin's included. Listen for it:

    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)
    

    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.

  2. Send those lines to their own file, in settings.py:

    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"}},
    }
    

    Create /var/log/django and give it to the user your app runs as, so Django can write there.

  3. Write the filter, /etc/fail2ban/filter.d/django-auth.conf:

    INI
    [Definition]
    failregex = login failed from <HOST>$
    ignoreregex =
    
  4. Add the jail to jail.local and restart Fail2Ban:

    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
    sudo fail2ban-client status
    
  2. Inspect the Django-Login Jail
    Shell
    sudo fail2ban-client status django-login
    
  3. Simulate Robot Knocks
    Shell
    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
    
    After 5 "bad" tries, your IP should appear in the banned list!

Test a filter against your real log before you wait for robots. fail2ban-regex reads the log and prints how many lines matched:

Shell
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
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 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 <jail> unbanip <ip>, 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 <jail>. 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 covers the rest by hand, from SSH keys to Celery. If you'd rather not build it yourself, see deploying Django without Docker: when ox prepares a server you own, 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.

Updated : added the Django admin setup, filter testing, unbanning, a FAQ, and fixed a jail name.

First published on blog.sorv.dev.

Keep reading

Deploy to a server you own

ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free during the beta.

Sign up with GitHub Read the docs