Take SSH off the public internet entirely - go from a fresh Ubuntu box to one you can only reach over your private tailnet.
5 minlinux · security · tailscale · ssh · vps
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:
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:
bash
curl -fsSL https://tailscale.com/install.sh | shsudo tailscale up
Follow the printed URL to authenticate, and the machine joins your tailnet with a stable
100.x.y.z address:
bash
tailscale ip -4 # the tailnet address for this boxtailscale 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 box instead of memorising 100.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:
bash
sudo adduser wihansudo usermod -aG sudo wihan
Give that user your SSH key. The tailnet is already up, so the easiest route is from your
laptop:
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:
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.conf that sets PasswordAuthentication yes, and
sshd honours the first value it reads - so an earlier-sorting file wins over your
99- one. Check and clear it:
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:
bash
sudo ufw default deny incomingsudo ufw default allow outgoingsudo ufw allow in on tailscale0 to any port 22 proto tcpsudo 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:
bash
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:
bash
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:
bash
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 reach sshd. 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:
bash
ssh wihan@box # ✅ works (over the tailnet)ssh root@box # ❌ permission deniedssh 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.