Blog
Reading an SSH Brute-Force Log: Starting with fail2ban
Monday, 07:42, and the email holds fail2ban's summary: twenty-three IPs banned in the last twelve hours, jail name sshd. No disk alerts, no "Accepted password" successes, nothing urgent. I still copied that day's auth.log and fail2ban logs onto the analysis box as a small forensics exercise — what exactly is happening on public port 22, and are the existing controls stopping what they should?
The alert, and what I ruled out first
The alert body only had counts and the jail name, no attacker intent. First I ruled out three scarier possibilities: any successful login with an unusual account, a session opened followed by abnormal sudo, and the same source sweeping other ports in a short window (cross-checked with firewall or web logs).
grep -E 'Accepted (password|publickey)' /var/log/auth.log | grep -v 'cron'
grep 'Failed password' /var/log/auth.log | wc -l
zgrep -h 'Ban' /var/log/fail2ban.log* | tail -n 50
Result: the only successful logins were my own key sessions, timestamps matching my work habits; thousands of failures; and the Ban/Unban pattern in waves, consistent with "scan a block, move to the next" automation. First conclusion, written in my notes: no sign of compromise — elevated background noise worth profiling, not a reason to touch production at midnight.
Rebuilding the attack profile from the Failed lines
The failure records are highly templated. Username side: root, admin, ubuntu, test, oracle, postgres rotating, with the occasional common dictionary name mixed in. Source IPs mostly sit in cloud-host ranges, geographically scattered, each with a short lifetime — try a few dozen times, get banned, switch exit. Almost no sustained guessing against my actual usernames.
grep 'Failed password' /var/log/auth.log \
| awk '{print $(NF-3)}' | sort | uniq -c | sort -nr | head
grep 'Invalid user' /var/log/auth.log \
| awk '{print $8}' | sort | uniq -c | sort -nr | head
This profile says most of it is indiscriminate internet scanning, not someone who has my home NAS in their sights. That doesn't make the danger zero: if password login were still on with a weak password, statistics would eventually land; and if a private key were stolen, the logs would wear an entirely different face — successful publickey logins, unusual sources, files being poked right after login. This sample had none of that.
Time-wise, busy overnight in UTC and quieter in the morning — consistent with global botnet schedules. No escalation curve of "only attacks during my work hours, and the usernames keep getting closer."
Checking against the controls in place
The controls already live on that box during the investigation: sshd key-only, password and root password login disabled, fail2ban's sshd jail, and ufw default-deny inbound with only necessary ports open. Against the logs: no successful brute force; the disk cost was mainly log volume; and fail2ban cutting off repeat sources truncated consecutive failures from the same IP, lowering the noise.
For "background radiation," that stack is enough. The threat model it doesn't cover is a different table: an sshd zero-day, a key leaking from your laptop, or another opening at the application layer. SSH hardening never replaces system updates and shrinking the attack surface. fail2ban is noise reduction and delay, not the root of access control.
Did the same IPs hit other doors?
I took the five most frequent IPs from the ban list and searched web access logs and the router's connection tracking. Two of them had hit 80/443 path scans at nearby timestamps (common PHP admin paths, .env probes); the other three only showed up on 22. So some sources are "sweep all or common ports" scripts and some are dedicated SSH dictionary bots. For a home stack the implication: web reverse proxy and SSH protections need separate designs — don't assume "SSH is hardened, so I'm immune everywhere."
Remediation and tightening, not just a write-up
No dramatic emergency changes that day. I did four reversible things: confirmed PasswordAuthentication no was still in effect; tuned fail2ban's bantime and findtime to this box's actual noise level; recorded a "normal noise baseline" in my notes — tens of banned IPs daily, highly templated usernames, no targeted real usernames; and started the plan to pull admin SSH into WireGuard, ending with port 22 closed in the public security group.
The second week after moving into the tunnel, auth.log was so quiet it felt strange. That was the steady state I wanted: fewer alert emails, not because we look away, but because the listening surface itself disappeared.
At the end of my forensics notes I keep a fixed "if this alert comes again" checklist: first search for Accepted and real usernames, then compare this week's ban count for an order-of-magnitude change, and only then decide whether to touch sshd or the security group. Writing up one mundane brute-force attempt is about having a baseline for next time — what Tuesday-morning normal radiation looks like, versus usernames suddenly becoming your email prefix, or an Accepted that shouldn't exist. Most of security work is keeping judgment in the noise, not escalating every ban into an incident.