Any Linux server with port 22 open to the internet is being probed continuously. Most of it is automated and untargeted. Some of it is not.
Key Takeaways
- Two patterns matter and they need different responses: concentrated attempts from one source, and distributed attempts spread thin across many.
- Per-IP thresholds catch the first and miss the second entirely, because no single source crosses the limit.
- Blocking is a response. Disabling password authentication removes the problem, because there is nothing left to guess.
- Blocked does not mean safe. After any heavy attempt, verify nothing succeeded and check for persistence.
- Any auto-blocking system needs an allowlist for your own addresses, or you will eventually lock yourself out during an incident.
Table of Contents
- What it looks like
- Blocking automatically
- Removing the attack surface entirely
- What to check after a heavy attempt
- The safety net that matters
What it looks like
Check your auth log:
sudo grep "Failed password" /var/log/auth.log | tail -50
Count attempts by source:
sudo grep "Failed password" /var/log/auth.log \
| awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20
Two patterns matter. Concentrated: one IP, hundreds of attempts, usually against root or admin, noisy and easy to block. Distributed: many IPs, a handful of attempts each, often against a valid username. That one is quieter, more deliberate, and slips past naive thresholds.
Counting by username instead of by source
The distributed case becomes visible when you pivot the same data:
sudo grep "Failed password" /var/log/auth.log \
| grep -oP "for (invalid user )?\K\S+" | sort | uniq -c | sort -rn | head -20
Untargeted botnets try root, admin, test, ubuntu, oracle, a dictionary of defaults. If the top entry is a real username that exists on your system, particularly one that is not obvious from the outside, someone has done reconnaissance. That changes the situation from background noise to something worth attention.
On a systemd host without a traditional auth log, the same data lives in the journal:
sudo journalctl -u ssh --since "24 hours ago" | grep "Failed password"
Reading the volume honestly
A public server seeing thousands of failed attempts per day is normal and not itself an incident. What matters is the shape: a sudden change in volume, a shift to a valid username, attempts arriving from a range that also appears in your web logs, or attempts that stop abruptly, which occasionally means one succeeded.
Blocking automatically
fail2ban handles the concentrated case well. A reasonable SSH jail:
[sshd]
enabled = true
maxretry = 4
findtime = 600
bantime = 3600
For distributed attempts, per-IP thresholds do not trigger. You need to correlate across sources, the signal is the username being targeted, not the IP. If you are weighing fail2ban against a shared-reputation approach that catches distributed attacks earlier, I compared the two in fail2ban vs CrowdSec.
Confirm the jail is actually matching, because a filter that has silently stopped working looks identical to a quiet week:
sudo fail2ban-client status sshd
Zero bans on an internet-facing server usually means the filter no longer matches your log format, not that nobody is trying. The wider set of decisions around ban scope and duration is in how automated IP blocking works and how it goes wrong.
Removing the attack surface entirely
Blocking is a response. Prevention is better: disable password authentication and the brute force problem disappears, because there is no password to guess. Attempts still appear in logs but cannot succeed. That change, and the rest of the SSH lockdown, is covered in the SSH hardening checklist.
Worth being explicit about what this does and does not solve. It ends guessing as an attack path completely. It does nothing about a key that has been stolen from a developer laptop, or a deploy credential leaked in a repository. Those authenticate correctly and produce a normal Accepted publickey line, which is why the next section still matters after you have made this change.
What to check after a heavy attempt
Blocked does not mean safe. Verify nothing got through:
sudo grep "Accepted" /var/log/auth.log | tail -20
sudo last -20
Then check for persistence: new entries in ~/.ssh/authorized_keys, new cron jobs, new systemd services, changed sshd_config. If an attacker did get in, they often leave a webshell in a web-writable directory as a second way back - worth checking for at the same time.
Concretely, the fastest sweep:
for d in /root /home/*; do
[ -f "$d/.ssh/authorized_keys" ] && echo "== $d" && cat "$d/.ssh/authorized_keys"
done
awk -F: '$3 == 0 {print}' /etc/passwd
ls -la /etc/sudoers.d/
find /etc/systemd/system -newermt "-7 days" -type f 2>/dev/null
An added key is the one that matters most, because it survives the password rotation most people perform after an incident. The full treatment is in detecting privilege escalation on a Linux server, and if any of this comes back unexpected, how to tell if your Linux server has been compromised is the wider triage.
The safety net that matters
One risk of aggressive auto-blocking is banning yourself. It happens more than people admit, a bad password from an office IP during an incident, and now you cannot reach the server.
Any auto-blocking system needs a permanent allowlist for your own infrastructure and known-good admin IPs. SecAI treats this as non-negotiable: your own server IPs and trusted addresses are exempt from auto-block by design, so remediation can never lock you out. It blocks hostile sources and monitors for persistence automatically, while keeping you safely on the allowlist.
Before you enable anything that blocks automatically, confirm two things: that your own ranges are allowlisted, and that you know how to reach your provider's web console if it goes wrong anyway. Where this fits among the other controls worth running is covered in the guide to Linux server security software.
Frequently Asked Questions
Is it normal to see thousands of failed SSH attempts per day? On a public server, yes. It is background noise from untargeted scanning. What matters is a change in the pattern rather than the raw count.
Does moving SSH off port 22 stop brute force? It removes most automated noise, which makes your logs readable. A targeted attacker will find the new port in a scan. Treat it as noise reduction rather than a control.
Should I ban permanently after repeated attempts? Escalating temporary bans work better. Addresses get reassigned, and a permanent ruleset grows into something nobody dares to prune.
How do I catch distributed attempts that never cross a threshold? Correlate on the targeted username rather than the source address, and watch for deviation from your server's own normal login pattern rather than a fixed number.
An attempt succeeded. What now? Treat the server as compromised. Check for added keys, new users, sudoers changes, and scheduled tasks, and work through the triage in how to tell if your Linux server has been compromised before deciding whether to clean or rebuild.
Blocking on a per-IP threshold is only part of it. Server-level monitoring for a VPS also covers what happens after a successful login, which is the part brute-force tooling cannot see.
Before any of that, it is worth confirming your SSH server is configured the way you think it is. Run sshd -T and paste the output into the free SSH config checker: it runs entirely in your browser and is the quickest way to find out that a drop-in file has been quietly overriding the setting you edited.
The automated version of all of this, including the distributed attacks a per-IP threshold cannot catch, is covered under SSH brute force protection.