← tuanphung.dev
A security log

242,296 login attempts
in a week, zero break-ins

I had Claude run a routine security check on the little VPS that hosts this site. Nothing specific — just "have a look." So it read the system journal, and the journal had a lot to say: in the previous seven days, the server had logged 242,296 failed SSH logins. Claude checked every username they tried against the server's config. The surprising part isn't that someone's knocking — it's that not one of those attempts could ever have worked, and the fix to keep it that way is three small changes.

The one-sentence version

A quarter-million brute-force attempts is just internet weather — it happens to every public server, all the time, like rain. It is harmless the moment the server only accepts SSH keys, with no password door for anyone to push on.

Every public IP with port 22 open gets this. Botnets sweep the whole internet around the clock, trying admin / admin on every address they find. It looks scary in the logs and means almost nothing — if you've shut the one door they're actually pushing on.

How it surfaced

The whole thing started as one short message from me. Here's roughly how our conversation went (server names and addresses scrubbed):

security-check — claude code
can you run a security check on our server?
Claude
Done — read-only audit, nothing changed. The box isn't compromised, but two things stand out:
SSH is open to the internet and accepts passwords242,296 failed logins in 7 days.
5 kernel security updates installed but waiting on a reboot.
Good news: every account is key-only, so none of those attempts can succeed. Still worth locking down properly.
wait — there were really 242,296 attempts? how do you know?
Claude
Straight from the system journal — sshd logs every failed attempt, I just counted them:
journalctl -u ssh --since "7 days ago" \
  | grep -ciE "failed|invalid user"
242296
It's not targeted at you — it's botnets sweeping the whole IPv4 range. They're mostly guessing admin, ubuntu, postgres
ok, fix it — disable passwords + reboot
Claude
Done, and verified I didn't lock us out first:
Password login disabled — keys only.
Per-IP rate limit on port 22; repeat offenders get a 1-hour ban.
Updates applied, rebooted onto the new kernel — all sites back up.

A short, cleaned-up recreation of the actual session. That's me on the left.

What a quarter-million knocks looks like

Claude read through a quarter-million failed password guesses so I didn't have to. It is less exciting than it sounds. Once you see the breakdown, the fear drains out of the big number — this is a few dozen botnets running the same tired word list. Here's the week, by the day:

Mon ███████████████████░░░░░ 17,670 Tue ████████████████░░░░░░░░░ 15,736 Wed ██████████████████░░░░░░░ 17,103 Thu ███████████████░░░░░░░░░░ 13,960 Fri ██████████████░░░░░░░░░░░ 13,252 Sat █████████████████████████ 22,976 Sun ██████████████████░░░░░░░ 16,926
~13k–23k attempts every single day. None succeeded.

Top usernames they tried

Username guessed Attempts Why
admin16,814The eternal first guess
ubuntu3,337Default cloud-image user
user1,812Hope springs eternal
solana1,717Hunting crypto validators
postgres501Looking for an open database

Notice what's not there: my actual username. They're not attacking me — they're trying default names on every IP that exists and seeing what sticks. Some of them spent 501 tries on postgres, a database user this server doesn't even have. It's a burglar walking down the street, trying every door handle — including the doors that don't exist.

Why none of it can win

A brute-force attack guesses passwords. If the server doesn't accept passwords — only cryptographic keys — there's nothing to guess. The attacker would need to steal a private key that never leaves my laptop, not type the right word. That single setting turns the whole flood into harmless noise. Claude checked the config: every account on this box is key-only. The attackers were guessing answers to a question the server stopped asking.

Password login on
A door with a lock
Every guess is a real attempt at the lock. Given enough time and a weak password, one eventually fits.
Keys only
A wall with no keyhole
There's nothing to pick. The guesses bounce off. The only key lives on your own machine and is never sent over the network.

The three changes

This is what Claude and I set up, in order of how much each one matters. The first one does almost all the work; the other two are good habits on top.

01

Turn off password logins

Generate a key pair on your laptop (ssh-keygen -t ed25519), copy the public half to the server (ssh-copy-id), confirm you can log in with it — then close the password door. Drop a file in /etc/ssh/sshd_config.d/:

PasswordAuthentication no
KbdInteractiveAuthentication no

Then sudo sshd -t to check the syntax and sudo systemctl reload ssh. Keep your current session open and test a fresh login in a second terminal before you trust it. That open session is your way back in if you got something wrong. Claude tested the fresh login before reloading anything — locking me out of my own server would have been a short career for it.

02

Rate-limit the knocking

Brute force can't break in, but a quarter-million log lines a week is still noise that hides the events you'd actually want to see. A firewall rule that slows down repeat connections from the same address — and bans the worst offenders for an hour — keeps the journal readable. The shape of it, in nftables:

# new SSH connections from one IP, faster than
# ~1/min sustained (burst 10) → drop & remember it
tcp dport 22 ct state new \
  meter scan { ip saddr limit rate over 1/minute burst 10 packets } \
  add @offenders { ip saddr } drop

# anyone in the offenders set is dropped on sight
tcp dport 22 ip saddr @offenders drop

Tools like fail2ban do the same job by watching the logs instead of the packets — either is fine. The point is to stop spending CPU and disk on people who can't get in anyway.

03

Better still: take the door off the street

The strongest move is to not expose port 22 to the public internet at all. Put the server on a private mesh network — I use Tailscale — and reach it over that. The attacks don't slow down; they simply can't find the door. (One caveat: if something like a CI pipeline needs to SSH in from the public internet, you either keep a rate-limited public port for it or move that pipeline onto the mesh too.)

And the boring one: actually reboot

My box had five kernel security patches installed but never activated — it had been up 56 days. Unattended upgrades download and install fixes, but a kernel patch only takes effect after a restart. If your uptime is measured in months, you're probably running a kernel with known holes. Check for /var/run/reboot-required, pick a quiet moment, and reboot. (Check that your services come back up afterwards — one of ours lost a race for a network port on boot and needed a small push. That's a story for another note.)

A quarter-million failed logins is what a public SSH port sounds like — it's weather, not an attack on you. The number that matters isn't 242,296; it's zero, the count of attempts that could ever have worked. Turn off passwords, rate-limit the rest, and reboot for your patches. Then let something like Claude read the auth log, and go read something more interesting yourself.