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.
| Component | Function | Configuration Location |
|---|---|---|
| Filter | Recognizes log lines considered failed attempts using regex patterns | /etc/fail2ban/filter.d/*.conf |
| Jail | Binds filter, log source, port, and thresholds (maxretry, findtime, bantime) for a single service | jail.conf, jail.local, jail.d/*.conf |
| Action | Executes 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
- Install the
fail2banpackage.sudo apt install fail2ban - This package includes a systemd unit that is automatically enabled and started upon installation completion. Confirm its status.
sudo systemctl status fail2ban - View active jails without any additional configuration.
sudo fail2ban-client status
Verification and Troubleshooting
- A healthy
systemctl statusoutput displaysactive (running), and runningfail2ban-client statuson a fresh installation typically shows a single jail namedsshdwithout any manual setup. This occurs because the Ubuntu package includes the file/etc/fail2ban/jail.d/defaults-debian.conf, which enables thesshdjail by default, providing basic protection against SSH brute force right out of the box. - This default
sshdjail 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
- Create the
jail.localfile, which is initially empty as it is not provided by the package.sudo nano /etc/fail2ban/jail.local - Populate it with the following
[DEFAULT]block. The office subnet192.168.1.0/24is intentionally added toignoreipalongside defaultlocalhost, 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 - Reload the configuration without dropping active connections.
sudo fail2ban-client reload
Verification and Troubleshooting
- Confirm that the new parameters are read by the
sshdjail.sudo fail2ban-client get sshd bantime sudo fail2ban-client get sshd maxretry - The default
bantimevalue 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 offersbantime.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
- Add the
banactionline to the[DEFAULT]block injail.localcreated in Section 32.2.2.
Add the following two lines inside the existingsudo nano /etc/fail2ban/jail.local[DEFAULT]block.banaction = nftables banaction_allports = nftables[type=allports] - Reload Fail2ban so the new action applies across all active jails.
sudo fail2ban-client reload
Verification and Troubleshooting
- Ensure the
f2b-tabletable 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; ensurefail2ban-client statusshows running jails, then retry after performing the ban test step in Section 32.5.3. - The
fail2banpackage requires eithernftablesoriptablesinstalled as a dependency, and because both have been available since Chapter 31, no additional packages are needed to enable thisbanaction.
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
- Add the
[sshd]block tojail.local, separate from the existing[DEFAULT]block.
Add the following block at the bottom of the file, on the same level assudo nano /etc/fail2ban/jail.local[DEFAULT], not inside it.[sshd] enabled = true port = 2222 - 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
sshdjail in Ubuntu Server 26.04 usesbackend = systemdby default, meaning Fail2ban reads SSH logs directly from the journal viapython3-systemdbindings, rather than from/var/log/auth.log. These bindings are automatically installed as a package dependency forfail2ban, 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
- Add the relevant jail block based on the active web server to
jail.local. For Nginx:
Or for Apache:[nginx-http-auth] enabled = true[apache-auth] enabled = true - 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-authandapache-authonly recognize HTTP Basic Auth failures, built-in web server authentication features used via directives likeauth_basicin Nginx or themod_auth_basicmodule 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
- View a summary of all active jails.
sudo fail2ban-client status - View details for a specific jail, including currently banned IPs.
sudo fail2ban-client status sshd
Verification and Troubleshooting
- A healthy
status sshdoutput displays current and historical failed attempt counts under theFiltersection, followed by currently banned IPs and total bans under theActionssection, 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 listline for thesshdjail is normal behavior in Ubuntu Server 26.04, not a sign of failure. Because this jail usesbackend = systemdas 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
- 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 - If the same IP happens to be banned across multiple jails simultaneously, such as
sshdandnginx-http-authtogether, 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
unbancommand only releases the ban at the firewall level for the current session. The same IP can still be automatically banned again once it exceedsmaxretry, so ensure the root cause is addressed, such as adding the IP toignoreipif 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
- Test the
sshdfilter 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
failregexin thesshdfilter, 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 installedopenssh-serverversion, 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/24subnet included inignoreip, 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.

