Bar chart of failed SSH login attempts in 24 hours to 3 November 2026 on three public servers with password login off: US 2,234, Germany 793, India 331. A second chart for the India server: first 24 hours after setup 2,902, second 24 hours 241.

We read 24 hours of SSH logs on three public servers we run. They took 3,358 login attempts between them, about 1,100 per server per day, from 85 to 155 different addresses each. On the server we set up most recently, the first attempt came 5 minutes 40 seconds after its logs began. None of this needs anyone to know your server exists: bots scan every public address all day.

This guide locks a new Ubuntu 24.04 server down in the order that can't lock you out: a sudo user, then the firewall, then SSH, then fail2ban. We ran every command on a clean Ubuntu 24.04.5 server (1 vCPU, 2 GB, kernel 6.8) on 3 November 2026, with a second server attacking it, and we kept the two traps we found: an SSH config file whose name decides whether it works, and a fail2ban default that ignores most of what bots do on a key-only server.

What 24 hours of attacks looks like

These are the 24 hours up to about 01:20 UTC on 3 November 2026. All three servers already had password login off and fail2ban on, so nothing got in. "Attempts" counts connections that gave up at the login stage; a bot can't even try a password once the server only offers keys.

ServerLogin attemptsAs rootAs a made-up userDifferent addressesfail2ban bans
US2,2341,51157315513
Germany79355112613238
India331296105859

The columns come from different log lines, so they don't add up exactly. Two things stand out. Root is the top target by far, which is why root login goes off below. And the attacks are spread out: dozens of addresses, a few tries each, so bans only slow this down. It's the keys that stop it.

The most tried usernames across the three servers, not counting root:

UsernameTries in 24 hours
admin153
r00t24
user16
test13
debian11
oracle8
sammy7
ftpuser6

The India server also shows how a new address gets found. It took 2,902 attempts from 95 addresses in its first 24 hours from 31 October, then 241 the next day and 331 by 3 November. We can't say why the first day was the loudest, but it's a good reason to do this before anything else, not at the end of the week.

One limit of this data: we won't open password login on a public server to count what happens, so we have no "before" column. What we can tell you from the test below is what password login being off looks like to a client.

What it costs

MeasureResult
Memory used, clean server288 MB of 2 GB
Memory used, after all steps and a reboot312 MB
fail2ban process33 MB
Disk240 MB more, including package lists
fail2ban install time47 s, including apt-get update

Step 1: a sudo user with your key

Log in as root and leave that session open until the end of the guide. It's your way back if something goes wrong. Then make a user and give it your key. If your provider logged you in as ubuntu, it's already your sudo user and you can skip this.

bash
adduser --disabled-password --gecos '' deploy
usermod -aG sudo deploy
echo 'deploy ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-deploy
chmod 440 /etc/sudoers.d/90-deploy
visudo -cf /etc/sudoers.d/90-deploy      # must say "parsed OK"

install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
chown deploy:deploy /home/deploy/.ssh/authorized_keys
chmod 600 /home/deploy/.ssh/authorized_keys

No password means sudo can't ask for one, so this rule skips the prompt. The key then is what protects the account, so guard it, and put a passphrase on it. From your own computer, in a new terminal:

bash
ssh deploy@YOUR_SERVER 'id -un; sudo -n whoami'
# deploy
# root

Don't go on until that prints both lines.

Step 2: the firewall, with SSH allowed first

ufw is installed on Ubuntu but off. Allow SSH before you turn it on. Add anything else you serve now, such as ufw allow 80,443/tcp.

bash
ufw allow OpenSSH
ufw default deny incoming
ufw default allow outgoing
ufw --force enable
ufw status verbose

Check from your own computer that SSH still works and that the rest is closed. From our second server with nmap:

text
nmap -Pn -p22,80,3306,5432 YOUR_SERVER
# 22/tcp   open     ssh
# 80/tcp   filtered http
# 3306/tcp filtered mysql
# 5432/tcp filtered postgresql

Docker ignores these rules for published ports, which we tested and fixed in the Docker guide.

Step 3: SSH keys only, and the file name trap

Ubuntu's SSH reads the files in /etc/ssh/sshd_config.d/ in name order, and the first value it finds for a setting wins. Images from different places ship their own files here, and some have a 50-cloud-init.conf with PasswordAuthentication yes. We tried it:

Files in sshd_config.dsshd -T says
50-cloud-init.conf (yes) and 99-hardening.conf (no)passwordauthentication yes, your setting is ignored
00-hardening.conf (no) and 50-cloud-init.conf (yes)passwordauthentication no

So name yours to sort first:

bash
cat > /etc/ssh/sshd_config.d/00-hardening.conf <<EOF
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
MaxAuthTries 3
X11Forwarding no
EOF

sshd -t && echo syntax ok       # never reload without this
sshd -T | grep -E '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin|maxauthtries|x11forwarding) '
systemctl reload ssh

sshd -T prints what the server will really use, so trust it over reading the files. Your root session stays open through the reload. Now test from a new terminal before you close anything:

bash
ssh deploy@YOUR_SERVER 'echo key login ok'
ssh root@YOUR_SERVER true        # root@...: Permission denied (publickey).

Prove password login is off

Don't take the config's word for it. Ask the server which methods it offers, with no key in play:

text
ssh -o PreferredAuthentications=none -o BatchMode=yes nobody@YOUR_SERVER true
# nobody@...: Permission denied (publickey).

nmap -p22 --script ssh-auth-methods --script-args ssh.user=root YOUR_SERVER
# |   Supported authentication methods:
# |_    publickey

If you see password in either answer, it isn't off. We went one step further: we gave the deploy user a real password and tried it with sshpass. The right password got Permission denied (publickey), so we removed the password and locked it again.

Step 4: fail2ban, with the setting that matters

Ubuntu's package turns on an SSH jail by itself, with a 10 minute ban after 5 failures. But the default filter mode doesn't count what bots do on a key-only server. We attacked our test server 12 times in a row with a wrong key, starting fresh for each line:

Filter modeTarget userFailures countedBanned?
normal (default)root0No
normal (default)admin (doesn't exist)9Yes
aggressiveroot31Yes
aggressiveadmin (doesn't exist)31Yes

Root is the most tried name in our logs, and the default mode ignored all 12 of those attempts. So set mode = aggressive, and use longer bans that grow for repeat visitors:

bash
apt-get update
apt-get install -y fail2ban

cat > /etc/fail2ban/jail.local <<EOF
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 1w
ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled = true
backend = systemd
mode = aggressive
EOF

systemctl enable --now fail2ban
systemctl restart fail2ban
fail2ban-client status sshd

Add your own fixed IP to ignoreip if you have one. A failed key at home can ban you too. Your open root session keeps working during a ban, and this gets you back in:

bash
fail2ban-client set sshd unbanip YOUR_IP

Prove it bans

From a second server, not your own computer, try a few logins with a user that doesn't exist. With these settings, on mode = aggressive, each attempt counted twice, so ours was banned on the third try and got this from then on:

text
ssh: connect to host YOUR_SERVER port 22: Connection refused
bash
fail2ban-client status sshd
#  |- Currently banned: 1
#  `- Banned IP list: 192.168.78.2

nft list ruleset | grep -A3 f2b          # the ban is an nftables rule, not iptables

That's the other small surprise: on Ubuntu 24.04 the package bans through nftables, so iptables -S shows nothing and you may think it failed.

Step 5: reboot, and check it all again

bash
reboot

# then, from a new terminal:
ssh deploy@YOUR_SERVER
systemctl is-active ssh fail2ban         # active, active
sudo ufw status | head -1                # Status: active
sudo sshd -T | grep ^passwordauth        # passwordauthentication no

All of them held on our test server. Next come automatic security updates, and if you've got private services to reach, a WireGuard VPN lets you close SSH to the world altogether.

Which of our servers can run this

All of them, every step. We ran ufw on a server under $20 and it works there too. On those servers your public SSH port isn't 22: use the one the console shows you, and ufw allow OpenSSH is still right, because the port on the inside is 22. If you ordered with a password instead of an SSH key, put your key on the server first, because step 3 turns passwords off.

Sources

Our own run on Ubuntu 24.04.5, kernel 6.8.0-147, QEMU/KVM, fail2ban 1.0.2, 3 November 2026. Attack counts are from the ssh journal of three public servers we run, read on 3 November 2026; the first 24 hours are the India server's from 31 October. Docs: sshd_config manual (first value wins), ufw manual, fail2ban on GitHub.

Pick a region and a budget and see your best servers, all on Ubuntu 24.04 and ready for these steps.

See the servers