Failed SSH Login Attempts: Find, Count, and Harden
Last verified on Nov 8, 2026.
SSH servers on the internet get password guesses within minutes of being exposed, so a long list of failed logins is normal. What matters is whether any of it succeeded, and whether the pattern points at a targeted attempt instead of background scanning. This guide shows where sshd writes its events, what the messages look like, how to count and filter them, and how to separate noise from a login you need to investigate.
Where sshd Logs
sshd logs through syslog at the AUTH facility by default, and at log level INFO. Where those messages land depends on the distribution:
If your system logs only to the journal, there may be no auth.log at all. For the journal, the useful options are -u (unit), -t (syslog identifier), -g (grep, a pattern match), --since, and -o json. The journalctl guide covers them in more depth.
One change affects how you filter. Since OpenSSH 9.8, sshd is split into a listener and a per-session binary, and the release notes say some log messages are now tagged as coming from a process named sshd-session instead of sshd. A filter on the literal string sshd[ misses those lines. Match on the message text, or select both identifiers in the journal:
sudo journalctl -t sshd -t sshd-session --since "24 hours ago" --no-pagerWhat the Messages Look Like
The formats come from the OpenSSH source, in auth.c. A login attempt is logged as Accepted or Failed, then the authentication method, then the user, source address, and port. An unknown user gets invalid user inserted:
2026-11-08T03:14:07.512340+00:00 web-1 sshd[21873]: Failed password for root from 203.0.113.50 port 51822 ssh2
2026-11-08T03:14:09.201118+00:00 web-1 sshd[21875]: Invalid user admin from 203.0.113.50 port 51840
2026-11-08T03:14:11.774902+00:00 web-1 sshd[21875]: Failed password for invalid user admin from 203.0.113.50 port 51840 ssh2
2026-11-08T09:22:41.003311+00:00 web-1 sshd[22410]: Accepted publickey for deploy from 198.51.100.7 port 40210 ssh2: ED25519 SHA256:Zt1x...
2026-11-08T09:25:02.660021+00:00 web-1 sshd[22433]: Accepted password for root from 203.0.113.50 port 52011 ssh2Reading them:
Failed password for <user>: a password was tried for a user that exists. The method name (password,publickey,keyboard-interactive) changes with how the attempt was made.Invalid user <user> from <ip>: the username does not exist on the host. This is scanning for common names.Failed password for invalid user <user>: the follow-up for the same attempt.Accepted <method> for <user> from <ip>: a successful authentication. For public keys the line ends with the key type and fingerprint.maximum authentication attempts exceeded for ...: the connection used up its allowed attempts.
Expect fewer lines than attempts. MaxAuthTries defaults to 6, and per the man page, once the number of failures reaches half of that value, additional failures are logged. A client that tries two passwords and disconnects may leave no Failed line.
lastb, last, and lastlog
Three commands read binary login records, which is useful when text logs have been rotated or tampered with:
sudo lastb -i -F | head -30 # bad login attempts, by IP, with full timestamps
last -i -F | head -30 # successful logins and logouts
lastlog -t 7 # users whose most recent login was in the last 7 days
lastlog -u deploy # last login for one accountlastb is last reading /var/log/btmp, which records bad login attempts. The util-linux documentation notes that wtmp and btmp are only written if the files exist, so on a host where btmp was never created, lastb will show nothing and tell you nothing. lastlog shows each account's most recent login and prints **Never logged in** for accounts that have not. A service account that normally never logs in interactively but has a recent lastlog entry is worth a look.
Counting Failures
Count failed passwords by source address. The awk takes the field after from, which works for both valid and invalid users:
sudo grep "Failed password" /var/log/auth.log \
| awk '{for(i=1;i<=NF;i++) if($i=="from") print $(i+1)}' \
| sort | uniq -c | sort -rn | head -20Include rotated logs, which are often compressed after rotation:
sudo zgrep -h "Failed password" /var/log/auth.log* \
| awk '{for(i=1;i<=NF;i++) if($i=="from") print $(i+1)}' | sort | uniq -c | sort -rn | headUse /var/log/secure* on RHEL, or the journal:
sudo journalctl -u sshd --since "7 days ago" -g "Failed password" -o cat --no-pager \
| awk '{for(i=1;i<=NF;i++) if($i=="from") print $(i+1)}' | sort | uniq -c | sort -rn | headWhich usernames are being guessed:
sudo grep -hoE "Invalid user .* from" /var/log/auth.log | sed -E 's/Invalid user (.*) from/\1/' \
| sort | uniq -c | sort -rn | head -20Failures per hour show whether an attack is ongoing or finished:
sudo grep "Failed password" /var/log/auth.log | cut -c1-13 | sort | uniq -c(The cut assumes the 2026-11-08T03 timestamp style. For the older Nov 8 03:14:07 style, use cut -c1-9.)
Brute Force or a Real Login?
A count by IP tells you who is noisy. It does not tell you whether anyone got in. Check for addresses that appear in both failures and successes:
sudo grep -E "(Failed|Accepted) (password|publickey|keyboard-interactive) for" /var/log/auth.log \
| awk '{
ip=""; for(i=1;i<=NF;i++) if($i=="from") ip=$(i+1);
if ($0 ~ /Failed/) f[ip]++; else a[ip]++
}
END { for (ip in a) if (f[ip] > 0) printf "%-40s failed=%d accepted=%d\n", ip, f[ip], a[ip] }'An address with failures and then Accepted is the case to investigate. Then pull everything that address did, in order:
sudo grep "203.0.113.50" /var/log/auth.logSigns that point to a successful brute force:
- Failures, then
Accepted passwordfor the same user from the same IP. Sometimes the first failures are forroot, then the right guess comes for a different account. Accepted passwordfor a user who only ever uses keys. If your hosts are key-only, anyAccepted passwordis an anomaly.- A login from a new country or provider for a user with a stable history, using
last -ifor comparison. - Activity right after the login (see access anomalies for what follows a successful entry):
sudouse, new entries in~/.ssh/authorized_keys, new cron jobs, and outbound connections. TheAcceptedline is the start of the timeline, not the end.
Signs that point to noise:
- Thousands of
Invalid userlines from many addresses, none withAccepted. - One address trying dozens of usernames with one or two attempts each, then moving on.
- Failures that stop when you add a firewall rule. Scanners also fire payloads at web ports from the same addresses, so cross-check with attack payloads.
If you find a real success, treat the host as compromised: preserve the logs and btmp/wtmp, check authorized_keys for every account, and look for persistence before you rotate anything. The authentication anomalies guide covers detecting this pattern continuously.
Hardening sshd
Disable password authentication and the guessing stops mattering. In a drop-in file, so package upgrades do not overwrite it (Ubuntu includes /etc/ssh/sshd_config.d/*.conf at the top of sshd_config):
# /etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
MaxAuthTries 3
LoginGraceTime 30
AllowUsers deploy adminValidate and apply, keeping an existing session open in case you lock yourself out:
sudo sshd -t && sudo systemctl restart ssh # "sshd" on RHEL-family systemsNotes on the settings, from the sshd_config man page. PasswordAuthentication defaults to yes. PermitRootLogin defaults to prohibit-password, which blocks password and keyboard-interactive for root but allows keys. AllowUsers limits login to names that match. Newer OpenSSH also enables PerSourcePenalties by default, which briefly refuses connections from sources that fail authentication or probe invalid users. The default penalties are short (seconds), so it slows scanning rather than stopping it.
With password login off, failed password lines will still appear, because bots keep trying. They become noise instead of risk.
fail2ban
fail2ban scans log files such as /var/log/auth.log and bans addresses that cause repeated authentication errors. It ships an [sshd] jail in jail.conf; jails are disabled unless enabled = true. Put overrides in jail.local:
# /etc/fail2ban/jail.local
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1hThe defaults in jail.conf are maxretry = 5, findtime = 10m, and bantime = 10m: a host is banned if it generates maxretry failures during the last findtime. Check the result with sudo fail2ban-client status sshd. fail2ban reduces the noise and the rate of guessing. It is not a replacement for key-only authentication, and a distributed attack from many addresses will stay under any per-address limit.
Centralizing SSH Logs
Local logs are the first thing an intruder edits. Ship them off the host. Sazabi, an AI-native observability and security platform, reads sshd authentication messages from journald, or from /var/log/auth.log or /var/log/secure on hosts that write files, alongside Linux auditd records, using Fluent Bit or the OpenTelemetry Collector. See the auditd source docs for the setup. Once the messages are flowing you can ask which addresses had failures and then a success in the same hour, in plain language, instead of running the awk above on each host.
Frequently Asked Questions
Where are failed SSH login attempts logged?
In /var/log/auth.log on Debian and Ubuntu, and /var/log/secure on RHEL-family systems. On systems that use journald, run journalctl -u ssh (Debian, Ubuntu) or journalctl -u sshd (RHEL) to read the same messages.
How do I count failed SSH logins by IP address?
Filter for "Failed password" lines and extract the address after the word from, then sort and count: grep "Failed password" /var/log/auth.log | awk '{for(i=1;i<=NF;i++) if($i=="from") print $(i+1)}' | sort | uniq -c | sort -rn.
How do I tell a brute force attempt from a successful login?
Look for an "Accepted" line from the same IP that produced the failures. Failures only are noise. A burst of failures followed by Accepted from one address is the case to investigate.
What does "Invalid user" mean in the SSH log?
The username does not exist on the host. Bots guess names like admin, test, and ubuntu, so invalid user lines are mostly scanning and tell you little on their own.
Sources
- LogLevel defaults to INFO; MaxAuthTries defaults to 6 and failures are logged once half that value is reached; PasswordAuthentication defaults to yes; PermitRootLogin defaults to prohibit-password; PerSourcePenalties settings and defaults (accessed Nov 8, 2026)
- OpenSSH source: the log formats "Accepted|Failed <method> for [invalid user ]<user> from <ip> port <port> ssh2" and "Invalid user <user> from <ip> port <port>" (accessed Nov 8, 2026)
- OpenSSH 9.8 split sshd into sshd and a per-session sshd-session binary, so some log messages are tagged sshd-session instead of sshd (accessed Nov 8, 2026)
- On Ubuntu, authentication events such as sshd remote logins and sudo use are in /var/log/auth.log (accessed Nov 8, 2026)
- On RHEL, /var/log/secure holds security and authentication-related messages (accessed Nov 8, 2026)
- journalctl options -u/--unit, -t/--identifier, -g/--grep, -S/--since, and -o json (accessed Nov 8, 2026)
- lastb is last reading /var/log/btmp, which holds bad login attempts; btmp and wtmp are only written if the files exist; -i shows IP addresses and -F full times (accessed Nov 8, 2026)
- lastlog prints the most recent login of each user; -t DAYS limits to recent logins and -u selects a user (accessed Nov 8, 2026)
- fail2ban jail defaults: bantime 10m, findtime 10m, maxretry 5; jails are disabled unless enabled = true; an [sshd] jail ships in jail.conf (accessed Nov 8, 2026)
- Sazabi reads sshd auth messages from journald or auth.log/secure along with auditd records, via Fluent Bit or the OpenTelemetry Collector (accessed Nov 8, 2026)