wihan/dev

writing / post

Securing your Linux VPS with Tailscale

2026-06-27linux · security · tailscale · ssh · vps5 minmarkdown

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:

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.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:

sudo grep -rE 'PasswordAuthentication|PermitRootLogin' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/

Comment out any conflicting yes lines 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:

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