How to stop SSH brute force attacks
Last updated:
Any server with port 22 open to the internet gets password guesses within minutes of booting, all day, from thousands of addresses. Most of them are bots trying root, admin and common names with lists of leaked passwords. They only get in where a password is weak, but they fill the logs and use CPU, and one weak account is enough.
The fix is in layers: make passwords useless (keys only), take root off the table, then ban and rate limit what is left. The commands below are for Debian and Ubuntu, and for the RHEL family (RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora).
Check whether you are being attacked
The SSH daemon logs every failed attempt. Count the last 24 hours, then see which addresses send the most:
sudo journalctl --since "24 hours ago" | grep -cE "Failed password|Invalid user"sudo journalctl --since "24 hours ago" \
| grep -E "Failed password|Invalid user" \
| grep -oE "from [0-9a-fA-F.:]+ port" | awk '{print $2}' \
| sort | uniq -c | sort -rn | head -20Hundreds or thousands of lines a day is normal for a server on a public IP. If the journal is not kept between reboots, the same lines are in /var/log/auth.log on Debian and Ubuntu and in /var/log/secure on the RHEL family.
Set up SSH keys and test them first
Everything that follows turns passwords off, so a working key comes first. On your own computer, create a key, copy it to the server and log in with it. Replace deploy with your user and SERVER_IP with the server's address.
ssh-keygen -t ed25519 -C "laptop"
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@SERVER_IP
ssh -i ~/.ssh/id_ed25519 deploy@SERVER_IPThe last command must log you in without asking for the account's password (it may ask for the key's passphrase, which stays on your computer).
Warning: From here on, keep one SSH session to the server open and do every test from a second terminal. If a change locks you out, the open session is how you undo it. Also find your provider's web console (VNC or serial) before you start: it works without SSH.
Create a sudo user before disabling root
If you log in as root today, create a normal user with sudo rights first, and give it the same keys.
sudo adduser deploy
sudo usermod -aG sudo deploysudo useradd -m deploy
sudo passwd deploy
sudo usermod -aG wheel deploysudo mkdir -p /home/deploy/.ssh
sudo cp /root/.ssh/authorized_keys /home/deploy/.ssh/
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keyssudo restorecon -R /home/deploy/.sshFrom the second terminal, log in as the new user and run sudo -v: it must accept the user's password. Only then go on.
Disable password login and root login
Current Debian, Ubuntu, RHEL 9 family and Fedora read extra files from /etc/ssh/sshd_config.d/. Check that yours does:
grep -i '^Include' /etc/ssh/sshd_configIf it prints Include /etc/ssh/sshd_config.d/*.conf, write the settings in a file of their own. SSH keeps the first value it reads for each setting, and the files are read in name order, so the 00- prefix wins over files such as 50-cloud-init.conf, which on some cloud images turns passwords back on.
sudo tee /etc/ssh/sshd_config.d/00-hardening.conf > /dev/null <<'EOF'
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
MaxAuthTries 4
LoginGraceTime 30
EOFIf there is no Include line (RHEL 8 and its rebuilds), set the same six lines in /etc/ssh/sshd_config itself, replacing the existing ones. Then test the configuration and look at the values SSH will actually use:
sudo sshd -t && echo "config OK"
sudo sshd -T | grep -Ei '^(pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|permitrootlogin|maxauthtries) 'Each line must show the value you set. If root still has to log in with a key, for a backup job for example, use PermitRootLogin prohibit-password instead of no. Reload SSH: open sessions stay connected.
sudo systemctl reload sshsudo systemctl reload sshdWarning: Do not close the open session yet. From the second terminal, log in with your key, then check that a password is refused: the command below must end with Permission denied (publickey).
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password deploy@SERVER_IPChange the SSH port, knowing what it does not do
Moving SSH off port 22 cuts the noise in the logs, because most bots only try 22. It does not make SSH safer: port scanners find a new port within hours, and a targeted attacker finds it at once. With key-only login the port does not matter much. Do it for cleaner logs, not for security, and only after the steps above.
Pick a port (2222 here), open it in the firewall and, on the RHEL family, tell SELinux that SSH may use it:
sudo ufw allow 2222/tcpsudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --reload
sudo dnf install -y policycoreutils-python-utils
sudo semanage port -a -t ssh_port_t -p tcp 2222Open the port in your provider's cloud firewall or security group too. Then set the port and restart SSH:
echo "Port 2222" | sudo tee /etc/ssh/sshd_config.d/01-port.conf
sudo sshd -t && echo "config OK"On Ubuntu 24.04 and later SSH can be started by ssh.socket: if the first command below prints active, run all four lines; otherwise only the last one.
systemctl is-active ssh.socket
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo systemctl restart sshsudo systemctl restart sshdssh -p 2222 deploy@SERVER_IPWarning: Only when the login on the new port works, remove the rule for port 22 from the firewall. Fail2ban, sshguard and the rate limits below must use the new port too.
Ban repeat offenders with fail2ban or sshguard
With passwords off, guesses cannot succeed, but banning the addresses that keep trying cuts the load and the noise. fail2ban reads the log and bans an address at the firewall after a number of failures.
sudo apt update
sudo apt install -y fail2ban python3-systemdsudo dnf install -y epel-release
sudo dnf install -y fail2banOn RHEL itself and Oracle Linux, enable EPEL as their documentation says, then install fail2ban. On Fedora it is in the main repositories. Amazon Linux 2023 cannot use EPEL: use the firewall rate limit below instead.
Settings go in jail.local, never in jail.conf, which updates overwrite. Check that you do not have one already, then create it. Replace YOUR_OFFICE_IP with the fixed addresses you work from, or delete it, and set port to your SSH port if you changed it.
ls -l /etc/fail2ban/jail.localsudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 YOUR_OFFICE_IP
bantime = 1h
bantime.increment = true
findtime = 10m
maxretry = 5
[sshd]
enabled = true
backend = systemd
port = ssh
EOFThis bans an address for one hour after 5 failures in 10 minutes, and for longer each time it comes back. backend = systemd reads the journal, which works on Debian 12 and later where /var/log/auth.log may not exist.
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshdUnder attack, Total failed and Currently banned grow within minutes. If Total failed stays at 0 while the journal shows failed logins, the jail is not reading the right log. To lift a ban:
sudo fail2ban-client set sshd unbanip 203.0.113.7sshguard, the lighter alternative
sshguard does the same job with less configuration and is packaged for Debian and Ubuntu (and in EPEL). Put your own addresses in its whitelist first, then watch sudo journalctl -u sshguard -f during an attack to see the blocks. Use one of the two, not both.
sudo apt install -y sshguard
echo "YOUR_OFFICE_IP" | sudo tee -a /etc/sshguard/whitelist
sudo systemctl restart sshguardRate limit or restrict SSH at the firewall
On Ubuntu and Debian with ufw, limit refuses an address that opens 6 or more connections in 30 seconds. It replaces a plain allow 22/tcp rule. If sudo ufw status also shows an OpenSSH rule, delete it once the limit is in place (sudo ufw delete allow OpenSSH): it would match before the limit.
sudo ufw limit 22/tcp
sudo ufw enable
sudo ufw status verboseWarning: If ufw is not active yet, ufw enable starts blocking every port without a rule. Allow the services the server offers (for example sudo ufw allow 80,443/tcp) before enabling it, and keep the open session.
firewalld's rich rules have a limit, but it counts all sources together, so an attack can use up the allowance and lock you out too. On the RHEL family, use fail2ban for per-address limits.
Allow SSH only from your addresses
The strongest step, when you work from fixed addresses or a VPN: nobody else can even reach SSH.
sudo ufw allow from YOUR_OFFICE_IP to any port 22 proto tcp
sudo ufw delete limit 22/tcp
sudo ufw status numberedsudo firewall-cmd --permanent --zone=trusted --add-source=YOUR_OFFICE_IP
sudo firewall-cmd --reload
sudo firewall-cmd --permanent --zone=public --remove-service=ssh
sudo firewall-cmd --reloadWarning: This locks out every other address, including yours if it changes. Test a new login after the first reload and before removing the public SSH rule, and keep the provider's web console as the way back.
How InfraFence handles SSH brute force
The InfraFence agent reads the SSH log as it is written and bans an address at the firewall once it reaches the failure threshold within the time window, both of which you can change in the dashboard.
- 15 detection patterns: failed passwords, invalid users, PAM failures, pre-auth scanning, protocol mismatches and key-exchange drops.
- Escalating bans: 24 hours, 7 days, 30 days, then permanent, or a duration you set. Optionally, an address banned on one server is banned on all your servers.
- The same protection for Postfix, Dovecot, MySQL, PostgreSQL, MongoDB, Pure-FTPd, ProFTPD and vsftpd logins.
- The hardening checks flag root login, password login, the default port and a high
MaxAuthTries.MaxAuthTriesis fixed with one click, in a file of its own (/etc/ssh/sshd_config.d/00-infrafence-hardening.conf) that is tested before SSH is reloaded. Root login, passwords and the port can lock you out, so they are explained step by step instead, as in this guide. - Every SSH session is followed from login to logout and given a risk score, so you know who was logged in when something else fired.
It works next to fail2ban: the agent recognizes it, and keeps its own rules in separate INFRAFENCE chains that it puts back after a firewall reload.
Questions
- Is changing the SSH port enough to stop brute force?
- No. It cuts the number of attempts in the logs, because most bots only try port 22, but scanners find the new port quickly. Key-only login is what makes the guesses useless.
- Should I use fail2ban or sshguard?
- Either works for SSH. fail2ban has more filters for other services and more options; sshguard needs less configuration. Run one, not both, so they do not fight over the same addresses.
- I disabled passwords and still see failed logins. Is something wrong?
- No. Bots keep trying because they do not know passwords are off. The attempts fail before any password is checked. Bans and rate limits reduce the noise.
Try it free on 1 server
Starter is free for one server, with full protection and automatic blocking. No credit card.
Install the agent as root, with your token in place of <TOKEN>:
curl -fsSL https://github.com/infrafence/infrafence-agent/releases/latest/download/install.sh | sudo bash -s -- --token <TOKEN>The token is created in the dashboard, in Servers › Add server.
