How to Check Failed SSH Login Attempts on Linux: lastb, journalctl and the Brute-Force Signal
CodexFuse Team · 2026-11-04T10:26:16.638Z · 12 min read min read
Monitor failed SSH logins on Linux: the lastb 30-second scan, journalctl forensics, per-IP counting without the awk field trap, and how to read the brute-force signal.
Every internet-facing server gets password-guessing attempts against SSH within minutes of going online — it is background noise on port 22. The difference between a server owner who is in control and one who is not is simple: the first checks their failed SSH login attempts regularly. On Linux, you have two native ways to do it. The command sudo lastb | head -20 gives you the 30-second answer from the system's bad-login log, and journalctl -u ssh gives you the forensic detail behind every attempt. This guide shows you how to check failed SSH login attempts in Linux with both, how to count SSH attempts per IP without falling into a classic awk trap, and how to read the brute-force signal — so you know exactly when it is time to hand off to our Fail2ban guide. The whole workflow applies on any VPS hosting server or bare-metal box.
What is the fastest way to check failed SSH logins on Linux?
The fastest check is the lastb command — sudo lastb | head -20 — which reads the binary /var/log/btmp file — the system's dedicated log of bad login attempts. If the output shows the same IP address appearing over and over with usernames like admin and root, someone is actively guessing passwords on your server. Here is the complete 5-minute workflow, from quick scan to verdict:
- Quick scan:
sudo lastb | head -20— shows recent failed logins with username, source, and time. - Detail:
sudo journalctl -u ssh --since "1 hour ago" --no-pager | grep "Failed"— full sshd log lines for the last hour. - Count:
sudo grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head— top attacking IPs by attempt count. - Verdict: one IP, dozens of attempts seconds apart, dictionary usernames = brute-force attack in progress; many different IPs with a few attempts each = background internet noise.
- Response: when the signal is real, move to automated blocking — covered in the Fail2ban section at the end.
How do I read the lastb output?
lastb is the same program as last, except it reads /var/log/btmp by default, which "contains all the bad login attempts" (util-linux lastb(1) man page). Because btmp is a binary file, you cannot read it with a text editor — lastb is the parser. Its columns are username, tty, source, and datetime. For SSH attempts the tty column reads ssh:notty, meaning the login came over SSH and was never given a terminal.
A typical example line looks like this (example values):
$ sudo lastb | head -5 root ssh:notty 203.0.113.45 Wed Nov 4 10:14 - 10:14 (00:00) admin ssh:notty 203.0.113.45 Wed Nov 4 10:14 - 10:14 (00:00) test ssh:notty 203.0.113.45 Wed Nov 4 10:15 - 10:15 (00:00) root tty1 Wed Nov 4 10:16 - 10:16 (00:00)
Notice that lastb is system-wide, not SSH-only: rows can show ssh:notty (SSH), tty1 (console), or pts/22 (terminal session) — it records bad logins from every service that writes utmp entries. The command needs root privileges (btmp is owned root:utmp with -rw-rw---- permissions), so sudo is required. And there is one gotcha worth knowing on a fresh machine: "The system only logs information in these files if they are present," so on a fresh install you may need sudo touch /var/log/btmp before anything is recorded. An empty btmp is genuinely good news — it looks like this, verified on a fresh box this week:
$ sudo lastb btmp begins Wed Nov 4 06:29:14 2026
How do I check failed SSH attempts with journalctl?
Where lastb gives you the summary, the classic journalctl sshd failed query gives you the evidence: sudo journalctl -u ssh --since "1 hour ago" --no-pager | grep "Failed". "show messages for the specified systemd unit," so journalctl -u ssh shows only sshd's journal entries. One naming quirk: the unit is ssh on Debian and Ubuntu, but sshd on the RHEL family — if the first command returns nothing, try the other. These flags come straight from the journalctl(1) man page: --since "1 hour ago" accepts relative times, --no-pager keeps output out of a pager, and -f continuously prints new entries as they are appended "until Ctrl-C is hit."
The lines you are hunting look like this (from a real SOC learning log, documentation IPs):
Oct 12 02:14:01 web01 sshd[4171]: Invalid user admin from 198.51.100.143 port 50310 Oct 12 02:14:03 web01 sshd[4171]: Failed password for invalid user admin from 198.51.100.143 port 50310 ssh2 Oct 12 02:14:07 web01 sshd[4182]: Failed password for root from 198.51.100.143 port 50322 ssh2
Learn to read the two forms, because they mean different things. A guess at a username that does not exist produces two lines — Invalid user admin from ... and then Failed password for invalid user admin from .... A guess at a real account produces only one: Failed password for root from .... This distinction is proven by Fail2ban's own OpenSSH filter, whose regex makes the "invalid user" part optional because both forms exist in the wild. The forensic meaning, from the SOC lab's message table: "Invalid user admin" = username does not exist on this server; "Failed password for invalid user admin" = password tried for a non-existent user; "Failed password for root" = real account, wrong password.
Where do these lines live? If your server runs Ubuntu and you searched for "failed SSH login attempts Ubuntu", this is your answer: on Debian and Ubuntu they are written to /var/log/auth.log by rsyslog; on RHEL, CentOS, Fedora, and Rocky the same messages go to /var/log/secure; and on any systemd system they are all queryable with journalctl -u ssh (or -u sshd on RHEL).
How do I count failed SSH attempts per IP address?
Counting attempts per IP turns a wall of log lines into a ranked suspect list. The classic grep "Failed password" /var/log/auth.log pipeline — the grep failed password auth.log pattern everyone quotes — has a trap in it, and you should know about it before you copy it. Here is the version from Tecmint's guide:
sudo grep "Failed password" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -rn | head -20
This works — but only for the plain variant of the log line. In the "invalid user" variant, the words "invalid user admin" shift every field by two, so the IP moves from field 11 to field 13. We verified this by constructing both canonical lines this week:
| Log line variant | Naive awk '{print $11}' returns | Correct IP field |
|---|---|---|
Failed password for root from 203.0.113.45 port 54321 ssh2 | 203.0.113.45 ✓ | $11 |
Failed password for invalid user admin from 203.0.113.45 port 54321 ssh2 | admin ✗ (the username!) | $13 |
The naive pipeline silently prints usernames instead of IPs on invalid-user lines, corrupting your counts. The fix is to anchor from the end of the line instead of the start, because the tail from IP port PORT ssh2 never moves:
sudo grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head
$(NF-3) is correct for all four layouts — plain and invalid-user variants, in both auth.log and journalctl output — verified by construction this week. A fully field-free alternative from the SOC lab works too: grep -oE "from [0-9.]+" grabs the IP by pattern instead of position.
What does a brute-force attack actually look like in the logs?
A brute-force attack has a recognizable anatomy, and once you have seen it you cannot unsee it. It has four parts:
- One IP, many attempts, seconds apart. The SOC lab's sample shows eight password attempts — seven failures, then a success — in about 23 seconds. "No person types that fast."
- Guessed usernames first.
Invalid user adminandtestshow the attacker working through a list of common names. - Then real accounts.
rootanddeployappear with no "invalid user" prefix — they exist on the server, so the attacker is now guessing passwords for real accounts. - The
Accepted passwordline. A success after many failures from the same address is "the most serious signal" — it means a confirmed compromise, not just an attempt.
A practical alerting rule from the same source: raise an alert when one source IP has 5 or more failed SSH logins within 10 minutes, and raise the severity if that same IP also gets an Accepted login in that window. Compare that against background noise: any server with port 22 open to the internet gets a steady stream of password-guessing attempts, "often within minutes of going online" — so judge by concentration per IP, not by raw totals.
lastb vs journalctl: which should I use?
Use lastb for the 30-second scan and journalctl when you need the full story — they are not rivals, they are two magnifications of the same picture. The disambiguation matters: lastb reads the binary btmp file, which "contains all the bad login attempts" across every login service, while journalctl -u sshd is a unit filter that "shows messages for the specified systemd unit" — sshd only, never console, su, or FTP logins.
lastb | journalctl -u ssh | |
|---|---|---|
| Source | Binary /var/log/btmp | systemd journal (PAM + sshd entries) |
| Scope | All login services (SSH, console, su, FTP) | sshd only |
| Detail per attempt | Summary row: user, tty, source, time | Full message: exact line, port, timestamps |
| Time filtering | None built in (pipe to grep) | --since, --until, -f live follow |
| Best for | "Is anyone knocking?" — the daily 30-second check | "What exactly happened?" — investigating one IP |
One more distinction that trips people up: lastb shows only failures. Its sibling last reads /var/log/wtmp and "shows successful login sessions" — check both when you suspect a compromise, because a failure storm followed by a success in last is the worst-case pattern.
Why is lastb empty or missing on my server?
If lastb returns nothing at all, run through this checklist before assuming you are safe. First, the btmp file itself must exist — the man page warns that the system "only logs information in these files if they are present," so create it with sudo touch /var/log/btmp on a fresh install. Second, confirm you are querying the right unit name: ssh on Debian/Ubuntu, sshd on RHEL-family systems. Third, know that the classic commands are being retired on the newest distros: the wtmp format "cannot store timestamps past 2038," so Debian 13, Ubuntu 24.10 and later, and Fedora 43 and later ship without last and lastb entirely — the replacement is sudo lslogins --failed, and Ubuntu 24.04 LTS still ships the original commands. Finally, an empty result on a server whose SSH port is closed to the internet is perfectly normal: no exposure, no knocks.
What should I do after spotting a brute-force attack?
Monitoring tells you what is happening; automated blocking is what stops it. One configuration detail worth knowing first: OpenSSH's MaxAuthTries "specifies the maximum number of authentication attempts permitted per connection. Once the number of failures reaches half this value, additional failures are logged. The default is 6" (sshd_config(5) man page). That limit is per connection, not per IP — the attacker just opens a new connection and keeps guessing, which is exactly why a rate-based blocker matters. The standard answer is Fail2ban: it "reads the log and bans an address after repeated failures." Our Fail2ban setup guide walks through installing it, enabling the sshd jail, and reading its status — it is the direct follow-up to everything in this article. For the wider picture, our 5-layer SSH brute-force protection guide covers the other layers: key-only authentication, non-standard ports, and connection rate limiting.
Sources
- util-linux lastb(1) man page — man7.org/linux/man-pages/man1/last.1.html (fetched 2026-11-04): "lastb is the same as last, except that by default it shows a log of the /var/log/btmp file, which contains all the bad login attempts."
- journalctl(1) man page — man7.org/linux/man-pages/man1/journalctl.1.html (fetched 2026-11-04):
-u"show messages for the specified systemd unit";--sinceaccepts relative times;--no-pager"do not pipe output into a pager";-fcontinuously prints new entries "until Ctrl-C is hit". - sshd_config(5) man page — man7.org/linux/man-pages/man5/sshd_config.5.html (fetched 2026-11-04): MaxAuthTries default is 6, "the maximum number of authentication attempts permitted per connection."
- FossLinux login-history guide — fosslinux.com (fetched 2026-11-04): binary btmp/wtmp explainer, btmp permissions
-rw-rw---- 1 root utmp, wtmpdb on Ubuntu 26.04+, "thelastcommand reads/var/log/wtmpand shows successful login sessions." - Tecmint failed-SSH-logins guide — tecmint.com (fetched 2026-11-04): the classic
grep "Failed password" /var/log/auth.log | awk '{print $11}'pipeline. - Linuxize last-command guide — linuxize.com (fetched 2026-11-04): wtmp 2038 limit; Debian 13, Ubuntu 24.10+ and Fedora 43+ ship without last/lastb;
lslogins --failedreplacement. - Fail2ban OpenSSH filter — github.com/fail2ban (fetched 2026-11-04): filter regex proving both "Failed password for invalid user" and "Failed password for" forms exist;
Connection from <HOST>requires LogLevel VERBOSE. - SOC lab SSH brute-force learning log — github.com/vipercipher/soc-lab-beginner (fetched 2026-11-04): verbatim message meanings, 5-in-10-minutes alert rule, signal anatomy, documentation-IP auth.log extracts.
- Webasha SSH log analysis — webasha.com (fetched 2026-11-04): "Signs of brute force: many failures within seconds, from one or a few addresses, for many different usernames"; "Fail2ban reads the log and bans an address after repeated failures."
- MangoHost auth-log monitoring guide — mangohost.net (fetched 2026-11-04):
sudo grep "Failed password" /var/log/auth.logandsudo lastb -10on Ubuntu.
Frequently Asked Questions
Does lastb show successful logins?
No. lastb reads /var/log/btmp, which holds only bad login attempts. Successful sessions live in /var/log/wtmp and are read with plain last. Check both when investigating — a failure storm in lastb followed by a success in last from the same IP is the compromise pattern.
Where are failed SSH login attempts stored on Linux?
On Debian and Ubuntu, sshd failures are written to /var/log/auth.log by rsyslog; on RHEL, CentOS, Fedora, and Rocky, the same messages go to /var/log/secure. On any systemd system you can also query the journal with journalctl -u ssh (Debian/Ubuntu) or journalctl -u sshd (RHEL family). The system-wide binary summary lives in /var/log/btmp, read with lastb.
Why does lastb show "ssh:notty"?
The second column of lastb output is the tty the login attempt came through. SSH attempts never get a terminal at the authentication stage, so they are recorded as ssh:notty. Console attempts show tty1, terminal sessions show pts/N.
Is it normal to have hundreds of failed SSH logins?
Yes. Any server with port 22 open to the internet gets a steady stream of password-guessing attempts, often within minutes of going online. Judge by concentration, not volume: five or more failures from one IP in ten minutes is an attack worth acting on; a few attempts each from hundreds of IPs is background noise. Either way, automate the blocking with Fail2ban.
Can I see which password the attacker tried?
No. sshd logs the username, source IP, port, and timestamp of each failed attempt, but never the password that was tried. That is by design — logging attempted passwords would turn your auth log into a password database.

