# Securing your Linux VPS with Tailscale

> Take SSH off the public internet entirely - go from a fresh Ubuntu box to one you can only reach over your private tailnet.

- date: 2026-06-27
- tags: linux, 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](https://tailscale.com) 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:

```bash
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:

```bash
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:

```bash
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:

```bash
tailscale ip -4        # the tailnet address for this box
tailscale status       # everything on your tailnet
```

Two things worth doing in the [admin console](https://login.tailscale.com/admin) while
you're here:

- **Disable key expiry** for this machine (or bring it up with a pre-authorized
  [auth key](https://tailscale.com/kb/1085/auth-keys)). 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 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:

```bash
ssh-copy-id wihan@<tailnet-ip-or-magicdns-name>
```

Or set it up by hand on the server:

```bash
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`:

```bash
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:
>
> ```bash
> 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**:

```bash
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:

```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](https://tailscale.com/kb/1193/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 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](https://christitus.com/linux-security-mistakes/)
- Dreams of Code - [Setting up a production-ready VPS from scratch](https://dreamsofcode.io/blog/setting-up-a-production-ready-vps-from-scratch)
- [Tailscale documentation](https://tailscale.com/kb/)