SSH login attempts in my logs: am I being hacked?
You log in to your server, glance at /var/log/auth.log, and it's thousands of lines of Failed password for root from addresses all over the world. It looks like a siege. It's mostly weather.
Port 22 is the most predictable door on the internet, so automated scripts knock on it constantly, everywhere, all day. They aren't targeting you. They try a short list of user names (root, admin, oracle, ubuntu, test) with the passwords people choose most often, and move on. A failed attempt costs them nothing and tells you only that SSH is reachable. Your job is to answer two questions: did any attempt succeed, and could any future one?
Where to look
On Debian and Ubuntu with rsyslog, SSH writes to /var/log/auth.log. On Red Hat, Fedora, Rocky and Alma it's /var/log/secure. Newer Debian installs may not write that file at all and keep everything in the systemd journal. The journal works everywhere systemd runs; the service is called ssh on Debian and Ubuntu and sshd on the Red Hat family:
sudo journalctl -u ssh --since today # Debian, Ubuntu
sudo journalctl -u sshd --since "1 hour ago" # Red Hat, Fedora, Rocky, Alma
Three counts tell you most of the story:
sudo grep -c "Failed password" /var/log/auth.log
sudo grep "Accepted" /var/log/auth.log
sudo grep "Failed password" /var/log/auth.log | grep -oE "from [0-9.]+" | sort | uniq -c | sort -rn | head
The first is the size of the noise. The second is the one that matters: every successful login. The third shows which addresses try hardest.
A worked example: one night on a small VPS
Twelve lines from a server called web1. The two addresses of people (the attacker trying admin, and the owner logging in the next morning) are documentation addresses, so no real person appears here. The rest are public infrastructure.
Nov 6 03:12:41 web1 sshd[24811]: Invalid user admin from 203.0.113.77 port 51022
Nov 6 03:12:43 web1 sshd[24811]: Failed password for invalid user admin from 203.0.113.77 port 51022 ssh2
Nov 6 03:12:44 web1 sshd[24811]: Connection closed by invalid user admin 203.0.113.77 port 51022 [preauth]
Nov 6 03:12:50 web1 sshd[24815]: Failed password for root from 185.220.101.1 port 40122 ssh2
Nov 6 03:12:53 web1 sshd[24815]: Failed password for root from 185.220.101.1 port 40122 ssh2
Nov 6 03:12:55 web1 sshd[24815]: Disconnected from authenticating user root 185.220.101.1 port 40122 [preauth]
Nov 6 03:14:02 web1 sshd[24830]: Invalid user oracle from 3.5.140.1 port 33412
Nov 6 03:14:04 web1 sshd[24830]: Failed password for invalid user oracle from 3.5.140.1 port 33412 ssh2
Nov 6 03:14:04 web1 sshd[24830]: Received disconnect from 3.5.140.1 port 33412:11: Bye Bye [preauth]
Nov 6 03:20:17 web1 sshd[24902]: Connection closed by 167.94.138.35 port 49212 [preauth]
Nov 6 08:41:09 web1 sshd[25510]: Accepted publickey for deploy from 198.51.100.20 port 52814 ssh2: ED25519 SHA256:k3X0exampleexampleexampleexampleexampleexa
Nov 6 08:41:09 web1 sshd[25510]: pam_unix(sshd:session): session opened for user deploy(uid=1001) by (uid=0)Run through iplover's log checker, the five addresses sort out like this:
| IP address | Lines | Category | Network type |
|---|---|---|---|
| 203.0.113.77 | 3 | Private or reserved | Private or reserved address (not checked) |
| 185.220.101.1 | 3 | Tor exit nodes | Tor exit node |
| 3.5.140.1 | 3 | Cloud and hosting | Hosting, cloud or data center |
| 167.94.138.35 | 1 | Internet scanners | Not identified |
| 198.51.100.20 | 1 | Private or reserved | Private or reserved address (not checked) |
(The two documentation addresses show as reserved; in your own log, real addresses would be checked.) Now read the lines, because the categories only tell you who knocked, not whether the door opened.
Invalid user admin from 203.0.113.77: there's no account calledadminon this server, so this attempt could never have worked. Most of a typical auth.log is lines like this.Failed password for root from 185.220.101.1, twice: someone guessing root's password through Tor. The account exists, so this one matters more.Disconnected from authenticating user root … [preauth]means the session ended before login: preauth literally means “before authentication”.Invalid user oracle from 3.5.140.1: a script on an Amazon cloud server trying a database account name.Bye Bye [preauth]is the client giving up.Connection closed by 167.94.138.35 … [preauth]: Censys connected, read the SSH version banner and left without trying a password. That's a scanner taking inventory, not an attack.Accepted publickey for deploy from 198.51.100.20: the only successful login all night, with a key, by the owner. Thesession openedline right after it confirms the session started.
Verdict: one accepted login, and it's the owner's. Nobody got in. But root still accepts passwords, so the guessing on line four could succeed one day if root's password is weak. That's what to fix.
Did anyone actually get in?
Check every Accepted line. For each one, ask: is that my user name, my key or password, and an address I recognise (home, office, VPN)? Then cross-check with the login records:
last -a | head -20 # recent successful logins, with the remote address
sudo lastb | head -20 # recent failed logins (on systems that keep them)
who # who is logged in right now
Some newer distributions have replaced last and lastb with other tools; if they're missing, the journal and auth.log hold the same information.
An Accepted password or Accepted publickey line you can't explain is the one result that changes everything. In that case, assume the account is compromised:
- Look in
~/.ssh/authorized_keysfor every user (root included) and remove keys you don't recognise. - Look for new accounts (
/etc/passwd), newsudorights, and new cron jobs (crontab -lfor each user, plus/etc/cron.*). - Change the passwords of every account, and rotate any secrets stored on the server.
- If root was involved, the honest answer is to rebuild the server from a clean image and restore your data. An attacker with root can hide almost anything.
If you see this, do this
| What you see | What it means | What to do |
|---|---|---|
Invalid user … for names you don't have | Guessing that can't work | Nothing urgent. It disappears once passwords are off. |
Failed password for root or another real account | Guessing against an account that exists | Turn off password logins (below), or at least root's. |
Connection closed by … [preauth] with no password tried | A scanner reading your SSH banner | Nothing. |
| Thousands of failures a day from a few addresses | Persistent bots | Add fail2ban, or allow SSH only from your own addresses. |
Accepted … lines that are all yours | Nobody else got in | Nothing, apart from hardening. |
Accepted … from an address or user you can't explain | A likely break-in | Follow the steps above, starting with authorized_keys. |
Make guessing useless: keys only
A password can be guessed; an SSH key in practice can't. Once only keys are accepted, the bots can try forever.
First, create a key on your own computer and copy it to the server. macOS and Linux have the OpenSSH client built in. On Windows it's an optional feature, often already installed; if ssh isn't recognised, Microsoft's guide Get started with OpenSSH for Windows shows how to add it:
ssh-keygen -t ed25519
ssh-copy-id you@your-server # on Windows, paste ~/.ssh/id_ed25519.pub into the server's ~/.ssh/authorized_keys
Then, on the server, set these in /etc/ssh/sshd_config:
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
prohibit-password lets root in only with a key; use no to keep root out of SSH entirely. Before restarting, check what SSH will really use. Files in /etc/ssh/sshd_config.d/ are read too, and some cloud images ship one that turns passwords back on. For most settings the first value sshd reads wins:
sudo sshd -T | grep -Ei "passwordauthentication|kbdinteractive|permitrootlogin"
Restart SSH (sudo systemctl restart ssh, or sshd on the Red Hat family), and keep your current session open while you test a new login in a second window. If the new login fails, you can still fix things from the old one.
Cut the noise: fail2ban and firewalls
Keys make guessing harmless. fail2ban makes the log readable: it watches for repeated failures and blocks those addresses for a while in the firewall. On Debian and Ubuntu, sudo apt install fail2ban, then create /etc/fail2ban/jail.local:
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h
# Without /var/log/auth.log (newer Debian), read the journal:
# backend = systemd
Restart it with sudo systemctl restart fail2ban and check it with sudo fail2ban-client status sshd.
If only you need SSH, the strongest noise filter is a firewall rule that allows port 22 from your own addresses only, or a VPN in front of the server. Moving SSH to another port also cuts the noise a lot, but it's camouflage, not protection: a scanner will still find it.
What the addresses can't tell you
The addresses in auth.log are where the connections came from: often a Tor exit, a rented cloud server or someone else's hijacked machine. They don't identify a person, and blocking them one by one won't keep up. Reporting persistent attacks to the network's abuse contact (the IP lookup shows it) is reasonable for a cloud server that hammers you for days; for a few failed logins, it isn't worth your time.
Sources
Official documents we used to check the facts in this guide.