If you have ever tailed your access logs and watched a flood of 401 or 403 codes from random IPs around the world, you know the feeling. Django Fail2ban: in this first part of our Securing Django on EC2 series, we explore how to integrate Django with Fail2ban protections using Apache2.
Building an IP Ban System with Apache2 and Fail2ban
This is a practical guide for developers deploying Django on EC2 who want to use Fail2ban to detect malicious login attempts and dynamically block offending IP’s.
Fail2ban is an intrusion prevention software framework written in the Python programming. It is designed to prevent brute-force attacks by responding to log signals.
What We’ll Build
To build an effective Django Fail2ban setup, we want a configuration that:
- Watches our access logs for repeated failed login attempts.
- Bans the offending IPs at the web server level (before Django ever sees them).
- Lifts automatically when the ban expires.
- Works for both IPv4 and IPv6 addresses.
We will use a dynamic banlist file at:
/etc/apache2/djangoapp/banlist.conf
containing lines like:
Require not ip 203.0.113.55
Require not ip 2607:fb91:2d30:c2ab:bdf2:3fd6:c57b:6d30
Apache parses every site visit through this include file. If your IP is listed, you’re instantly blocked. It’s simple, auditable, and transparent.
Step 1: Create an Apache banlist include
Typically, when using Fail2ban, we apply a firewall ban rule (e.g., ufw). With the exception of Docker containers, this is foolproof attackers are prevented from accessing the server altogether.
However, when we deploy Django behind AWS CloudFront, we have no IP to block at the firewall level. So instead, we’ll use Apache’s access control.
Create a directory and banlist file for your site:
sudo mkdir -p /etc/apache2/djangoapp
echo '# Managed by fail2ban' | sudo tee /etc/apache2/djangoapp/banlist.conf
sudo chmod 0644 /etc/apache2/djangoapp/banlist.conf
Edit your site’s vhost (/etc/apache2/sites-available/djangoapp.conf) and include this file inside your access rules:
<RequireAll>
Require all granted
IncludeOptional /etc/apache2/djangoapp/banlist.conf
</RequireAll>
You can also scope the bans only to /admin/ if you prefer:
<Location "/admin/">
<RequireAll>
Require all granted
IncludeOptional /etc/apache2/djangoapp/banlist.conf
</RequireAll>
</Location>
Reload Apache:
sudo apachectl configtest
sudo systemctl reload apache2
This ensures every IP in the file is denied access to your Django app.
Step 2: Teach Fail2ban how to recognize bad login attempts
Fail2ban works by scanning log files for patterns, and when paired with Django, it becomes a powerful protective layer. We’ll tell it to watch for 401 responses on /admin/login.
Create the filter definition at:
/etc/fail2ban/filter.d/djangoapp-admin-login.conf
[Definition]
# Match Apache access log entries like:
# 2607:fb91:... - - [27/Oct/2025:19:35:51 +0000] "POST /admin/login/?next=/admin/ HTTP/1.1" 401 5195 ...
#
# <HOST> handles IPv4 and IPv6.
# We focus on POSTs to /admin/login that returned 401.
#failregex = ^<HOST> \S+ \S+ \[[^\]]+\] "POST \/admin\/login(?:\/)?(?:\?[^"]*)? HTTP\/1\.[01]" 401 \d+
failregex = ^<HOST> .* "POST .*HTTP\/1\.[01]" 401 \d+
ignoreregex =
Test it quickly:
sudo fail2ban-regex /var/log/apache2/djangoapp_access.log /etc/fail2ban/filter.d/djangoapp-admin-login.conf
You should see matches if you have failed admin logins in your logs.
Step 3: Write a custom Fail2ban action (core of the Django Fail2ban workflow) that edits the Apache banlist
Now we define how Fail2ban reacts when it detects an offender.
Create /etc/fail2ban/action.d/djangoapp-apache-banlist.conf:
[Definition]
# Parameters:
# - banlist: path to the Apache include file
# - apache_reload_cmd: how to reload Apache
actionstart = touch <banlist>
actionstop = /bin/true
# Add "Require not ip <ip>" if not already present, then graceful reload.
actionban = set -euo pipefail; \
BL="<banlist>"; IP="<ip>"; \
LINE="Require not ip ${IP}"; \
if ! /usr/bin/grep -qxF "$LINE" "$BL"; then echo "$LINE" >> "$BL"; fi; \
<apache_reload_cmd>
# Remove the line on unban, then reload.
actionunban = set -euo pipefail; \
BL="<banlist>"; IP="<ip>"; \
LINE="Require not ip ${IP}"; \
ESC="$(printf "%%s\n" "$LINE" | /usr/bin/sed -e "s/[.[\\*^$(){}+?|\\\\]/\\\\&/g")"; \
/usr/bin/sed -i -e "\|^$ESC$|d" "$BL"; \
<apache_reload_cmd>
[Init]
banlist = /etc/apache2/djangoapp/banlist.conf
apache_reload_cmd = /usr/sbin/apachectl graceful
Every time an IP is banned, Fail2ban inserts a Require not ip line, and Apache is gracefully reloaded.
Step 4: Configure the jail
Create /etc/fail2ban/jail.d/djangoapp-admin-login.local:
[djangoapp-admin-login]
enabled = true
filter = djangoapp-admin-login
logpath = /var/log/apache2/djangoapp_access.log
backend = auto
# Tuning:
# - After 5 bad logins within 10 minutes, ban for 24 hours.
maxretry = 4
findtime = 10m
bantime = 1h
action = aisly-apache-banlist[name=djangoapp, banlist=/etc/apache2/aisly/banlist.conf]
Restart Fail2ban:
sudo systemctl restart fail2ban
sudo fail2ban-client status djangoapp-admin-login
Step 5: Watch it in action
After a few bad login attempts, you should see:
sudo fail2ban-client status djangoapp-admin-login
# Banned IP list: 2600:4040:512c:c300:d8db:472:dc41:2918
cat /etc/apache2/djangoapp/banlist.conf
# Require not ip 2600:4040:512c:c300:d8db:472:dc41:2918
Unban it manually to test:
sudo fail2ban-client set djangoapp-admin-login unbanip 2600:4040:512c:c300:d8db:472:dc41:2918
Youll see the line disappear, and Apache will reload gracefully.
Wrapping Up: Django + Fail2ban (Strengthening your EC2 security with Django Fail2ban) for Real Security
We just built a clean bridge between Django’s login monitoring and Apache’s native access control, showing how Django Fail2ban integration can harden your app on EC2.
The beauty of this method is that once an IP lands in banlist.conf, it is cut off before Django even executes a single line of Python.
Server security does not have to be complicated. Sometimes, all it takes is a few lines of shell, a good regex, and a little sed magic.
Coming up next:
Securing Django on EC2, Part 2 Using Fail2ban to Block Repeated 403s and Suspicious Traffic Patterns.

