How To Protect SSH From Brute-Force Attacks With Fail2ban
Tricknowtech Team 7 min read
Goal
In this tutorial, you will install fail2ban, configure a jail that monitors SSH authentication failures, and verify it automatically bans IP addresses that exceed a failed-login threshold.
Prerequisites
A Linux server/VPS you manage via SSH
A non-root user with sudo privileges
Ideally, a second machine or session you can use for testing without risking your main connection
Let an AI agent do this for you
Copy a ready-made prompt for an AI coding assistant with terminal access to your server (Claude Code, Cursor, or similar) — it can carry out the steps below for you. Review what it plans to run before it executes anything.
Fail2ban is a log-parsing intrusion prevention tool. It tails log files, matches failed-login patterns against them, and when an IP address crosses a threshold of failures within a time window, it adds a temporary firewall rule to block that IP outright. Applied to SSH, it turns a slow drip of automated password-guessing attempts into a self-limiting nuisance instead of an ongoing risk.
Step 1 — Install fail2ban
Install fail2ban with apt. No third-party repository or manual download is needed.
bash
sudo apt update
sudo apt install -y fail2ban
If your system uses a different package manager (dnf, pacman, zypper, etc.), install the fail2ban package with that instead — the rest of this tutorial is unaffected by which package manager you used. This installs fail2ban with a default configuration and sets it up as a systemd service, which you'll interact with via systemctl.
The main defaults file, /etc/fail2ban/jail.conf, ships with the package and gets overwritten every time fail2ban is updated. Never edit it directly — any changes you make there will silently vanish on the next update.
Instead, fail2ban is designed around a companion file: /etc/fail2ban/jail.local. It is not shipped by the package, so the package manager never touches it. Fail2ban reads jail.conf first and then jail.local, and any setting defined in jail.local overrides the same setting from jail.conf. This is the standard, update-safe way to configure jails.
Step 3 — Create a jail.local configuration for SSH
Create /etc/fail2ban/jail.local with a [DEFAULT] section for global settings and an [sshd] section that enables the SSH jail specifically:
findtime — the rolling time window in which failed attempts are counted. Here, 10 minutes.
maxretry — how many failures within that window trigger a ban. Here, 5 failed attempts in 10 minutes.
bantime — how long a triggered ban lasts. 1h is a reasonable starting point for a first pass; you can tighten or loosen it once you've watched real traffic.
Tricknowtech VPS Hosting
Dedicated KVM resources and full root access — deployed in under 60 seconds, no ticket required.
filter and logpath — which log-pattern definition and which log file/source to apply it to. The %(sshd_log)s and %(sshd_backend)s values are placeholders fail2ban resolves automatically based on your system's logging setup, so you normally don't need to hardcode a path yourself.
backend — how fail2ban reads the log (polling a file vs. reading systemd's journal). Left as the auto-resolved default here for the same reason.
For repeat offenders — an IP that gets banned, waits it out, and comes right back to guessing again — fail2ban also has a concept of a "recidive" jail that watches for addresses which keep triggering other jails and applies a much longer, or even permanent (bantime = -1), ban to them. Setting it up is a separate step beyond the scope of this tutorial; it's worth knowing the option exists.
Step 4 — Restart fail2ban and check its status
bash
sudo systemctl restart fail2ban
sudo systemctl status fail2ban
The status output should show active (running). If it shows failed instead, there is a syntax error in jail.local — check the output of journalctl -u fail2ban for the specific line it choked on.
Step 5 — Verify the sshd jail is active
bash
sudo fail2ban-client status sshd
This prints the jail's current state: how many failed attempts it has seen, how many IPs are currently banned, the total banned since the service started, and the list of currently banned addresses. Right after a restart, everything should read zero — that's expected; it means the jail is watching, not that anything is wrong.
Step 6 — List and unban banned IP addresses
Running fail2ban-client status sshd at any time shows the current ban list. To manually lift a ban on a specific address:
bash
sudo fail2ban-client set sshd unbanip 203.0.113.99
“This is the command to reach for if you accidentally lock yourself out — for example, by mistyping your own password enough times to trip maxretry. Run it from any session you still have access to (a web-based console, another logged-in shell, etc.), or ask a colleague with server access to run it for you.”
Step 7 — How this fits with disabling SSH password authentication
If you've already hardened SSH by setting PasswordAuthentication no in sshd_config (a step covered in a general server-hardening tutorial), password brute-forcing against SSH is already impossible — there's no password prompt left to guess against. Fail2ban's sshd jail is still worth running in that case, for two reasons. First, defense-in-depth: if password auth is ever re-enabled temporarily (for troubleshooting, for a new user, by mistake), you're not suddenly unprotected. Second, fail2ban isn't SSH-specific — the same mechanism extends to any other service with a log-based filter definition, including Nginx basic-auth endpoints, mail servers, and web application login forms, so having it running and understood now makes adding those jails later straightforward.
Step 8 — Test that fail2ban actually bans a brute-forcing IP
To confirm the jail works end-to-end, deliberately trigger it — but do this from a different machine than the one you use to manage the server, so you don't ban your own working connection.
bash
# From a SEPARATE machine, not your usual admin session:
ssh nonexistentuser@your-server-ip
# Enter any wrong password when prompted, repeat 5+ times
# (or attempt against a real username with a wrong password)
After exceeding maxretry within findtime, check the jail from your normal admin session on the server:
bash
sudo fail2ban-client status sshd
The test machine's IP should now appear in the banned IP list, and Currently banned / Total banned counts should have incremented. Further SSH connection attempts from that machine will time out or be refused until bantime expires (or until you run unbanip against it, as in Step 6).
Troubleshooting — fail2ban never bans anyone despite failed logins
If Currently failed and Total failed in fail2ban-client status sshd stay at zero no matter how many bad login attempts you make, the almost-always cause is that logpath/backend don't match where your system actually records SSH authentication failures. This varies slightly between systems: some log SSH authentication failures to a plain-text file at /var/log/auth.log, while others rely entirely on the systemd journal and have no auth.log file at all.
Check both places to see where failures are actually landing:
Whichever command shows your recent failed-password lines is the source fail2ban needs to read. Adjust logpath and/or backend in jail.local so they point at that source instead of relying on the auto-resolved default, then restart fail2ban and re-run the Step 8 test to confirm.