How the attack works
Scanners sweep internet address space for anything answering on port 22, or wherever SSH listens, and hand the list to tools that try to log in again and again. Nothing about it is clever. It works because some servers still accept passwords and some accounts still have weak or reused ones. The variants differ in what they guess:
- Dictionary guessing: common usernames (root, admin, test, ubuntu) with common passwords. MITRE ATT&CK calls this password guessing.
- Credential stuffing: username and password pairs leaked from other breaches, betting that someone reused a password.
- Password spraying: one or two likely passwords tried across many usernames, which keeps the failures per account low.
- Distributed, low-and-slow: one campaign spread over many addresses, each making a few attempts with pauses in between, so that no single source crosses a per-address limit.
Most of this is untargeted. A guessed username that really exists on your system, and is not obvious from outside, is the exception worth noticing.
What it looks like in the log
Where sshd writes depends on the distribution. Ubuntu Server, and Debian when rsyslog is installed, write /var/log/auth.log. Rocky Linux and AlmaLinux write /var/log/secure. The systemd journal has the same lines on all of them, and Debian 12 and later keep only the journal by default. In the journal the SSH unit is named ssh on Debian and Ubuntu (Debian 12, Ubuntu 24.04) and sshd on Rocky Linux and AlmaLinux. These read-only commands show recent failures:
# Ubuntu, or Debian with rsyslog installed
sudo grep -E "Failed password|Invalid user" /var/log/auth.log | tail -n 20
# Rocky Linux, AlmaLinux
sudo grep -E "Failed password|Invalid user" /var/log/secure | tail -n 20
# The journal on Debian and Ubuntu (unit name: ssh)
sudo journalctl -u ssh --since "1 hour ago" --no-pager | grep -E "Failed password|Invalid user"
# The journal on Rocky Linux and AlmaLinux (unit name: sshd)
sudo journalctl -u sshd --since "1 hour ago" --no-pager | grep -E "Failed password|Invalid user"
# Did anything succeed? (use -u sshd on Rocky Linux and AlmaLinux)
sudo journalctl -u ssh --since "24 hours ago" --no-pager | grep "Accepted""Failed password for root from 203.0.113.7 port 50122 ssh2" is a wrong password for an account that exists; "Invalid user admin from 203.0.113.7" means the account does not exist. With password login turned off you see fewer "Failed password" lines and more connections closed before authentication, marked [preauth], because there is no password prompt left to fail.
The line that matters is "Accepted password for" from an address that was failing shortly before. Treat it as a possible compromise and work through the incident checklist before changing anything.
Why it matters, and when it does not
Against a server that only accepts keys, brute force is background noise. The guesses fail however many there are, because a private key is not something that can be guessed. The cost is log volume and a little CPU for each handshake.
Against a server that accepts passwords, it is a lottery the attacker can play for free, indefinitely. One weak or reused password is enough, and if root can log in with a password, one correct guess is full control. What follows is usually automated too: a key added for later, a miner started, the server put to work scanning for the next victim.
Find unauthorised SSH keysWhat is cryptojacking?Signs a Linux server has been hacked
Defences, in order
The first two remove the risk; the rest reduce the noise and the exposure.
- Key-only authentication. Set PasswordAuthentication no and KbdInteractiveAuthentication no, after proving your key login works in a second session. This alone makes guessing pointless.
- No root login with a password. PermitRootLogin no, or prohibit-password (the OpenSSH default) if root must log in with a key.
- sshd's own limits. MaxStartups (default 10:30:100) refuses a growing share of unauthenticated connections as they pile up, and MaxAuthTries (default 6) caps attempts per connection. OpenSSH 9.8 and later also has PerSourcePenalties, which makes sshd refuse connections for a time from an address that keeps failing.
- A banning tool. fail2ban or CrowdSec adds a firewall rule for addresses that fail repeatedly. On a journal-only system a file-based jail can match nothing while reporting healthy, so check what it reads.
- A firewall allowlist. Where administrators connect from known addresses, a VPN or a bastion host, allow SSH only from there. Where practical, this removes the exposure instead of reacting to it.
# Effective settings, after every Include and drop-in file
sudo sshd -T | grep -Ei "^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin|maxstartups|maxauthtries) "sshd -T prints what the daemon will really use, which can differ from the main file when a drop-in overrides it. If passwordauthentication says yes, the first defence is not in place. The full sequence, including the steps that keep you from locking yourself out, is in the brute-force guide.
How to stop SSH brute force: the full guideSSH config checkerfail2ban config checkerfail2ban is not banning IPs
What blocking cannot do
Every banning tool counts failures per source address, and so does SecAI. A campaign spread across many addresses, each trying a few times, never crosses the threshold, and an attacker with a botnet simply moves to a fresh address. A ban also does nothing once a guess succeeds: the attacker is then an ordinary authenticated user.
Blocks have side effects too. One address can be shared by many people behind carrier-grade NAT, and an automatic blocker without an allowlist for your own addresses will eventually lock out an administrator at the worst moment. Blocking keeps the logs readable; key-only authentication is what makes the server safe.
How SecAI handles it
SecAI counts failed logins per source address from the authentication log. An address seen three times within thirty minutes is blocked automatically, but only on a server set to Automatic, through the firewall the server already runs (fail2ban, CrowdSec, CSF, firewalld or iptables). The block expires on its own after 6 hours (longer for an address that keeps coming back, never more than 7 days), never applies to your own or trusted addresses, and takes effect within minutes. On a server set to Approval the same block is proposed and waits in Awaiting Approval for a person.
Because blocking has the limits above, SecAI also reports a successful login that follows failures or comes from a new source, and it flags weak effective SSH settings such as password authentication or root login, with a hardening fix that waits for your approval.