InfraRunBook
    Back to articles

    Setting Up Fail2Ban to Block Brute-Force Attacks on Your Server

    Cyber Security for Servers
    Published: Aug 24, 2026
    Updated: Aug 24, 2026

    A practical, step-by-step guide to installing and configuring Fail2Ban on a Linux server, with a full working jail.local example, verification steps, and the common mistakes that quietly break bans.

    Setting Up Fail2Ban to Block Brute-Force Attacks on Your Server

    If you have ever tailed

    /var/log/auth.log
    on a server sitting on the public internet, you already know the story. Within minutes of boot, you'll see a steady drizzle of failed SSH logins from IPs you've never heard of, usually trying root, admin, or some default account name. Most of these are automated scanners working through a list of hosts, not targeted attacks, but that doesn't make them harmless. Given enough time and a weak password somewhere, one of those attempts eventually lands. Fail2Ban is the tool I reach for first on every new box I provision, specifically because it turns that background noise into something the server handles on its own, without me babysitting logs at 2 a.m.

    In this guide I'll walk through setting up Fail2Ban on a typical Linux server, using sw-infrarunbook-01 as our example host, to protect SSH and a couple of other common services. I'll give you the full configuration I actually use, not just fragments, and I'll call out the mistakes I've personally made or seen colleagues make when they set this up in a hurry.

    Prerequisites

    Before you start, make sure you actually have these things in place. Skipping this step is how people end up locking themselves out of their own server, which is a genuinely bad way to start a Tuesday.

    • A Linux server with root or sudo access. I'm using Ubuntu/Debian conventions here, but I'll note the RHEL/CentOS differences where they matter.
    • A working firewall backend. Fail2Ban doesn't block traffic itself; it manipulates iptables, nftables, or firewalld rules. Know which one your system uses before you begin.
    • SSH access via a method that won't get you banned accidentally, ideally key-based auth from a known IP, or at minimum, know your own public IP address so you can whitelist it.
    • Root log access. Fail2Ban parses log files like
      /var/log/auth.log
      or, on systemd-based distros, reads directly from the journal.

    One thing I always do before touching Fail2Ban configuration: open a second terminal session to the server and keep it connected. If a bad regex or an overly aggressive ban rule locks out your own IP, having a second live session means you can fix it without needing out-of-band console access. I have seen this bite people who edited configs over a single SSH session and then couldn't reconnect to undo their own mistake.

    Step-by-Step Setup

    Start with installation. On Debian-based systems:

    sudo apt update
    sudo apt install fail2ban -y

    On RHEL-based systems, you'll need EPEL first:

    sudo dnf install epel-release -y
    sudo dnf install fail2ban -y

    Once installed, do not edit

    /etc/fail2ban/jail.conf
    directly. This is the single most common mistake I see, and it's worth repeating: that file gets overwritten on package updates, and any custom rules you put there will silently vanish the next time Fail2Ban is upgraded. Fail2Ban is designed around a layered config system specifically to avoid this, so use it.

    Create a local override file instead:

    sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
    sudo nano /etc/fail2ban/jail.local

    Inside

    jail.local
    , the file is organized into a
    [DEFAULT]
    section that applies globally, followed by per-service jail sections like
    [sshd]
    . The defaults ship fairly conservative, so the first thing I adjust is the ban behavior itself. Find (or add) these under
    [DEFAULT]
    :

    [DEFAULT]
    bantime  = 1h
    findtime = 10m
    maxretry = 5
    backend  = systemd
    ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8

    bantime
    controls how long an IP stays blocked,
    findtime
    is the window in which failures are counted, and
    maxretry
    is how many failures within that window trigger a ban. I set
    ignoreip
    to include our internal management network (10.0.0.0/8 in this example) so our own monitoring tools and jump hosts never get caught in the net. Add your admin workstation's static IP here too if you have one.

    Next, enable the SSH jail explicitly. Even though it's often enabled by default, I like to be explicit rather than trust defaults I haven't read:

    [sshd]
    enabled  = true
    port     = ssh
    filter   = sshd
    logpath  = %(sshd_log)s
    maxretry = 4
    bantime  = 3600

    If you've moved SSH off port 22, which I'd recommend doing anyway as a baseline hardening step, make sure the

    port
    line reflects that, either as a number or a named service from
    /etc/services
    .

    After editing, restart the service and check that it actually started without errors, since a typo in jail.local will fail silently in ways that only show up when you check status:

    sudo systemctl restart fail2ban
    sudo systemctl status fail2ban

    Full Configuration Example

    Here is a complete

    jail.local
    I've used on production hosts, covering SSH and a couple of other commonly exposed services. Adjust the paths and ports to match what's actually running on sw-infrarunbook-01.

    [DEFAULT]
    # Global settings applied to all jails unless overridden
    bantime  = 1h
    findtime = 10m
    maxretry = 5
    backend  = systemd
    ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8
    banaction = iptables-multiport
    destemail = infrarunbook-admin@solvethenetwork.com
    sender    = fail2ban@solvethenetwork.com
    mta       = sendmail
    action    = %(action_mwl)s
    
    [sshd]
    enabled  = true
    port     = ssh
    filter   = sshd
    logpath  = %(sshd_log)s
    maxretry = 4
    bantime  = 3600
    findtime = 600
    
    [sshd-aggressive]
    enabled  = false
    filter   = sshd
    logpath  = %(sshd_log)s
    maxretry = 2
    bantime  = 86400
    findtime = 300
    
    [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
    
    [recidive]
    enabled  = true
    filter   = recidive
    logpath  = /var/log/fail2ban.log
    action   = %(action_mwl)s
    bantime  = 604800
    findtime = 86400
    maxretry = 3

    A few notes on choices in that file. I keep a

    [sshd-aggressive]
    jail defined but disabled by default, so it's there to flip on quickly during an active attack wave without having to write it from scratch under pressure. The
    [recidive]
    jail is one people often skip, but it's genuinely useful: it watches Fail2Ban's own log for IPs that keep getting banned repeatedly across different jails, and escalates them to a week-long ban. In my experience this catches the more persistent scanners that time their retries to just outlast a one-hour ban.

    The

    destemail
    and
    action = %(action_mwl)s
    combination sends an email with the log lines whenever a ban happens. This is optional and adds mail server dependency, so if you don't have local mail delivery configured, drop it back to
    action = %(action_)s
    to just ban without notification, which is simpler and has fewer moving parts to break.

    Verification Steps

    Don't just assume it's working because the service started. Verify it properly, because a jail that's "enabled" on paper but pointed at the wrong log path will never ban anyone.

    First, check that fail2ban-client sees your jails:

    sudo fail2ban-client status

    You should see a list like

    Jail list: sshd, nginx-http-auth, recidive
    . If a jail you enabled is missing from that list, check
    journalctl -u fail2ban
    for the parsing error that kept it from loading.

    Next, check the details of a specific jail:

    sudo fail2ban-client status sshd

    This shows currently failed attempts, total banned IPs, and the actual ban list. To confirm the filter regex is actually matching your log format, use fail2ban-regex against the live log before you trust it in production:

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

    This dry-runs the filter against real log lines and tells you how many matched. If it reports zero matches on a server you know has failed logins, your log format doesn't match what the filter expects, which is common when logging is customized or when using a non-standard SSH daemon build.

    Finally, test an actual ban safely. From a separate machine (not your primary access point), deliberately fail an SSH login a few times against your test IP allowance, then check:

    sudo fail2ban-client status sshd
    sudo iptables -L -n | grep 

    You should see the IP listed in both the jail's banned list and the actual firewall rule chain. If it shows in Fail2Ban's status but not in iptables, your

    banaction
    is misconfigured or your firewall backend doesn't match what Fail2Ban expects.

    Common Mistakes

    The mistake I see most often, by a wide margin, is editing

    jail.conf
    instead of
    jail.local
    . I mentioned it above, but it deserves repeating because it's the one that causes people to lose their custom configuration silently, sometimes months later during a routine package update, with no obvious link between the update and the sudden loss of protection.

    Second is forgetting to whitelist your own management IPs in

    ignoreip
    . I've had colleagues set
    maxretry = 3
    to be aggressive, then get locked out themselves after a string of typos in their own password during a late-night session. It's not fun explaining to a client why their access got blocked by their own security tool.

    Third is mismatched log paths, particularly on systems that have switched to journald-only logging without a traditional

    /var/log/auth.log
    file. If
    logpath
    points to a file that doesn't exist or is empty, the jail loads without error but simply never sees any failures. Switching
    backend = systemd
    and removing an explicit
    logpath
    for jails that support journal mode fixes this.

    Fourth, people assume a ban is permanent when it's not.

    bantime
    of 1 hour means exactly that. Persistent attackers cycle back after the ban expires and get banned again, which usually stays fine because it's the same net effect, but if you want a genuinely permanent block for known bad actors, use
    bantime = -1
    in a dedicated jail, or better yet, feed those IPs into a separate blocklist maintained outside Fail2Ban entirely.

    Last, I've seen people configure Fail2Ban and then never look at it again. Attack patterns change, log formats change when you upgrade services, and filters written for one version of a daemon's log format sometimes stop matching after an upgrade changes the message wording. Check

    fail2ban-client status sshd
    every so often, not because you expect it to be broken, but because a quiet Fail2Ban with zero bans over months on an internet-facing SSH port is itself worth investigating rather than celebrating.

    Fail2Ban isn't a replacement for good authentication hardening, key-based SSH, disabling root login, and a properly configured firewall still matter more. But as a layer that quietly absorbs the constant background noise of automated brute-force attempts, it's one of the highest-value, lowest-effort tools you can add to a server, and it's worth setting up correctly the first time rather than patching it together under pressure during an actual incident.

    Frequently Asked Questions

    Will Fail2Ban block legitimate users who mistype their password a few times?

    It can, which is why maxretry and findtime need tuning to your environment. A maxretry of 4-5 within a 10-minute window is a reasonable balance that tolerates a couple of typos without leaving the door open to brute-force attempts.

    Does Fail2Ban work with nftables instead of iptables?

    Yes. Set banaction to nftables-multiport (or the appropriate nftables action in /etc/fail2ban/action.d/) instead of iptables-multiport, and confirm your distro's Fail2Ban package includes the nftables action files.

    How do I permanently unban an IP address?

    Use sudo fail2ban-client set <jail> unbanip <ip>, replacing <jail> with the relevant jail name like sshd. This removes the firewall rule immediately without waiting for bantime to expire.

    Can Fail2Ban protect services other than SSH?

    Yes, it ships with filters for many common services including nginx, Apache, Postfix, and Dovecot, and you can write custom filters for any application that logs failed authentication attempts to a file or journal.

    What happens if I lock myself out with a bad Fail2Ban rule?

    If you still have an active session, run fail2ban-client unbanip on your own IP or stop the service with systemctl stop fail2ban. If you're fully locked out, you'll need out-of-band console access from your hosting provider to fix the configuration.

    Related Articles