How to ignore a bunch of unwanted traffic by using a private tunnel.
While I’ve been playing with new Coolify servers, I’ve started the new habit of limiting SSH access to my Tailscale network.
This drops waves of illegitimate SSH connection attempts and makes me feel smarter.
Arriving at this wasn’t obvious; I first tried Tailscale a few years ago and honestly didn’t know what I should be doing with it.
Tailscale creates a private network you can add devices to, almost like each one is joining your local network from wherever it happens to live. This applies to pretty much whatever device you can think of: your (Mac, Windows, Linux) desktop machine, your phone, your NAS, any servers you have running…
How to ignore a bunch of unwanted traffic by using a private tunnel.
While I’ve been playing with new Coolify servers, I’ve started the new habit of limiting SSH access to my Tailscale network.
This drops waves of illegitimate SSH connection attempts and makes me feel smarter.
Arriving at this wasn’t obvious; I first tried Tailscale a few years ago and honestly didn’t know what I should be doing with it.
Tailscale creates a private network you can add devices to, almost like each one is joining your local network from wherever it happens to live. This applies to pretty much whatever device you can think of: your (Mac, Windows, Linux) desktop machine, your phone, your NAS, any servers you have running in the cloud. Take a few seconds to install Tailscale’s client, and the device pops onto your “tailnet.”
This comes with some perks! If the device is only addressable via IPv6—something I’m still figuring out how to deal with—Tailscale makes it easier to access.
Each device has a name you can use to address it, which is nice for SSH. I’ll show you what I mean.
You might connect to a web server via its IPv4 address like this:
ssh ubuntu@55.55.55.55
You’d have to remember, of course, that 55.55.55.55 is your server’s IP address. Or create an alias for it in ~/.ssh/config. Or create a DNS record so my-server.my-domain.tld resolves to it:
ssh ubuntu@my-server.my-domain.tld
Every Tailscale device gets a name, so if I added that server to my tailnet and called it myserver, I could use that name without remembering an IP address or assigning a public hostname to it:
ssh ubuntu@myserver
Pretty cool!
That doesn’t work outside my Tailscale network—it’s only for the special club of authenticated clients using this private tunnel.
Eventually I realized this is a rather important feature.1
I’d been setting up new web servers to send their logs to Axiom, where I created charts and alerts for SSH authentication attempts and successes.
If you’ve ever paid attention to SSH connection attempts, you know that as soon as you’ve got a server online there’s an endless stream of garbage. It’s unsettling.
Seeing all these sketchy failures again, I thought it’d be nice to only accept SSH connections from my Tailscale network. I already use Tailscale machine names because it’s convenient—why not wall off the public port and use the tunnel instead?
I have yet to see a Tailscale device lose its connection, and if my usual desktop machine burst into flames I’d still have other devices I could use to access to the VPS.
It turns out limiting connections was quick and straightforward! Steps:
- Enable the UFW firewall. (Typically disabled by default.)
- Tell UFW to allow traffic on web ports (80, 443), since it’s a web server.
- Tell UFW to allow incoming connections specifically from Tailscale.
- Tell UFW to drop all other incoming connection attempts.
Before:
After:
Step by Step Instructions
This assumes you’ve got a VPS running Ubuntu and a Tailscale account, and you’ve got a root session running on the server.
1. Install the Tailscale client
- Run
curl -fsSL https://tailscale.com/install.sh | shto install the package. - Run
tailscale upto connect. - Click the resulting link, add the device, and optionally disable its expiry. (Machine settings → Disable key expiry)
2. Enable UFW and customize its rules
Don’t freak out! The rules don’t apply until you run ufw reload.
ufw enable
ufw default allow outgoing
ufw default deny incoming
ufw allow http
ufw allow https
ufw allow in on tailscale0
The last line allows all inbound connections from the Tailscale network, which is tailscale0.
3. Reload UFW and restart SSH
Okay now freak out! Take a moment and be extra sure you didn’t forget about any other inbound connections you might need to allow, or non-Tailscale devices that may need to get to this server. If those are urgently necessary and you didn’t add rules for them, you’re about to lock those things out.
Once you’re ready, restart both services:
ufw reload
service ssh restart
4. Confirm joy
You should be able to continue connecting via SSH using your Tailscale device name or addresses, but not from outside. Your UFW logs will also show you all kinds of blocked traffic that can no longer even attempt to establish SSH connections.
And now you can sleep a little easier.
November 13th Update!
Bertrand kindly wrote about this article and asked how I solved port 8000 still being open on my server running Coolify.
It turns out I didn’t! Visiting the bare IP address at port 8000 connected me with Coolify, which is bad because only 80 and 443 should have been responding. This is an issue specifically with Docker and UFW that at least one project aims to fix.
I followed these wonderfully-clear instructions from step 3 onward to bind Docker to Tailscale’s IP address, which solved the problem.
Footnotes
- Arguably the one big feature. ↩