Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Security
  6. /
  7. How To Install Fail2ban Ubuntu
GUIDESecurity

How to Install and Configure Fail2Ban on Ubuntu 24.04

28 min read

How to Install and Configure Fail2Ban on Ubuntu 24.04

Within minutes of a new VPS coming online, automated scanners will start hammering port 22 with username and password guesses. If you tail /var/log/auth.log on a fresh server, you will typically see hundreds of failed SSH login attempts per hour from IPs scattered across the world. Fail2Ban is the classic, battle-tested answer to that noise. This guide walks you through installing, configuring, and extending Fail2Ban on Ubuntu 24.04 LTS, from your first apt install to a hardened setup that protects SSH, nginx, WordPress, Postfix, Dovecot, and Fail2Ban itself against repeat offenders.

Running mail, WordPress, or anything exposed to the public internet? You need Fail2Ban on the box today. A CloudCore Starter VPS is a solid home for your first hardened Ubuntu server.

Table of Contents

  • What is Fail2Ban?
  • Why Every Server Needs Fail2Ban
  • Prerequisites
  • Step 1: Update System Packages
  • Step 2: Install Fail2Ban
  • Step 3: Enable and Start the Service
  • Step 4: Create jail.local
  • Step 5: Configure the SSH Jail
  • Step 6: Add Nginx Jails
  • Step 7: Protect WordPress with a Custom Filter
  • Step 8: Postfix and Dovecot Jails
  • Step 9: Enable Recidive for Repeat Offenders
  • Step 10: Write and Test a Custom Filter
  • Using fail2ban-client
  • Email Notifications
  • Whitelisting and ignoreip
  • Edge Banning with Cloudflare and AbuseIPDB
  • Persistent Bans and Incremental Ban Time
  • IPv6 Notes
  • Performance Tuning
  • Troubleshooting
  • FAQ
  • Next Steps
  • What is Fail2Ban?

    Fail2Ban is a log-scanning intrusion prevention framework written in Python. It watches log files for patterns that indicate abuse -- repeated failed SSH logins, nginx 401 responses, WordPress wp-login.php brute force, SMTP auth failures -- and bans the offending IP address by inserting a firewall rule. Bans are automatic, time-limited, and configurable per service.

    The architecture is small and focused. A daemon (fail2ban-server) loads jails, each jail pairs a filter (a regex that extracts IP addresses from matching log lines) with an action (what to do when a threshold is crossed). Filters live in /etc/fail2ban/filter.d/, actions in /etc/fail2ban/action.d/, and jails are declared in /etc/fail2ban/jail.conf or, much more safely, in your own /etc/fail2ban/jail.local override.

    Fail2Ban ships with filters for dozens of common services out of the box: OpenSSH, Postfix, Dovecot, Exim, ProFTPD, vsftpd, Apache, nginx, Roundcube, Asterisk, CouchDB, named, and many more. The action side is just as flexible -- block via iptables, nftables, ufw, ipset, Cloudflare's API, AWS security groups, or a simple /etc/hosts.deny entry. You can also chain actions, so a single ban can simultaneously block at the kernel firewall, report the IP to AbuseIPDB, and send you an email.

    Where it really pays off is the recidive jail -- a meta-jail that scans Fail2Ban's own log and bans IPs that keep getting banned by other jails. An attacker who trips the SSH jail three times in a day earns a one-week ban covering every protocol on the server. That single jail stops almost all long-running brute force campaigns cold.

    Why Every Server Needs Fail2Ban

    If you have ever watched a fresh VPS's auth.log, you already know the answer: the public IPv4 space is under constant scan. A new VPS on a major provider receives its first SSH connection attempt within roughly 90 seconds of booting. Within the first hour, expect 200-500 login attempts against root, admin, ubuntu, postgres, and a rotating cast of 50 common usernames. This is baseline background noise, not a targeted attack -- and it never stops.

    Without a rate limiter, three things go wrong:

    • Log noise drowns out real events. Genuine failed logins from your own team vanish into a wall of garbage.
    • CPU and I/O are wasted rejecting thousands of connections per hour. On a small VPS this is measurable.
    • Any weak password eventually loses. Most brute force campaigns are slow and patient. Given an unlimited number of tries, a dictionary of 10 million passwords, and enough time, attackers win against an 8-character password.
    Fail2Ban breaks the loop. After a handful of failed attempts (default: 5), the offender's IP is dropped at the firewall for an hour. When they come back -- and they do -- the recidive jail notices the pattern and bans them for a week. An attacker running a coordinated scan is forced to rotate through thousands of IPs just to maintain pressure, which raises the cost enough that most automated botnets move on to softer targets.

    Fail2Ban is not a replacement for SSH key auth, strong passwords, or a firewall -- it is a complement. Run all of them together.

    Prerequisites

    • A VPS running Ubuntu 24.04 LTS with root or sudo access
    • SSH access to your server
    • Basic familiarity with systemd and journalctl
    • UFW, iptables, or nftables already active (Fail2Ban uses whichever is present)
    • (Optional) A working mail relay (or Postfix in local-only mode) if you want email notifications
    Recommended Plan: CloudCore Starter
    >
    Fail2Ban is lightweight -- it runs happily on the smallest VPS. For most personal and small business workloads we recommend the CloudCore Starter plan. It gives you enough headroom for Fail2Ban plus SSH, nginx, Postfix, and a typical WordPress or Node.js site.

    Connect to your server to get started:

    bash
    ssh root@your-server-ip

    Step 1: Update System Packages

    Before installing anything, refresh your package index and install outstanding security updates:

    bash
    sudo apt update && sudo apt upgrade -y

    If the kernel was updated, reboot before continuing:

    bash
    sudo reboot

    Step 2: Install Fail2Ban

    Fail2Ban is in Ubuntu's main repositories. Install it with:

    bash
    sudo apt install -y fail2ban

    Expected output (abbreviated):

    text
    The following NEW packages will be installed:
      fail2ban python3-pyinotify python3-systemd whois
    ...
    Setting up fail2ban (1.0.2-3ubuntu0.1) ...
    Created symlink /etc/systemd/system/multi-user.target.wants/fail2ban.service -> /lib/systemd/system/fail2ban.service.

    The install pulls in python3-systemd (so Fail2Ban can read the journal directly) and whois (used by one of the bundled notification actions). The fail2ban.service systemd unit is enabled and started automatically.

    Verify the installed version:

    bash
    fail2ban-client --version

    Expected output:

    text
    Fail2Ban v1.0.2

    Step 3: Enable and Start the Service

    The package enables itself on install, but confirm:

    bash
    sudo systemctl enable --now fail2ban
    sudo systemctl status fail2ban

    Expected output:

    text
    ● fail2ban.service - Fail2Ban Service
         Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled; preset: enabled)
         Active: active (running) since Wed 2026-04-16 10:00:00 UTC; 10s ago
           Docs: man:fail2ban(1)
       Main PID: 1234 (fail2ban-server)
          Tasks: 5 (limit: 4915)
         Memory: 12.3M
            CPU: 180ms

    Check that Fail2Ban sees at least the default sshd jail:

    bash
    sudo fail2ban-client status

    Expected output:

    text
    Status
    |- Number of jail:      1
    - Jail list:   sshd</code></pre></div>

    Fail2Ban ships with the sshd jail enabled by default on Ubuntu, so you already have basic SSH protection from this point forward. Everything else in this guide builds on that baseline.

    Step 4: Create jail.local

    Rule one of Fail2Ban administration: do not edit /etc/fail2ban/jail.conf. That file is shipped and overwritten by the Debian/Ubuntu package on every upgrade. Instead, create a jail.local that overrides it. Both files are read, and settings in jail.local win.

    Create the file:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo nano /etc/fail2ban/jail.local</code></pre></div>

    Start with a [DEFAULT] block that sets sensible global policy for every jail:

    <div class="code-block" data-lang="ini"><div class="code-block__header"><span class="code-block__lang">ini</span></div><pre><code class="language-ini">[DEFAULT]

    How long a ban lasts (1 hour)

    bantime = 1h

    The window within which maxretry is counted (10 minutes)

    findtime = 10m

    Failures allowed within findtime before a ban

    maxretry = 5

    Read logs from systemd journal (preferred on Ubuntu 24.04)

    backend = systemd

    Default ban action -- use ufw if UFW is active, otherwise iptables-multiport

    banaction = ufw banaction_allports = ufw

    Trust these addresses and never ban them

    ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 192.168.0.0/16

    Destination email for notifications (only used if you enable mail actions)

    destemail = [email protected] sender = [email protected] mta = sendmail

    Default action: ban only (no email). Set to action_mwl for ban + mail with whois + log.

    action = %(action_)s</code></pre></div>

    A few notes on what you just wrote:

    • backend = systemd tells Fail2Ban to read events from the journal instead of tailing flat log files. On Ubuntu 24.04, SSH, nginx, Postfix, and Dovecot all log through systemd, which makes this backend more reliable than auto or polling. It also handles log rotation without any restart.
    • banaction = ufw assumes you are using UFW as your firewall front-end (the default on many Ubuntu installs). If you are using raw iptables, change both banaction and banaction_allports to iptables-multiport. If you are on nftables, use nftables-multiport.
    • ignoreip is a space-separated list of CIDR ranges that will never be banned. Add your home IP, your VPN's egress IP, and your office range before you start testing jails, otherwise a typo in your own password could lock you out.
    Save and close. Do not reload Fail2Ban yet -- we will add the individual jails first.

    Step 5: Configure the SSH Jail

    Append the SSH jail to jail.local:

    <div class="code-block" data-lang="ini"><div class="code-block__header"><span class="code-block__lang">ini</span></div><pre><code class="language-ini">[sshd] enabled = true port = ssh filter = sshd backend = systemd maxretry = 5 findtime = 10m bantime = 1h</code></pre></div>

    On Ubuntu 24.04 the OpenSSH server logs to the systemd journal, not to /var/log/auth.log. The backend = systemd setting above tells Fail2Ban to read journald, so you do not need a logpath entry. If you prefer to tail the legacy file (for example, because you have forwarded it elsewhere with rsyslog), swap the backend and add logpath = /var/log/auth.log instead.

    If you have moved SSH off port 22, update port accordingly -- for example port = 2222 -- so that the ban action knows which port to block.

    Reload Fail2Ban:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo systemctl reload fail2ban</code></pre></div>

    Check the jail came up:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo fail2ban-client status sshd</code></pre></div>

    Expected output:

    <div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">Status for the jail: sshd |- Filter | |- Currently failed: 0 | |- Total failed: 0 | - Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd - Actions |- Currently banned: 0 |- Total banned: 0 - Banned IP list:

    You are now protected against SSH brute force.

    Step 6: Add Nginx Jails

    If you run nginx, three built-in filters are worth enabling straight away: nginx-http-auth (brute force against HTTP basic auth), nginx-limit-req (clients breaching limit_req zones), and nginx-botsearch (scanners probing for /wp-admin, /.env, /phpmyadmin, and similar).

    Append to jail.local:

    ini
    [nginx-http-auth]
    enabled  = true
    filter   = nginx-http-auth
    port     = http,https
    logpath  = /var/log/nginx/error.log
    maxretry = 5

    [nginx-limit-req] enabled = true filter = nginx-limit-req port = http,https logpath = /var/log/nginx/error.log maxretry = 10 findtime = 5m bantime = 30m

    [nginx-botsearch] enabled = true filter = nginx-botsearch port = http,https logpath = /var/log/nginx/access.log maxretry = 2 findtime = 1d bantime = 1w

    Notes:

    • The first two read error.log because auth failures and limit_req rejections appear there. nginx-botsearch reads access.log because it pattern-matches on suspicious request URIs in successful-parse entries.
    • nginx-botsearch is aggressive by design -- two hits to /.env or /wp-login.php on a non-WordPress site is almost always malicious. We set bantime = 1w to hold offenders for a full week.
    • For nginx-limit-req to do anything useful, you need a limit_req_zone / limit_req directive in your nginx config that produces "limiting requests" log lines when breached.
    Reload and verify:

    bash
    sudo systemctl reload fail2ban
    sudo fail2ban-client status

    You should now see all four jails listed.

    Step 7: Protect WordPress with a Custom Filter

    Fail2Ban does not ship with a WordPress filter, but writing one takes two minutes. It targets the classic wp-login.php POST brute force pattern.

    Create the filter:

    bash
    sudo nano /etc/fail2ban/filter.d/wordpress.conf

    Paste:

    ini
    [Definition]
    failregex = ^<HOST> . "POST /wp-login\.php." (200|401|403) .*$
                ^<HOST> . "POST /xmlrpc\.php." (200|401|403) .*$
    ignoreregex =

    The <HOST> macro captures the client IP. The regex matches any POST to wp-login.php or xmlrpc.php. Because a real human login is also a POST that returns 200, we rely on frequency (maxretry) rather than trying to distinguish success from failure in the log line -- 5 logins in 10 minutes is almost never a legitimate user.

    Add the jail to jail.local:

    ini
    [wordpress]
    enabled  = true
    filter   = wordpress
    port     = http,https
    logpath  = /var/log/nginx/access.log
    maxretry = 5
    findtime = 10m
    bantime  = 2h

    Before reloading, test the filter against your live access log:

    bash
    sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress.conf

    Expected output (numbers will vary):

    text
    Running tests
    =============

    Use failregex filter file : wordpress, basedir: /etc/fail2ban Use log file : /var/log/nginx/access.log Use encoding : UTF-8

    Results =======

    Failregex: 42 total |- #) [# of hits] regular expression | 1) [28] ^<HOST> . "POST /wp-login\.php." (200|401|403) .*$ | 2) [14] ^<HOST> . "POST /xmlrpc\.php." (200|401|403) .*$ -</code></pre></div>

    If you see 0 total hits but you know there should be matches, your log format differs from the default and you will need to adjust the regex. Reload once the test passes:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo systemctl reload fail2ban</code></pre></div>

    Step 8: Postfix and Dovecot Jails

    If this server handles mail, enable the Postfix and Dovecot jails to stop SMTP/IMAP brute force:

    <div class="code-block" data-lang="ini"><div class="code-block__header"><span class="code-block__lang">ini</span></div><pre><code class="language-ini">[postfix] enabled = true filter = postfix port = smtp,465,submission backend = systemd maxretry = 5 bantime = 1h

    [postfix-sasl] enabled = true filter = postfix-sasl port = smtp,465,submission,imap,imaps,pop3,pop3s backend = systemd maxretry = 3 bantime = 2h

    [dovecot] enabled = true filter = dovecot port = pop3,pop3s,imap,imaps,submission,465,sieve backend = systemd maxretry = 5 bantime = 1h</code></pre></div>

    The postfix-sasl jail is narrower and stricter than the plain postfix jail: it triggers only on authenticated submission failures, which are a near-certain sign of attempted relay abuse. Three strikes and you're out.

    Reload:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo systemctl reload fail2ban sudo fail2ban-client status postfix-sasl</code></pre></div>

    Step 9: Enable Recidive for Repeat Offenders

    The recidive jail is the single most valuable jail after sshd. It watches Fail2Ban's own log for ban events and bans any IP that trips any jail multiple times.

    Append:

    <div class="code-block" data-lang="ini"><div class="code-block__header"><span class="code-block__lang">ini</span></div><pre><code class="language-ini">[recidive] enabled = true filter = recidive logpath = /var/log/fail2ban.log action = %(banaction_allports)s[name=%(__name__)s] bantime = 1w findtime = 1d maxretry = 3</code></pre></div>

    This says: if an IP gets banned by any other jail 3 times within 24 hours, ban it across all ports for a full week. That turns one bad SSH session into a blanket ban covering SSH, HTTP, HTTPS, SMTP, and IMAP.

    Note that recidive uses a file logpath (/var/log/fail2ban.log), not systemd. Fail2Ban writes its own action log to that file by default on Ubuntu, so no configuration change is needed.

    Reload:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo systemctl reload fail2ban sudo fail2ban-client status recidive</code></pre></div>

    Step 10: Write and Test a Custom Filter

    At some point you will need a filter for software that Fail2Ban doesn't know about -- a custom web app, a game server, an API with a custom auth log. The pattern is always the same.

    Assume you run a Node.js app that writes failed login lines like this to /var/log/myapp/app.log:

    <div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">2026-04-16T10:15:22Z WARN login_failed ip=203.0.113.47 user=admin reason=bad_password</code></pre></div>

    Create a filter:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo nano /etc/fail2ban/filter.d/myapp.conf</code></pre></div>

    Paste:

    <div class="code-block" data-lang="ini"><div class="code-block__header"><span class="code-block__lang">ini</span></div><pre><code class="language-ini">[Definition] failregex = ^. WARN login_failed ip=&lt;HOST&gt; .reason=bad_password.*$ ignoreregex =</code></pre></div>

    Two important pieces:

    • failregex is the pattern that identifies a failure. The macro is required -- it is what Fail2Ban uses to extract the IP. If your regex has no , Fail2Ban silently matches nothing.
    • ignoreregex lets you exclude specific lines that would otherwise match. Useful for suppressing legitimate cases (for example, health-check IPs or known monitoring probes).
    Test before wiring it up:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo fail2ban-regex /var/log/myapp/app.log /etc/fail2ban/filter.d/myapp.conf</code></pre></div>

    You want to see a non-zero Failregex hit count and 0 under Ignoreregex. You can also test with canned input:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash">sudo fail2ban-regex \ '2026-04-16T10:15:22Z WARN login_failed ip=203.0.113.47 user=admin reason=bad_password' \ /etc/fail2ban/filter.d/myapp.conf</code></pre></div>

    Once the filter matches, add a jail to jail.local:

    <div class="code-block" data-lang="ini"><div class="code-block__header"><span class="code-block__lang">ini</span></div><pre><code class="language-ini">[myapp] enabled = true filter = myapp port = http,https logpath = /var/log/myapp/app.log maxretry = 5</code></pre></div>

    And reload.

    Using fail2ban-client

    fail2ban-client is the day-to-day operator tool. The commands you will actually use:

    <div class="code-block" data-lang="bash"><div class="code-block__header"><span class="code-block__lang">bash</span></div><pre><code class="language-bash"># Overall status -- which jails are running sudo fail2ban-client status

    Status of a specific jail, including currently banned IPs

    sudo fail2ban-client status sshd

    Manually ban an IP in a jail

    sudo fail2ban-client set sshd banip 198.51.100.22

    Manually unban an IP in a jail

    sudo fail2ban-client set sshd unbanip 198.51.100.22

    Unban an IP from every jail that currently holds it

    sudo fail2ban-client unban 198.51.100.22

    Unban every IP across every jail (emergency use)

    sudo fail2ban-client unban --all

    Reload after editing jail.local or a filter

    sudo fail2ban-client reload

    Reload a single jail (faster, no full restart)

    sudo fail2ban-client reload sshd

    Show the current runtime value of a jail setting

    sudo fail2ban-client get sshd bantime

    Change a runtime setting without editing config (lost on restart)

    sudo fail2ban-client set sshd bantime 7200</code></pre></div>

    Output of status sshd on a server that has seen real traffic:

    <div class="code-block" data-lang="text"><div class="code-block__header"><span class="code-block__lang">text</span></div><pre><code class="language-text">Status for the jail: sshd |- Filter | |- Currently failed: 2 | |- Total failed: 1847 | - Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd - Actions |- Currently banned: 4 |- Total banned: 219 - Banned IP list: 45.148.10.12 141.98.11.22 193.32.162.56 218.92.0.14

    Email Notifications

    To get an email every time an IP is banned, change the jail action from %(action_)s (ban only) to %(action_mwl)s (ban + mail with whois + log snippet).

    In jail.local, either in [DEFAULT] for everything or per-jail:

    ini
    [sshd]
    enabled  = true
    action   = %(action_mwl)s
    destemail = [email protected]
    sender    = [email protected]

    The mwl action uses the sendmail-whois-lines action under the hood: it runs whois against the banned IP, pulls the matching lines out of your log, and mails the full bundle. Emails look like this:

    text
    Subject: [Fail2Ban] sshd: banned 203.0.113.47 from ...

    Hi,

    The IP 203.0.113.47 has just been banned by Fail2Ban after 5 attempts against sshd.

    Here is more information about 203.0.113.47 (from whois ...): inetnum: 203.0.113.0 - 203.0.113.255 ...

    Lines containing failures of 203.0.113.47: Apr 16 10:15:02 srv sshd[1847]: Failed password for root from 203.0.113.47 ...

    action_mw (without the l) skips the log lines -- use it if emails are too noisy.

    For this to work, sendmail (or a drop-in like msmtp, postfix in null-client mode, or ssmtp) must be installed and able to relay mail. On a fresh VPS, sudo apt install -y postfix and selecting "Internet Site" is the quickest path.

    Whitelisting and ignoreip

    Never ban yourself. The ignoreip directive accepts a space-separated list of:

    • IPv4 or IPv6 addresses
    • CIDR ranges (10.0.0.0/8, 2001:db8::/32)
    • DNS names -- Fail2Ban resolves them at start time
    Example [DEFAULT] block:

    ini
    ignoreip = 127.0.0.1/8 ::1 203.0.113.42 192.168.0.0/16 home.example.com

    Keep in mind:

    • DNS-based whitelisting is resolved once at startup (and on reload). A dynamic hostname that changes IP will stop working until you reload.
    • ignoreip applies to all jails. You can override it per-jail if, for example, you want monitoring IPs to be ignored by sshd but not by nginx-limit-req.
    • Use ignoreself = true (the default) to automatically exempt the server's own interfaces.
    If you have a jump host or an office VPN, put its egress IP in ignoreip before you reload, or your first mistyped password will lock you out.

    Edge Banning with Cloudflare and AbuseIPDB

    Fail2Ban bans at your server's firewall, which is great but wastes a round trip -- the attacker still reaches your edge. If you run behind Cloudflare, push bans one layer further out.

    Cloudflare API action

    Ubuntu ships cloudflare.conf and cloudflare-token.conf under /etc/fail2ban/action.d/. The token-based one is current and preferred. Create an API token in your Cloudflare dashboard with Zone -> Firewall Services -> Edit permission scoped to the zone you want to protect.

    Drop a credentials file:

    bash
    sudo mkdir -p /etc/fail2ban/action.d
    sudo tee /etc/fail2ban/action.d/cloudflare-token.local > /dev/null <<'EOF'
    [Init]
    cftoken = YOUR_CLOUDFLARE_API_TOKEN_HERE
    EOF
    sudo chmod 600 /etc/fail2ban/action.d/cloudflare-token.local

    Then chain the action on a web-facing jail:

    ini
    [nginx-botsearch]
    enabled  = true
    filter   = nginx-botsearch
    port     = http,https
    logpath  = /var/log/nginx/access.log
    maxretry = 2
    bantime  = 1w
    action   = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s"]
               cloudflare-token[cftoken="YOUR_CLOUDFLARE_API_TOKEN_HERE"]

    Now a ban lands both in your local firewall and Cloudflare's edge rules, so the attacker never reaches your origin.

    AbuseIPDB reporting

    AbuseIPDB crowdsources reputation data. Fail2Ban can report every ban back to the database, which helps everyone. Get an API key at abuseipdb.com and add the action:

    ini
    [sshd]
    action = %(action_)s
             abuseipdb[abuseipdb_apikey="YOUR_KEY", abuseipdb_category="18,22"]

    Category 18 is "Brute-Force" and 22 is "SSH". See /etc/fail2ban/action.d/abuseipdb.conf for the full category list.

    Persistent Bans and Incremental Ban Time

    By default, bans evaporate when Fail2Ban restarts, because the ban list lives in memory. Fail2Ban 0.11+ (Ubuntu 24.04 ships 1.0.2) supports persistent bans via a SQLite database at /var/lib/fail2ban/fail2ban.sqlite3 -- it is enabled by default. When the daemon starts, any still-active bans are re-applied to the firewall.

    What most people don't enable is incremental ban time, which makes each subsequent ban longer than the last. Add to [DEFAULT]:

    ini
    bantime.increment = true
    bantime.factor    = 2
    bantime.maxtime   = 1w
    bantime.rndtime   = 10m
    bantime.overalljails = true

    With these settings:

    • First ban = bantime (1h)
    • Second ban = 2h
    • Third ban = 4h, then 8h, 16h, 32h, 64h, capped at 1w
    • rndtime adds a random 0-10 minute jitter so botnets can't sync around your unban schedule
    • overalljails = true means a ban in any jail counts toward the increment, not just the same jail
    This is strictly better than a static bantime for almost every workload. The only case where you want static bantime is a service where occasional false positives are likely and you don't want a typo to produce a week-long ban.

    IPv6 Notes

    Ubuntu 24.04 supports IPv6 out of the box, and so does Fail2Ban. The bundled banactions (ufw, iptables-multiport, nftables-multiport) all handle v6 correctly when the underlying firewall is configured for v6 as well. Two small things to check:

    • If you're on UFW, make sure IPV6=yes is set in /etc/default/ufw (it is by default).
    • Your ignoreip list should include ::1 and any v6 ranges you trust.
    • The systemd journal backend picks up v6 addresses just like v4.
    Verify v6 is being watched:

    bash
    sudo fail2ban-client status sshd

    If you have had real IPv6 brute force, you should see v6 addresses in Banned IP list.

    A Jail for Fail2Ban Itself

    The recidive jail you enabled in Step 9 already protects Fail2Ban itself: if an attacker keeps getting banned by any jail, recidive escalates them to a week-long all-ports ban. This is the community-standard way to harden Fail2Ban against patient attackers who rotate IPs slowly -- it turns "stay under maxretry and try again in an hour" into "get caught three times in a day and lose a week of access."

    If you want stricter behaviour, lower recidive's maxretry to 2 and raise bantime to 4w.

    Performance Tuning

    Fail2Ban is efficient but not free. On large systems with GB-sized log files and hundreds of active jails, a few tweaks help.

    Prefer the systemd backend

    On Ubuntu 24.04, backend = systemd is dramatically faster than polling flat files. The journal indexes entries by unit, so Fail2Ban reads only lines that could possibly match. Polling a 2 GB access log on every scan cycle is much more expensive.

    Rotate aggressively

    If you must use a file backend, make sure logrotate is running daily and keeps logs trim. A 500 MB access.log will slow the access.log-based nginx jails' cold-start scan considerably.

    Use ipset or nftables for large ban lists

    iptables-multiport inserts one rule per banned IP. Once you have more than a few hundred active bans, packet filtering slows down. Switch to iptables-ipset-proto6 (or use nftables' native set support) so bans live in a kernel hash set that matches in O(1). On Ubuntu 24.04, nftables is the modern default:

    ini
    banaction          = nftables-multiport
    banaction_allports = nftables-allports

    Reduce findtime on noisy jails

    If nginx-limit-req fires constantly, reduce its findtime from 10m to 1m. That keeps the internal failure queue small and bans snappier.

    Disable jails you don't need

    Every enabled jail consumes CPU per incoming log event. If you don't run Postfix, don't enable postfix. If you don't run Dovecot, don't enable dovecot. Less is more.

    Logging Configuration

    Fail2Ban's own log is at /var/log/fail2ban.log by default. Adjust verbosity in /etc/fail2ban/fail2ban.local (create it -- again, do not edit fail2ban.conf):

    ini
    [Definition]
    loglevel = INFO
    logtarget = /var/log/fail2ban.log

    Loglevels in order of verbosity: CRITICAL, ERROR, WARNING, NOTICE, INFO (default), DEBUG, TRACEDEBUG, HEAVYDEBUG. Bump to DEBUG temporarily when troubleshooting a regex, then put it back -- DEBUG fills disk fast on a busy server.

    Tail the log to watch bans in real time:

    bash
    sudo tail -f /var/log/fail2ban.log

    Troubleshooting

    ProblemCauseSolution
    Jail starts but never bans anythingfailregex doesn't match actual log formatRun sudo fail2ban-regex <logfile> <filter.conf>. If Failregex: 0 total, your regex is wrong. Compare a real log line against the pattern character by character.
    systemctl status fail2ban shows "failed" after reloadSyntax error in jail.local or a filtersudo fail2ban-client -d prints the full merged config and errors on the first problem. journalctl -u fail2ban -n 50 shows the exception.
    fail2ban-client set sshd banip X.X.X.X returns success but no firewall rule appearsUFW disabled, or banaction mismatched with actual firewallCheck sudo ufw status. If inactive, either enable UFW or change banaction to iptables-multiport. Verify with sudo iptables -L -n \</td><td>grep X.X.X.X.
    Whitelist doesn't workTypo in ignoreip, or hostname hasn't been re-resolvedsudo fail2ban-client get DEFAULT ignoreip shows the active list. Hostnames resolve at startup only -- sudo systemctl restart fail2ban after DNS changes.
    CPU spikes to 100%Regex catastrophic backtracking, or polling huge filesCheck with sudo fail2ban-regex --verbose. Switch to backend = systemd. Rewrite greedy . patterns to non-greedy .? or anchored alternatives.
    Bans don't persist across rebootsSQLite DB disabled or world-writable /var/lib/fail2ban was clearedCheck /etc/fail2ban/fail2ban.local for dbfile. Default is /var/lib/fail2ban/fail2ban.sqlite3. It should exist and be writable by root.
    IPv6 addresses show up as ?Older Fail2Ban on a mismatched filterUbuntu 24.04 ships a recent enough build that this is not an issue. If you see it, update the filter in /etc/fail2ban/filter.d/.
    recidive jail never bans anything despite SSH bans firinglogpath for recidive points at the wrong fileConfirm Fail2Ban is logging to /var/log/fail2ban.log (not journal). Check fail2ban.conf for logtarget.
    You locked yourself outToo aggressive, no ignoreipFrom the provider's web console or KVM, sudo fail2ban-client unban <your-ip>, then add your IP to ignoreip before reloading.

    Essential Diagnostic Commands

    bash
    # Real-time Fail2Ban log
    sudo tail -f /var/log/fail2ban.log

    Systemd service log

    sudo journalctl -u fail2ban -f

    Dump the full merged configuration

    sudo fail2ban-client -d

    Test a filter against a live log

    sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

    List every jail and its current counters

    sudo fail2ban-client status

    FAQ

    Fail2Ban vs CrowdSec -- which should I use?

    Both scan logs and ban abusers, but they solve slightly different problems. Fail2Ban is a classic, minimal, self-contained daemon: one server, one log, one ban list, no external dependencies. Every ban is local. CrowdSec is a newer design built around a shared reputation network: your local agent detects abusers and reports them, and in return you pull down a community blocklist of IPs that other CrowdSec users have seen attacking them. CrowdSec scales better to large fleets, but Fail2Ban is simpler, more scriptable, and has been battle-tested for two decades. For a single VPS or a small fleet, Fail2Ban is often the right choice. For a larger attack surface where you want network effects, add CrowdSec. Many operators run both: Fail2Ban on the edge for immediate reaction, CrowdSec for crowd-sourced reputation. See our CrowdSec installation guide for the complementary setup.

    Fail2Ban vs a cloud WAF like Cloudflare -- do I need both?

    Yes. A WAF stops attacks at the edge based on static rulesets (OWASP rules, rate limits per path, bot scoring), and it is excellent at that. Fail2Ban stops attacks at the origin based on your service's actual authentication logs. The WAF doesn't know whether your SSH logins are succeeding, and Fail2Ban doesn't know what the attacker's browser fingerprint looks like. They are complementary layers. The Cloudflare action integration described above connects them -- Fail2Ban detects at the origin and pushes bans back to the edge -- which gives you both signals.

    How do I deal with false positives?

    Three techniques, in order of preference: (1) whitelist the source in ignoreip, (2) add specific patterns to the filter's ignoreregex so they don't count as failures, (3) lower maxretry thresholds so genuine retries don't trigger bans. If you find yourself banning a single IP repeatedly for no reason, use sudo fail2ban-regex against a real log sample to see which regex is matching -- it is almost always a too-greedy failregex. The fix is to tighten the pattern, not to raise the threshold.

    Can Fail2Ban do country-based (GeoIP) blocking?

    Not natively. Fail2Ban bans individual IPs based on log events, not entire countries. For geo-blocking, use your firewall's native geoip module: ipset with MaxMind's GeoLite2 data, nftables' @geoip sets, or a CDN's country-level firewall rule. You can combine the two -- geoip-block entire regions you never serve, and let Fail2Ban handle the long tail inside allowed regions.

    Does Fail2Ban support ipset?

    Yes. The iptables-ipset-proto6 and iptables-ipset-proto6-allports banactions store banned IPs in an ipset hash rather than inserting one iptables rule per IP. This is essential for servers that accumulate thousands of bans -- kernel lookups stay O(1) regardless of list size. On Ubuntu 24.04 with nftables as the default backend, use nftables-multiport instead, which uses native nftables sets and is equally efficient.

    How do I run Fail2Ban in a Docker container?

    Two approaches. The easy one: run Fail2Ban on the host and let it watch logs from containers mounted in (via bind-mounts) or read from the journal (the journal contains all container logs if you use the journald log driver). The harder one: run Fail2Ban inside a container with --cap-add=NET_ADMIN and --network=host, reading mounted log volumes. Either works, but the host-based approach is almost always simpler and avoids the complication of a container trying to manage the host's firewall. If you run the whole app stack inside containers behind Traefik or nginx, have Traefik log access events in a format Fail2Ban understands and write a filter for it.

    Does reload or restart drop active bans?

    fail2ban-client reload preserves active bans (and the SQLite backing store). A full systemctl restart fail2ban also preserves them, because the database is read back on startup and the firewall rules are re-inserted. Prefer reload for config changes -- it is faster and avoids a small window where the service is down.

    Next Steps

    Now that Fail2Ban is protecting your server, build on the foundation:

    • Layer CrowdSec on top -- Read our CrowdSec install guide to add crowd-sourced threat intelligence alongside your local Fail2Ban rules. The two together cover both known-bad IPs (CrowdSec) and your own service's live attackers (Fail2Ban).
    • Move SSH behind a VPN -- The best rate limiter is "no public SSH at all." Set up WireGuard on Ubuntu 24.04 and bind SSH to the VPN interface only. Fail2Ban then guards everything else.
    • Harden nginx -- Our nginx hardening guide covers TLS, security headers, rate limiting, and limit_req zones that feed the nginx-limit-req jail.
    • Monitor bans over time -- Ship /var/log/fail2ban.log to a central log store (Loki, ELK, or even a simple Grafana Cloud trial) and graph bans per day. You will spot attack campaigns as they ramp up.
    • Audit your filter regexes quarterly -- Log formats change as software updates. A filter that matched last year may silently match nothing today. fail2ban-regex against a recent log sample takes 30 seconds and catches these regressions.

    Need a server to run Fail2Ban on?
    >
    Every CloudCore Starter VPS gives you a clean Ubuntu 24.04 LTS install with full root access, a real public IP, and enough headroom to run Fail2Ban, UFW, nginx, and a handful of services without breaking a sweat.
    >
    - Ubuntu 24.04 LTS pre-installed
    - Full root / sudo access
    - Dedicated IPv4 + IPv6
    - 99.9% uptime SLA
    - Deploy in under 60 seconds
    >
    Launch Your Hardened VPS and start banning bots before your first coffee break.

    Was this article helpful?

    ← Back to SecurityBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket