Spin up a VPS, and within minutes bots are knocking on port 22 trying root with a
dictionary of passwords. The usual advice - keys instead of passwords, fail2ban,
maybe a non-standard port - shrinks the attack surface but never removes it. The port
is still there, still public, still answering.
There’s a better move: take SSH off the public internet completely. Put the box on a
Tailscale tailnet - a private WireGuard mesh between your own
devices - and then close the public SSH port. Now the only way to reach it is to
already be on your network. A scanner on the open internet can’t even see that sshd
exists.
Here’s the full path, from a fresh Ubuntu 24.04 box to one that’s effectively invisible.
⚠️ Don’t lock yourself out. SSH hardening is the one place where a typo strands you. Keep your current root/console session open the whole time, and only close it once you’ve opened a new session the hardened way. Every VPS provider also gives you a web console as a fallback - know where it is before you start.
1. Update everything first
Nothing else matters if the kernel and OpenSSH are months behind. Bring the box current:
sudo apt update && sudo apt full-upgrade -y
sudo apt autoremove --purge -y
If the kernel was upgraded, reboot (sudo reboot). Then make security updates automatic
so the box stays patched without you babysitting it:
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Unpatched software is the most common way a box gets owned - and the easiest to fix.
2. Install Tailscale
This is the private path you’ll lock everything down to, so it goes in before you touch SSH. The install script handles every distro:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
Follow the printed URL to authenticate, and the machine joins your tailnet with a stable
100.x.y.z address:
tailscale ip -4 # the tailnet address for this box
tailscale status # everything on your tailnet
Two things worth doing in the admin console while you’re here:
- Disable key expiry for this machine (or bring it up with a pre-authorized auth key). A server shouldn’t drop off the tailnet because an interactive key expired.
- Enable MagicDNS, so you can
ssh boxinstead of memorising100.x.y.z.
3. Create a non-root user
Logging in as root means every session - and every mistake - runs with full
privileges, and gives bots a username they already know. Create a normal user with
sudo rights instead:
sudo adduser wihan
sudo usermod -aG sudo wihan
Give that user your SSH key. The tailnet is already up, so the easiest route is from your laptop:
ssh-copy-id wihan@<tailnet-ip-or-magicdns-name>
Or set it up by hand on the server:
sudo install -d -m 700 -o wihan -g wihan /home/wihan/.ssh
sudo -u wihan tee /home/wihan/.ssh/authorized_keys >/dev/null <<'KEY'
ssh-ed25519 AAAA... your-key-here
KEY
sudo chmod 600 /home/wihan/.ssh/authorized_keys
Now open a fresh terminal and confirm ssh wihan@box works and sudo works before
going any further. If it doesn’t, fix it now while you still have your root session.
💡 Generate keys with
ssh-ed25519, not RSA - shorter, faster, and the modern default:ssh-keygen -t ed25519.
4. Harden SSH
Three changes do the heavy lifting: no root, no passwords, and - the one that makes this post worth writing - SSH only over the tailnet.
Put it all in a drop-in file rather than editing the stock sshd_config:
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf >/dev/null <<'CONF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AllowUsers wihan
MaxAuthTries 3
LoginGraceTime 20
X11Forwarding no
CONF
No root login over SSH
PermitRootLogin no removes the single most-targeted account from SSH entirely. You’ve
got a sudo user now; use it.
No password authentication
PasswordAuthentication no (plus KbdInteractiveAuthentication no) means a key is the
only way in. Brute force becomes impossible - there’s nothing to guess.
⚠️ Cloud images fight you here. Many providers ship a
/etc/ssh/sshd_config.d/50-cloud-init.confthat setsPasswordAuthentication yes, andsshdhonours the first value it reads - so an earlier-sorting file wins over your99-one. Check and clear it:sudo grep -rE 'PasswordAuthentication|PermitRootLogin' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/Comment out any conflicting
yeslines you find.
SSH only on the tailnet
The public port is the part everyone leaves open. Close it. Use UFW to deny inbound
traffic by default and allow SSH only on the tailscale0 interface:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow in on tailscale0 to any port 22 proto tcp
sudo ufw --force enable
That’s the whole trick. Port 22 still listens, but the firewall only lets packets reach it if they arrived over the tailnet. From the open internet the box looks like it has no SSH at all. If you run public services (a web app on 80/443, say), allow those explicitly - and only those:
sudo ufw allow 443/tcp
Note what we are not doing: sudo ufw allow 22. That would reopen SSH to the world
and undo everything.
Apply it - carefully
Validate the config before restarting, or a syntax error takes sshd down with it:
sudo sshd -t && sudo systemctl restart ssh
Then, without closing your current session, open a new one over the tailnet to prove it still works:
ssh wihan@box
Only once that succeeds should you close the original session.
A few more worth doing
| Setting | Why |
|---|---|
AllowUsers wihan |
Even with keys, only named accounts may log in. |
MaxAuthTries 3 |
Cuts off probing connections fast. |
X11Forwarding no |
You don’t need it on a server; turn it off. |
And two things people reach for that you can now skip:
- Changing the SSH port. Pure security theatre once the port is off the public internet - there’s no one out there to hide it from.
fail2ban. It exists to ban brute-forcers, and you no longer have any: nothing public can reachsshd. Keep it only if you expose other public services.
For the strongest version of this, look at Tailscale SSH:
sudo tailscale up --ssh lets the tailnet handle SSH auth via your identity and ACLs,
so you can drop sshd from the public box entirely.
Verify you’re locked down
From your laptop, run through the checklist:
ssh wihan@box # ✅ works (over the tailnet)
ssh root@box # ❌ permission denied
ssh wihan@<public-ip> # ❌ connection times out - port is closed publicly
If the first succeeds and the other two fail, you’re done: a box you can administer comfortably, that the rest of the internet can’t even find.
Further reading
- Chris Titus - Linux Security Mistakes
- Dreams of Code - Setting up a production-ready VPS from scratch
- Tailscale documentation