How to Stop SSH Brute-Force Attacks (5 Layers: Keys-Only, fail2ban, sshd Limits, Firewall, and 2FA)

CodexFuse Team · 2026-11-04T08:20:32.460Z · 12 min read min read

If you run any server with SSH exposed to the internet, bots are already knocking on its door. LogTriage's October 2026 analysis of real server logs found 1,584 addresses making 41,780 failed SSH login attempts against a single small production server in one month — about 1,348 attempts a day, with 5,201 distinct usernames tried and root alone accounting for 32% of guesses. So how to prevent SSH brute-force attacks before one of those guesses lands? The answer is a stack of five layers, applied in order: disable password logins in favor of SSH keys, ban repeat offenders with fail2ban, tighten sshd's own attempt limits, rate-limit at the firewall, and add two-factor authentication on top. Most of the hardening guides you will find stop at one or two of these. On a typical VPS hosting setup you control all five, and the first one alone removes the entire class of attack. This guide walks through each layer with the exact settings that ship on modern systems, and it is defensive all the way down — there is nothing here that helps anyone attack someone else's server.

How Do You Stop SSH Brute-Force Attacks?

To stop SSH brute-force attacks, you remove passwords as a target and then stack four defensive layers on top: SSH-key-only authentication, fail2ban banning, sshd attempt limits, firewall rate-limiting, and two-factor authentication. Each layer covers the gaps of the one before it — keys eliminate guessing, fail2ban silences the bots, sshd limits bound what a single connection can do, the firewall bounds how fast connections can arrive, and 2FA catches the case where a key itself is stolen. Here is the stack in the order you should apply it:

  1. Keys only: set PasswordAuthentication no so there is no password to guess. This single change kills the brute-force attack class outright.
  2. fail2ban: watch the auth logs and firewall-block any IP that fails too many logins in a row.
  3. sshd limits: cap auth attempts per connection (MaxAuthTries) and how long an unauthenticated connection may linger (LoginGraceTime).
  4. Firewall rate-limit: ufw limit ssh/tcp throttles how fast any one IP can even open connections.
  5. 2FA: require a one-time code in addition to the key, so a stolen key alone opens nothing.

Layer 1: SSH Key Authentication — Disable Password Login

A brute-force attack guesses passwords. If your server does not accept passwords, there is nothing to guess — this is why keys-only is always layer one, and why everything below it is about noise reduction and defense in depth. OpenSSH's own man page says it plainly: PasswordAuthentication is "allowed" and defaults to "yes," and PubkeyAuthentication is also allowed by default — so a stock server accepts both, and the bots know it.

Start on your own machine. Generate an ed25519 key pair and copy the public half to the server:

ssh-keygen -t ed25519 -C "[email protected]"
ssh-copy-id user@server

If you have no key yet, our free SSH key generator creates one in the browser and shows you exactly how to install it. Then prove key login works before you disable passwords — open a new SSH session with your key while the old one is still connected. Only then, on the server, add the hardening drop-in:

sudo tee /etc/ssh/sshd_config.d/10-hardening.conf >/dev/null <<'CONF'
PasswordAuthentication no
PermitRootLogin prohibit-password
CONF
sudo sshd -t && sudo systemctl reload ssh

Two details matter here. First, on cloud images, cloud-init drops its own config into /etc/ssh/sshd_config.d/50-cloud-init.conf — and sshd honors the first value it reads, so a drop-in that re-enables passwords after yours silently wins. Check the whole directory, not just the main file. Second, this is the step where people lock themselves out: never close your existing SSH session until a fresh connection with your key succeeds. The never-ban-yourself discipline applies at every layer, and it is cheapest here.

Layer 2: Ban Repeat Offenders with fail2ban

With passwords off, the bots keep connecting, failing, and retrying — costing CPU and burying real log lines. fail2ban watches the authentication logs and adds firewall rules that drop an IP after it fails too many logins — the standard tool for stopping SSH brute-force login attempts at the log layer: the fail2ban project ships an sshd jail specifically for failed SSH authentication, and version 1.1.1 is current as of 2026.

One rule governs all fail2ban configuration, stated verbatim in the project's official wiki: never edit .conf files, because "package upgrades may overwrite them" — create .local files that override only the settings you change. A second rule: upstream ships all jails disabled by default, though Debian and Ubuntu's defaults-debian.conf pre-enables [sshd] for you. A minimal fail2ban Ubuntu setup for the SSH lane looks like this:

[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 YOUR_OFFICE_IP/32

[sshd]
enabled = true
port    = ssh
backend = systemd

The shipped defaults are bantime = 10m, findtime = 10m, maxretry = 5, and ignoreip = 127.0.0.1/8 ::1 — the example above keeps the window tight and adds your own address to the allowlist so the tool never bans you during testing. Verify it is working with fail2ban-client status sshd, and free yourself with fail2ban-client set sshd unbanip YOUR_IP if you ever trip your own rule. This section covers the problem-frame placement of fail2ban in the five-layer stack; the full tool tutorial — first install, first jail, ban and unban walkthroughs — belongs to our Fail2ban beginner guide, which goes deeper on each of those commands.

Layer 3: Tighten sshd's Own Attempt Limits

sshd ships with generous defaults that assume a friendly network. Two settings bring them in line with a hostile internet. MaxAuthTries caps how many authentication attempts one connection may make, and its man page documents the default of 6 — "Once the number of failures reaches half this value, additional failures are logged," so dropping it also quiets your logs. LoginGraceTime caps how long an unauthenticated connection may sit idle, and the man page states: "The server disconnects after this time if the user has not successfully logged in" — the default is 120 seconds, which is generous enough for slow-connection-exhaustion abuse. Sensible production values:

MaxAuthTries 3
LoginGraceTime 30

Add these to the same sshd_config.d/10-hardening.conf drop-in, then validate and reload with sshd -t && sudo systemctl reload ssh (on Ubuntu 24.04, a restart of the ssh.service unit is the documented route for config changes to take effect). Three tries per connection is plenty for a human with a key agent; it is far too few for a bot cycling a password list, which is exactly the point.

Layer 4: Rate-Limit SSH at the Firewall

fail2ban reacts to failed logins; the firewall can react to connection speed, which stops the noise before sshd ever sees it. UFW's rate-limit rule is purpose-built for this: the ufw limit ssh rate limit needs no tuning, because the official ufw man page fixes the behavior in stone — it denies connections when an IP address "attempts to initiate 6 or more connections within 30 seconds."

Two sequencing rules keep this layer safe. First, whitelist your own address before enabling limits: ufw allow from YOUR_IP to any port 22 proto tcp. Second, if you have already moved SSH off port 22, apply the limit to the real port instead (ufw limit 2222/tcp). Moving the port off 22 cuts log noise because most bots only knock on the default port — but treat that as noise reduction, not security; scanners that probe every port will still find it. The firewall layer's real value is the per-IP speed cap, which holds no matter which port SSH listens on.

Layer 5: Add Two-Factor Authentication

With keys-only in place, a stolen private key becomes the single remaining path into the server. Two-factor authentication closes it: the attacker would need both your key file and the six-digit code from your phone. The standard self-hosted route is the Google Authenticator PAM module, libpam-google-authenticator, and its README documents the one setting that makes rollout painless — the nullok option: with auth required pam_google_authenticator.so nullok, users who have not enrolled yet can still log in without a code, so your SSH keys keep working codelessly while you enroll at your own pace. Remove nullok after everyone is enrolled, and the code becomes mandatory.

The sshd side needs three settings to enforce the chain: UsePAM yes, KbdInteractiveAuthentication yes, and AuthenticationMethods publickey,keyboard-interactive. Use the modern name — the OpenSSH 8.7 release notes say they "remove references to ChallengeResponseAuthentication in favour of KbdInteractiveAuthentication" and retain the old name only as a deprecated alias. Note the default behavior of AuthenticationMethods: sshd accepts any single listed method by default, so keys alone bypass the code prompt unless you explicitly chain the two methods as shown. Enroll on the server with google-authenticator (scan the QR code with your authenticator app), then verify from a fresh session that login now requires key and code.

The Five-Layer Stack, at a Glance

One page, one table — this is the original quick-reference you can cite, and it is also the answer to "where do I start?" in order:

LayerWhat it killsEffort
1. SSH keys, passwords offThe entire brute-force attack class — nothing left to guessMinutes, one config change
2. fail2banRepeated offenders — firewall-blocks IPs after too many failuresOne package, one jail.local
3. sshd limitsUnbounded guessing per connection and idle connection hoardingTwo lines in sshd_config.d
4. Firewall rate-limitConnection floods — 6+ connections in 30s per IP is deniedOne ufw limit rule
5. 2FAStolen private keys — key alone no longer opens the doorPAM module + phone enrollment

If you only do one layer, do layer one. Keys-only ends password guessing the way removing a door ends lock-picking; every layer after it is about quieter logs, cheaper CPU, and the stolen-key edge case.

The Never-Lock-Yourself-Out Checklist

Hardening SSH has a single classic failure mode: the administrator, not the attacker, gets locked out. Run this checklist at every layer, not just layer one:

  • Keep your working session open. Make every change, then prove a brand-new connection works before closing the old one. Two sessions, one change — the second session is your insurance policy.
  • Allowlist your own addresses. Put your office and home IPs in fail2ban's ignoreip and in UFW allow rules first, before tightening anything.
  • Back up before you edit. sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F) takes two seconds and has saved more weekends than any other command.
  • Validate before reloading. sshd -t catches syntax errors while they are still harmless. A reload of a broken config is a scheduled outage; a validated one is a non-event.
  • Watch the journal after each layer. sudo journalctl -u ssh --since "10 minutes ago" | grep -c 'Failed password' gives you the before-and-after number that proves each layer is working.

How Bad Is It Really? The Scale of SSH Brute-Forcing

It is not a hypothetical threat. LogTriage's "What Attacks an Internet-Facing Server: October 2026," published 1 November 2026 from real production server logs, measured 1,584 distinct addresses making 41,780 failed SSH login attempts against one small server in a single month — roughly 1,348 attempts per day. Two findings are worth absorbing: attackers tried 5,201 distinct usernames, and 68% of guesses hit accounts that do not even exist, which means the bots are spraying, not targeting. This is exactly why the five layers work — keys-only ignores the spray entirely, fail2ban and the firewall throttle the sprayers, and 2FA covers the one path the spray cannot reach.

Sources

Every technical claim above was verified against these sources, fetched on 2026-11-04:

  • OpenSSH sshd_config man page (man.openbsd.org) — verbatim defaults for PasswordAuthentication, PubkeyAuthentication, PermitRootLogin, MaxAuthTries, and LoginGraceTime.
  • fail2ban official wiki, "Proper fail2ban configuration" (github.com/fail2ban/fail2ban) — the never-edit-.conf rule and jail.local pattern.
  • fail2ban 1.1.1 release notes and Debian package metadata — current version as of late 2026.
  • UFW man page (manpages.ubuntu.com) — the verbatim "6 or more connections within 30 seconds" rate-limit rule.
  • google/google-authenticator-libpam README — the nullok rollout semantics for SSH 2FA.
  • OpenSSH 8.7 release notes (openssh.com) — the ChallengeResponseAuthentication to KbdInteractiveAuthentication rename.
  • LogTriage, "What Attacks an Internet-Facing Server: October 2026" (published 2026-11-01) — the 41,780-attempt measurement.
  • Xitoring, "Stop SSH Brute-Force Attacks on Linux: 5 Fixes" — independent confirmation of the five-layer ordering.
  • Ubuntu 24.04 server documentation — openssh 9.6.x packaging and sshd service behavior.

Frequently Asked Questions

Does fail2ban alone stop SSH brute-force login attempts?

No — fail2ban is a throttle, not a wall. It bans IPs after they fail too many logins, but the passwords are still there to be guessed. The correct baseline is SSH keys with password authentication disabled, which removes the attack class entirely; fail2ban then handles the noise and the edge cases, as layer two of the stack.

What is the fail2ban jail.local rule about?

The fail2ban project says to never edit .conf files because package upgrades can overwrite them. You create /etc/fail2ban/jail.local (or a file in jail.d/) containing only the settings you want to override — everything else inherits the shipped defaults. That one rule is the difference between a configuration that survives updates and one that silently resets.

Should I change the SSH port from 22?

Moving off port 22 dramatically reduces log noise, because most bots only knock on the default port — but it is noise reduction, not security. Scanners that probe all ports will find SSH wherever it listens. Change the port if you want quieter logs; do not count it as a security layer, and apply your firewall rate-limit to the real port.

Will enabling 2FA for SSH lock me out if I lose my phone?

Only if you skip the rollout discipline. Enroll with nullok first so unenrolled logins still work while you set up, print or screenshot your emergency backup codes during enrollment, and keep a second SSH session open while you test. Remove nullok only after you have logged in with key plus code at least once.

Why not just block all SSH except from my IP?

IP allowlisting is excellent — ufw allow from YOUR_IP to any port 22 proto tcp — and if your address is static, it is one of the strongest controls available. The catch is mobility: the moment you log in from a hotel, a phone hotspot, or a new office range, you are locked out. Most administrators combine an allowlist for known ranges with the full five-layer stack for everywhere else.

Featured on Twelve Tools Hunt for Tools Featured on LaunchBuff Launch Llama Tools Launch your MCP Top Web Development Companies in the US - OnToplist.com Designer Listings Listed on SeeWhatNewAI