Stop brute-force attacks on SSH and web logins

Updated November 2026

Any server with SSH open on the internet gets password-guessing attempts (a brute-force attack). That is normal background noise. It is a problem only if a weak password lets someone in, or if the volume slows the server down. First check that no guess succeeded. Then switch SSH to key-only logins, which makes the guessing useless, and use fail2ban and rate limits to cut the noise.

Do this first.

  1. Check for successful logins you don't recognise (command below). If you find one, stop here and go to Leaked credentials.
  2. Make sure your own SSH key login works, then turn off password logins for SSH.
  3. Install fail2ban with the sshd jail. Add your own fixed IP address to ignoreip first.

Symptoms

Read the logs

Debian 12 and later don't install rsyslog by default, so there may be no /var/log/auth.log. The systemd journal works everywhere. On RHEL-family systems, use -u sshd (the text file is /var/log/secure).

# Did anyone get in? (look for addresses you don't know)
sudo journalctl -u ssh --since "7 days ago" -g 'Accepted (password|publickey|keyboard-interactive)'

# Top 20 guessing addresses in the last hour
sudo journalctl -u ssh --since "1 hour ago" -g 'Failed password|Invalid user' --no-pager \
  | grep -oE 'from [0-9a-fA-F.:]+' | awk '{print $2}' | sort | uniq -c | sort -rn | head -20

# Top 20 addresses posting to a web login page
sudo awk '$6 == "\"POST" && $7 ~ /wp-login|login/ {print $1}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -rn | head -20

The last command assumes nginx's default combined log format.

1. Make SSH key-only

Put your settings in a drop-in file with a low number. sshd uses the first value it reads for each keyword, and Debian, Ubuntu and RHEL 9 and later read /etc/ssh/sshd_config.d/*.conf before the rest of the main file. A file such as 50-cloud-init.conf on cloud images may otherwise switch passwords back on.

sudo tee /etc/ssh/sshd_config.d/10-no-passwords.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
MaxAuthTries 3
LoginGraceTime 30
EOF
sudo sshd -t && sudo systemctl reload ssh     # RHEL family: reload sshd

Keep your current session open and test a new login from a second terminal. Use PermitRootLogin no if you always log in as a normal user. Recent OpenSSH releases also slow down repeat offenders on their own: PerSourcePenalties is on by default (for example in OpenSSH 10.0 on Debian 13).

2. Set up fail2ban

Debian/Ubuntu: sudo apt install fail2ban. AlmaLinux/Rocky: enable EPEL (sudo dnf install epel-release), then sudo dnf install fail2ban. On RHEL itself, enable EPEL as described in Fedora's EPEL documentation. Never edit jail.conf. Create /etc/fail2ban/jail.local:

[DEFAULT]
# your own fixed IPs/VPN range, so you can't ban yourself
ignoreip = 127.0.0.1/8 ::1 203.0.113.10
bantime  = 1h
findtime = 10m
maxretry = 5
bantime.increment = true
# match your firewall: nftables-multiport, firewallcmd-rich-rules, ufw
banaction = nftables-multiport

[sshd]
enabled = true
backend = systemd

[nginx-http-auth]
enabled = true

[nginx-limit-req]
enabled = true

[nginx-botsearch]
enabled = true
sudo systemctl enable --now fail2ban
sudo fail2ban-client reload
sudo fail2ban-client status
sudo fail2ban-client status sshd

The defaults are bantime = 10m, findtime = 10m and maxretry = 5. bantime.increment makes the ban longer each time an address comes back. backend = systemd reads the journal, which is needed when there is no auth.log. Only enable the nginx jails if nginx is installed. If an enabled jail's log file is missing, fail2ban can fail to start. nginx-limit-req works together with the nginx rate limit in the next step.

3. Rate-limit web logins

In nginx, define a zone in the http block, then apply it inside the location that already serves your login URL:

# http { ... }
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;

# inside the existing location for /wp-login.php (or your login path)
limit_req zone=login burst=5 nodelay;
limit_req_status 429;

This allows each IP address about 10 login requests a minute, with a burst of 5. Rejected requests get HTTP 429 and an error-log line, which the nginx-limit-req jail turns into a ban. Test and reload (this reloads a service):

sudo nginx -t && sudo systemctl reload nginx

Ubuntu's ufw has a simple SSH rate limit too. sudo ufw limit 22/tcp denies an address that opens 6 or more connections within 30 seconds.

4. Add a second factor

5. Move admin panels off the internet

Database tools, control panels, monitoring dashboards and CMS admin areas don't need to be public. Make them listen on 127.0.0.1 or a VPN address only, or allow only your VPN range in the firewall:

sudo ufw allow from 10.8.0.0/24 to any port 8443 proto tcp
# or reach a localhost-only panel through SSH:
ssh -L 8443:127.0.0.1:8443 alice@server   # then open https://localhost:8443

A small WireGuard or OpenVPN server, or your provider's private network, are common choices. You can also restrict SSH itself to the VPN range once the VPN is reliable.

Confirm it is working

When to call a professional

If a guess succeeded, treat it as a break-in: see Signs of compromise and Incident response. If the login flood is large enough to slow the whole server, it is a traffic flood problem. See DDoS and floods and contact your host.

Sources