Fail2ban and Intrusion Prevention

Fail2ban and Intrusion Prevention

Bitnesia Aug 29, 2026 1 ID

Chapter 31 concludes with an intentional blind spot left open: no matter how neat a firewall is, it can only reject what we have explicitly forbidden, but it cannot recognize recurring attack patterns on its own. Section 31.6.3 even proved this blind spot itself through a manual grep command line to count the source IPs that appeared most frequently in the block log, a method honestly acknowledged as reactive and not scalable for a server receiving thousands of connection attempts every day. This chapter closes that blind spot through Fail2ban, the third layer in the defense in depth roadmap that we arranged in Section 30.1.2: a tool that automates the exact detection and blocking process mentioned earlier, reading logs continuously, recognizing recurring failed attempt patterns, and then directly instructing the firewall to block the source IP without requiring a Sysadmin to stare at the logs every day.

We will start from the concept of log-based intrusion prevention and the three core components that form how Fail2ban works, followed by installation and a safe configuration pattern via jail.local, aligning banaction with nftables which has been our firewall foundation since Chapter 31, enabling jails for SSH on our custom port as well as Nginx/Apache, and concluding with how to check the status and unban accidentally targeted IPs.

32.1 Concept of Log-Based Intrusion Prevention

Before diving into installation, it is good practice to first conceptually understand Fail2ban's operational model, as nearly all troubleshooting in subsequent sections leads back to the three core terms explained here.

32.1.1 From Manual Grep to Automated Detection: What Is Fail2ban

Fail2ban is a host-based intrusion prevention system that works by continuously monitoring log files or journal, matching each new line against specific patterns, and instructing the firewall to block the source IP once an IP exceeds the failed attempt threshold within a specific timeframe. Its operational model is simple yet effective: an attacker running automated SSH brute force, whose potential we discussed in Section 31.3.2, ultimately leaves traces in the form of recurring Failed password lines in the authentication log. Fail2ban reads these traces, counts how many failures occurred from a single IP within a specific period, and once the threshold is exceeded, that IP is immediately blocked for a set duration without manual intervention.

Its difference from UFW rate limiting, which we configured in Section 31.3.2, lies in its data source. UFW rate limiting operates purely on connection patterns at the network level, regardless of application log content, making it unable to distinguish failed login attempts from successful ones. Fail2ban reads the application log content itself, allowing it to recognize genuine authentication failures with much higher precision, while also extending to any service with structured logs, not limited to SSH.

32.1.2 Anatomy of Fail2ban: Filter, Jail, and Action

A Fail2ban configuration always consists of three interconnected components. A Filter is a collection of regex patterns defining which log lines are counted as failed attempts, saved as a separate file per service in /etc/fail2ban/filter.d/, such as sshd.conf to recognize Failed password lines in SSH logs. A Jail is a configuration unit that binds a filter with a log source, relevant ports, and threshold parameters such as the maximum number of attempts, defined in jail.conf and its derivatives. An Action consists of concrete commands executed when an IP is officially banned, typically nftables or iptables commands to reject traffic from that IP, defined in /etc/fail2ban/action.d/. All three complement one another; a jail cannot function without a clear filter and an action that knows how to block.

ComponentFunctionConfiguration Location
FilterRecognizes log lines considered failed attempts using regex patterns/etc/fail2ban/filter.d/*.conf
JailBinds filter, log source, port, and thresholds (maxretry, findtime, bantime) for a single servicejail.conf, jail.local, jail.d/*.conf
ActionExecutes real blocking commands on the firewall when an IP is banned/etc/fail2ban/action.d/*.conf

In practice, as Sysadmins we rarely write filters or actions from scratch. Fail2ban provides dozens of ready-to-use filters for common services, including SSH, Nginx, and Apache, which are the focus of Section 32.4. Our task mostly involves enabling relevant jails and adjusting their parameters, precisely what we will do starting in the next section.

32.2 Installation and Basic Configuration of Fail2ban

This section covers installing Fail2ban and building a safe configuration layer resilient against changes lost during package upgrades, before moving into more specific adjustments in subsequent sections.

32.2.1 Package Installation and Service Verification

The fail2ban package in Ubuntu Server 26.04 resides in the universe repository, which is active by default since installation as discussed in Section 6.2, requiring no additional steps to enable any repository.

Practical Steps

  1. Install the fail2ban package.
    sudo apt install fail2ban
  2. This package includes a systemd unit that is automatically enabled and started upon installation completion. Confirm its status.
    sudo systemctl status fail2ban
  3. View active jails without any additional configuration.
    sudo fail2ban-client status

Verification and Troubleshooting

  • A healthy systemctl status output displays active (running), and running fail2ban-client status on a fresh installation typically shows a single jail named sshd without any manual setup. This occurs because the Ubuntu package includes the file /etc/fail2ban/jail.d/defaults-debian.conf, which enables the sshd jail by default, providing basic protection against SSH brute force right out of the box.
  • This default sshd jail still uses the default port configuration, which may not match our custom SSH port from Section 3.2.4, an issue we resolve in Section 32.4.1. Do not stop at just having an active jail; ensure its port is correct as well.
  • Fail2ban keeps its own activity logs, separate from the service logs it monitors, which is useful for troubleshooting Fail2ban itself.
    sudo tail -f /var/log/fail2ban.log

32.2.2 jail.local: A Safe Customization Layer

The exact same pattern used with sshd_config in Chapter 3 and UFW's applications.d in Section 31.3.3 applies here. The jail.conf file is a package-supplied file that must not be edited directly, as it will be overwritten whenever the fail2ban package upgrades. Fail2ban reads configurations in a fixed order: jail.conf, followed by all files in jail.d/*.conf alphabetically, then jail.local, and finally jail.d/*.local, with values read later overriding identical values from earlier files. Because jail.local is read after all jail.d/*.conf files, including defaults-debian.conf which enabled the sshd jail earlier, this file is the safest place to house all our customizations.

Practical Steps

  1. Create the jail.local file, which is initially empty as it is not provided by the package.
    sudo nano /etc/fail2ban/jail.local
  2. Populate it with the following [DEFAULT] block. The office subnet 192.168.1.0/24 is intentionally added to ignoreip alongside default localhost, ensuring Sysadmins and Developers who occasionally mistype passwords from the internal network are never banned.
    [DEFAULT]
    ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24
    bantime  = 1h
    findtime = 10m
    maxretry = 5
  3. Reload the configuration without dropping active connections.
    sudo fail2ban-client reload

Verification and Troubleshooting

  • Confirm that the new parameters are read by the sshd jail.
    sudo fail2ban-client get sshd bantime
    sudo fail2ban-client get sshd maxretry
  • The default bantime value in Fail2ban is only 10 minutes, short enough that automated attackers can retry as soon as the ban expires. Increasing it to 1 hour as shown above is standard field practice. For even more aggressive defense, Fail2ban also offers bantime.increment = true, which multiplies the ban duration each time the same IP gets repeatedly banned, an option worth considering for servers frequently targeted by automated scanning.

32.3 banaction: Aligning Fail2ban with nftables

This section addresses an easily overlooked technical detail that has a real impact on our server's firewall consistency, given that all discussions in Chapter 31 emphasized nftables as the modern foundation behind UFW.

32.3.1 Default iptables-multiport and Why It Needs Adjustment

The banaction parameter defines the firewall mechanism Fail2ban invokes when blocking an IP. Although Ubuntu Server 26.04 runs entirely on top of nftables as discussed in Section 31.1.2, the default banaction value in Fail2ban's jail.conf up to the writing of this series remains iptables-multiport, not nftables. This choice is not incorrect or broken, because the iptables binary on our server is actually iptables-nft as demonstrated in Section 31.1.2, meaning rules written by Fail2ban via iptables-multiport still end up as nftables entries in the kernel through automated translation.

Nevertheless, using native nftables banaction remains more consistent for a server built entirely on the nftables mental model since Chapter 31. Ban rules written via the nftables action appear directly when running nft list ruleset, the exact same verification command used in Section 31.4, without needing to switch to iptables -L. This action also creates a distinct table named f2b-table with a chain hooked at the input point using priority -1, a number deliberately lower than priority 0 typically used by UFW chains or native rulesets in Section 31.4.1. This means Fail2ban ban rules are evaluated by the kernel prior to UFW rules, ensuring banned IPs are rejected even if their destination port happens to be allowed by UFW.

32.3.2 Switching banaction to Native nftables

Practical Steps

  1. Add the banaction line to the [DEFAULT] block in jail.local created in Section 32.2.2.
    sudo nano /etc/fail2ban/jail.local
    Add the following two lines inside the existing [DEFAULT] block.
    banaction = nftables
    banaction_allports = nftables[type=allports]
  2. Reload Fail2ban so the new action applies across all active jails.
    sudo fail2ban-client reload

Verification and Troubleshooting

  • Ensure the f2b-table table is created in the kernel even if no IPs are currently banned.
    sudo nft list table inet f2b-table
  • If the command above returns the error No such file or directory, this table is only created when a jail actually starts and executes its action for the first time; ensure fail2ban-client status shows running jails, then retry after performing the ban test step in Section 32.5.3.
  • The fail2ban package requires either nftables or iptables installed as a dependency, and because both have been available since Chapter 31, no additional packages are needed to enable this banaction.

32.4 Enabling Jails for SSH, Nginx, and Apache

The configuration foundation is ready. This section moves into specific jails most relevant to our server: SSH, which is active since Chapter 3, and the web servers installed in Chapter 12 or Chapter 13.

32.4.1 SSH Jail on a Custom Port

The default sshd jail in jail.conf uses port = ssh, an alias translated by Fail2ban via /etc/services to standard port 22. This is the exact same trap as the ufw limit ssh alias warned about in Section 31.3.2: our server runs SSH on port 2222 since Section 3.2.4, not port 22. If this port is not adjusted, ban rules generated by Fail2ban will only apply to port 22, which is no longer listening on our server, allowing attackers targeting port 2222 to go unblocked even if the sshd jail appears active and healthy.

Practical Steps

  1. Add the [sshd] block to jail.local, separate from the existing [DEFAULT] block.
    sudo nano /etc/fail2ban/jail.local
    Add the following block at the bottom of the file, on the same level as [DEFAULT], not inside it.
    [sshd]
    enabled = true
    port    = 2222
  2. Reload Fail2ban.
    sudo fail2ban-client reload

Verification and Troubleshooting

  • Confirm the registered port on the jail is indeed 2222, not 22.
    sudo fail2ban-client get sshd port
  • The sshd jail in Ubuntu Server 26.04 uses backend = systemd by default, meaning Fail2ban reads SSH logs directly from the journal via python3-systemd bindings, rather than from /var/log/auth.log. These bindings are automatically installed as a package dependency for fail2ban, requiring no additional installation.

32.4.2 Nginx and Apache Jails for HTTP Basic Auth

Fail2ban provides ready-to-use jails for both web servers discussed: nginx-http-auth for Nginx from Chapter 12 and apache-auth for Apache from Chapter 13. Choose the appropriate one corresponding to the web server running on your server.

Practical Steps

  1. Add the relevant jail block based on the active web server to jail.local. For Nginx:
    [nginx-http-auth]
    enabled = true
    Or for Apache:
    [apache-auth]
    enabled = true
  2. Reload Fail2ban.
    sudo fail2ban-client reload

Verification and Troubleshooting

  • Verify that the new jail is listed and running.
    sudo fail2ban-client status
  • An important honest note regarding this jail's limitations: nginx-http-auth and apache-auth only recognize HTTP Basic Auth failures, built-in web server authentication features used via directives like auth_basic in Nginx or the mod_auth_basic module in Apache. Static sites or simple reverse proxies from Chapters 12, 13, and 16 that do not use Basic Auth protection will never trigger this jail, as there are no authentication failure log lines to match. Protecting custom web application login forms, such as WordPress login pages or Node.js applications from Chapter 15, requires custom filters that specifically read those application logs, a topic outside the scope of this series.

32.5 Viewing Status and Unbanning IPs

A configuration without verification and correction methods is as risky as a firewall without logging discussed in Section 31.6. This concluding section completes daily Fail2ban operations: checking who is currently banned and unbanning mistakenly targeted IPs.

32.5.1 Checking Jail Status and Banned IP Lists

Practical Steps

  1. View a summary of all active jails.
    sudo fail2ban-client status
  2. View details for a specific jail, including currently banned IPs.
    sudo fail2ban-client status sshd

Verification and Troubleshooting

  • A healthy status sshd output displays current and historical failed attempt counts under the Filter section, followed by currently banned IPs and total bans under the Actions section, looking similar to the following:
    Status for the jail: sshd
    |- Filter
    |  |- Currently failed: 1
    |  |- Total failed:     14
    |  `- File list:
    `- Actions
       |- Currently banned: 1
       |- Total banned:     1
       `- Banned IP list:   203.0.113.45
  • An empty File list line for the sshd jail is normal behavior in Ubuntu Server 26.04, not a sign of failure. Because this jail uses backend = systemd as discussed in Section 32.4.1, Fail2ban reads from journald rather than log files, meaning no file path is displayed there.

32.5.2 Unbanning an IP Manually

An accidentally banned IP, such as an office IP that was not yet added to ignoreip in Section 32.2.2, must be unbanned without waiting for the bantime to expire.

Practical Steps

  1. Unban a single IP from a specific jail, replacing the example IP below with the actual IP shown in status sshd.
    sudo fail2ban-client set sshd unbanip 203.0.113.45
  2. If the same IP happens to be banned across multiple jails simultaneously, such as sshd and nginx-http-auth together, unban it from all jails without specifying jail names individually.
    sudo fail2ban-client unban 203.0.113.45

Verification and Troubleshooting

  • Confirm the IP is no longer listed under Banned IP list.
    sudo fail2ban-client status sshd
  • The unban command only releases the ban at the firewall level for the current session. The same IP can still be automatically banned again once it exceeds maxretry, so ensure the root cause is addressed, such as adding the IP to ignoreip if it originates from a trusted source, rather than repeatedly unbanning it whenever the incident recurs.

32.5.3 Testing Detection Without the Risk of Locking Yourself Out

Testing Fail2ban by intentionally entering incorrect passwords repeatedly from your active SSH session carries a real risk: once maxretry is reached, your own session gets blocked by the firewall. Fail2ban provides a dedicated fail2ban-regex tool to test filters against existing log entries without executing any actual ban actions, providing a much safer way to verify filter operations.

Practical Steps

  1. Test the sshd filter against existing journal content without triggering a real ban.
    sudo fail2ban-regex systemd-journal sshd

Verification and Troubleshooting

  • The output displays a summary of log lines matching the failregex in the sshd filter, useful for ensuring the filter properly recognizes SSH log formats on your server before relying on it in production. If matched counts remain zero despite recorded failed login attempts in journald, the filter likely does not match the log format produced by the installed openssh-server version, and should be reported as a bug to the Fail2ban GitHub project.
  • For end-to-end testing that involves real bans, perform tests from another network outside the 192.168.1.0/24 subnet included in ignoreip, or prepare a fallback access route such as console access from Section 3.4 before testing, preventing yourself from getting locked out of the server.

At this point, we have completed the third layer of defense in depth from Section 30.1.2: understanding how Fail2ban works through its three core components (filter, jail, and action), installing it with a safe jail.local configuration pattern resistant to package upgrades, aligning banaction with native nftables for consistency with Chapter 31 firewall foundations, enabling jails for SSH on custom ports as well as Nginx or Apache, through to daily operations for checking status and unbanning mistargeted IPs. Fail2ban still has honest limitations that must be acknowledged: it only reacts to patterns recognized by its filters, cannot stop slow-moving attackers operating below the maxretry threshold, and once an attacker successfully gains entry through another vulnerability, Fail2ban can no longer limit what that process can execute inside the system. Chapter 33 moves into the fourth layer of our defense in depth roadmap, AppArmor, which operates under a different premise: not preventing attackers from entering, but limiting damage a process can inflict even when running as root, while securing vulnerabilities that cannot easily be detected through simple log patterns.