Why self-host Fail2ban on a VPS
Fail2ban is one of the simplest and most widespread protection tools for an exposed server: it continuously reads your logs, applies regular expressions (the filters) to spot intrusion attempts, and bans the offending IP by adding it to nftables or iptables for a configurable duration. On a VPS, it is a first line of defense to put in place before the rest, because it depends on no external service, uses little and protects SSH against bruteforce as soon as the jail is enabled. Beyond SSH, you can extend it to Nginx (authentication attempts, URL scans), Postfix, WordPress or any service that writes logs. Everything runs locally on your machine: no data leaves, no third-party service is involved. It is a self-contained, easy-to-audit solution, provided you check on it from time to time, as we will see below.
The concrete benefits of this template
- Automatic banning — IPs that bruteforce SSH are banned as soon as the
maxretrythreshold is reached within thefindtimewindow. - Filters for many services — SSH, Nginx, Apache, Postfix, WordPress, and custom filters written in
filter.d. - Minimal footprint — it runs without trouble on the smallest of VPSes.
- Progressive banning (repeat offense) — the more an IP reoffends, the longer the ban.
- Email notifications — possible on each ban, with the
action_mworaction_mwlactions fromjail.conf. - Self-contained and local — no network dependency day to day, simple to audit and version.
Quantified requirements to host it
Fail2ban is frugal: a VPS with 1 vCPU and 512 MB of RAM is enough to run it, with no perceptible impact (an editorial recommendation, to be measured against your own load). On the software side, the upstream README for version 1.1.1 asks for Python 3.5 or later. Two components are optional: pyinotify 0.8.3 or later to watch files (Linux kernel 2.6.13 or later), and systemd 204 or later with the python-systemd package to read the journal. Fail2ban installs directly on the system (no need for Docker) and requires root access, a firewall backend (nftables or iptables) and access to the logs of the services to protect: the systemd journal or /var/log/auth.log for SSH depending on the distribution, and the Nginx access and error logs. It keeps its state in a SQLite database, /var/lib/fail2ban/fail2ban.sqlite3 by default. No additional domain or port is needed. The golden rule: fill in ignoreip with your IP before enabling banning, so you don't lock yourself out of your own server.
Install and configure Fail2ban on your VPS
Install Fail2ban
On Debian or Ubuntu:
sudo apt update && sudo apt install fail2ban. The service starts automatically. Check withsudo systemctl status fail2ban.Create a jail.local
Never modify
jail.confdirectly. Write your settings in/etc/fail2ban/jail.local, which overrides them: that's where you definebantime,findtime,maxretryand above allignoreip = 127.0.0.1/8 ::1 VOTRE_IPto protect yourself from a self-ban. The defaults injail.confarebantime = 10m,findtime = 10mandmaxretry = 5.Enable SSH protection
All jails are disabled by default in
jail.conf: explicitly enable[sshd]injail.localwithenabled = true, for examplemaxretry = 3andbantime = 1h. After 3 failures within thefindtimewindow, the IP is banned for one hour at the firewall level.Add a jail for Nginx
Enable the
[nginx-http-auth]and[nginx-botsearch]jails, provided byjail.conf, and make surelogpathpoints to your Nginx logs. This blocks repeated authentication attempts and malicious URL scans.Test the configuration, then reload it
Check the syntax first:
sudo fail2ban-client -t. Then apply it:sudo fail2ban-client reload(orsudo systemctl restart fail2ban).sudo fail2ban-client statuslists the active jails andsudo fail2ban-client status sshdshows the currently banned IPs.Test a ban and the unban
Check the filter against your own log with
sudo fail2ban-regex systemd-journal sshd. To simulate a ban without attacking anyone:sudo fail2ban-client set sshd banip 203.0.113.10, then readsudo fail2ban-client status sshd. To release a legitimate IP blocked by mistake:sudo fail2ban-client set sshd unbanip X.X.X.X.Logging in for the first time
There is no web interface and no account to create: fail2ban is a system daemon that, with the template's setting, bans IP addresses after 4 failed SSH attempts within 10 minutes. Over SSH:
fail2ban-client status sshd(banned IPs),fail2ban-client set sshd unbanip <IP>(to unban). Adjust these thresholds injail.local.
ignoreip, ban duration and repeat offenders
ignoreip accepts IP addresses, CIDR masks or DNS host names, separated by spaces or commas. By default ignoreself = true: the server's own local addresses are never banned, but your office or home IP is not covered by that. If it changes often, declare a dynamic host name or use ignorecommand, an external command that receives the IP and answers whether it should be ignored. Also keep a fallback access that does not depend on SSH, such as your VPS provider's console: a self-ban can then be fixed without outside help. The duration is set with bantime, in seconds or with the suffixes m, h, d, w, mo and y. It acts as the initial duration when progressive banning is enabled. For repeat offenders, jail.conf also ships a recidive jail, which reads fail2ban's own log.
Progressive banning: formula, cap and memory
Enable progressive banning in jail.local with bantime.increment = true. The default formula multiplies bantime by 1, 2, 4, 8, 16 and so on at each reoffense; bantime.factor (1 by default) is a common multiplier, and bantime.maxtime caps the duration in seconds, for example bantime.maxtime = 604800 for seven days. One point is often missed: fail2ban finds earlier bans in its database, which is purged according to dbpurgeage (one day by default in fail2ban.conf). For widely spaced reoffenses to be found, raise it; the comment on the recidive jail in jail.conf cites 648000 seconds. Pair all this with disabling password authentication (SSH keys only): Fail2ban then becomes a safety net rather than your only rampart.
Choosing the backend: auto, pyinotify, polling or systemd
The backend is the way a jail reads its logs. The default in jail.conf is backend = auto, which each jail can override. On Debian and Ubuntu, the paths-debian.conf file in the upstream 1.1.1 archive sets sshd_backend = systemd: sshd is then read from the systemd journal, not from /var/log/auth.log. Read sudo fail2ban-client status sshd to learn what is really being watched: the File list line indicates files, the Journal matches line indicates the journal. A distribution may change these values, so assume nothing: observe. Changing backend is done jail by jail, in jail.local, followed by sudo fail2ban-client reload.
The four Fail2ban backends
Scroll the table
| Backend | What it watches | Worth knowing |
|---|---|---|
| auto | Files (pyinotify, then polling); switches to systemd if no `logpath` file exists and the filter defines `journalmatch` | Default value in `jail.conf` |
| pyinotify | Files, through kernel inotify events | Linux only, Python pyinotify module required; better suited to files than polling according to the documentation |
| polling | Files, by re-reading at regular intervals | No external library, the simplest fallback |
| systemd | The systemd journal, through python-systemd | `logpath` is not valid; the filter goes through `journalmatch`; `journalflags` and `rotated` settings |
Checking that the jail is still watching
A started jail is not necessarily a jail that reads. Reported, not confirmed by the maintainers: in fail2ban issue 4250, opened in October 2026, an administrator on Debian with fail2ban 1.1.1 describes an sshd jail on the systemd backend that stops banning one to two days after start, while the Nginx and Postfix jails keep working. Two other administrators, both on FreeBSD according to their messages, report a similar symptom with the pyinotify backend; one says things became stable after switching to polling. A maintainer pointed out that the original issue was about the systemd backend and that the two symptoms may not be related. A last participant, on Arch with the systemd backend, describes the same stop after a journal rotation and puts forward a hypothesis about the rotated setting: it remains unconfirmed. The cause is not established to date, and nothing allows generalizing to your server. The check itself takes a minute.
Check in four steps that the jail still reads
Read the jail status
sudo fail2ban-client status sshdshowsCurrently failedandTotal failedfor the filter,File listorJournal matchesfor the source, thenCurrently banned,Total bannedandBanned IP listfor the action. NoteTotal failed.Compare with the log
Count the real failures over the same period:
sudo journalctl _COMM=sshd --since "1 hour ago" | grep -c "Failed password", orsudo grep -c "Failed password" /var/log/auth.logif the jail reads that file. Then readTotal failedagain: it should have grown. Compare the trend, not exact equality, because IPs inignoreipdo not count.Read fail2ban's own log
sudo tail -n 50 /var/log/fail2ban.log: a healthy jail writesFoundlines followed byBan. Several days of loneUnbanlines, while the authentication log shows attempts, point to a blind jail.Restart and change backend if needed
sudo systemctl restart fail2banrestarts the reading; the issue describes normal operation for one to two days after each start. For a file-based jail,backend = pollingis the simplest fallback. Schedule this check (cron or a monitoring tool) rather than relying on the absence of alerts.
Troubleshooting: the real error messages
These messages come from the Fail2ban 1.1.1 source code (clients and configuration readers); the default logging level is INFO. For each case the starting command is the same: sudo fail2ban-client -t tests the configuration, sudo journalctl -u fail2ban gives the beginning of the story.
Common error messages and fixes
Have not found any log file for 'sshd' jail.— the jail'slogpathpoints to no existing file and startup fails, unlessskip_if_nologs = true. With theautobackend, a jail whose filter definesjournalmatchswitches to systemd instead. Fix the path or setbackend = systemd.No file(s) found for glob /var/log/nginx/error.log— alogpathpattern matches nothing. Check the real location of the Nginx logs withls /var/log/nginx.Unable to read the filter 'nginx-xyz'— the name given tofilter =does not exist infilter.d. Fix the typo or add your filter file.Failed to access socket path: /var/run/fail2ban/fail2ban.sock. Is fail2ban running?— the client cannot find the server socket. The service is stopped or failing:sudo systemctl status fail2ban, then its log.Permission denied to socket: ... (you must be root)— thefail2ban-clientcommand was run without sufficient rights. Run it again withsudo.Unable to contact server. Is it running?— the socket exists but the server does not answer. Restart the service, then read its startup log.[sshd] Journal reader seems to be offline, reopen journal— fail2ban detected that its reading of the systemd journal had stopped (rotation) and reopens it. This message is at theMSGlevel, belowINFO: its absence from a normal log proves nothing, hence the check in the previous section.Error reading line from systemd journal— a read error on the systemd journal, followed by the system cause. Check the service's rights and the journal state withjournalctl --verify.
nftables or iptables, the VPS firewall and backup
The ban action is chosen with banaction. jail.conf offers iptables-multiport as a commented example; the paths-debian.conf file in the 1.1.1 archive sets banaction = nftables and banaction_allports = nftables[type=allports]. The nftables action creates by default the f2b-table table (inet family) and the f2b-chain chain, hooked on input with priority -1; sudo nft list table inet f2b-table shows the banned IPs. Pick a single family of actions, the same as the rest of your firewall, otherwise you look for your rules in the wrong table. The VPS firewall acts upstream: it filters what reaches the machine, for example by limiting SSH to your addresses, while Fail2ban reacts to what the logs say. The two complement each other, but keep ignoreip consistent with your rules. Behind a proxy or a CDN, the logs show the proxy's IP: restore the real IP before banning, otherwise you ban the proxy. For backup, archive the configuration (sudo tar czf fail2ban-config.tar.gz /etc/fail2ban) and the output of sudo fail2ban-client --dp, which documents the effective configuration. The /var/lib/fail2ban/fail2ban.sqlite3 database holds the memory of past bans.
The official documentation
For advanced configuration, options specific to the tool, and version changes, refer to the official Fail2Ban documentation. This guide covers deployment on a ServOrbit VPS; the vendor's documentation remains the reference for fine-tuning.