Ditching Cloudflared for Tailscale
Note
Sep 7, 2026
· ~4 min readA while back I wrote about SSHing into my home server from anywhere using a Cloudflare Tunnel. It worked. But the more I lived with it, the more it felt like the wrong tool.
Cloudflared needs a domain. It routes my SSH through Cloudflare’s edge. It’s a
daemon babysitting a config.yml, and every service I want to reach needs its
own ingress rule. That’s a lot of moving parts, and another company sitting in
the middle of me talking to my own machine.
So I ripped it out and moved to Tailscale.
Why it’s better for me
Tailscale is a WireGuard mesh. My machines talk to each other directly,
peer to peer, over an encrypted tunnel. No domain, no public IP, no open ports,
no ingress config. Every box gets a 100.x address and a name, and I can reach
any port on it, not just the one I remembered to forward.
And because everything I own is Nix, I don’t click around an app. I add a line, rebuild, done.
The Linux box (dom)
dom is my NixOS home server. Enabling Tailscale is one line, plus telling the
firewall to trust the tailnet interface so I’m not fighting my own rules.
# machines/dom/services.nix
{
# tailscale
services.tailscale.enable = true;
# firewall — allow ssh + trust the tailnet, block the rest
networking.firewall = {
enable = true;
allowedTCPPorts = [ 22 ];
trustedInterfaces = [ "tailscale0" ];
};
}
services.tailscale.enable installs the daemon, starts it at boot, and opens
the UDP port it needs to punch through NAT. Rebuild:
sudo nixos-rebuild switch --flake .#dom
The Mac (whiterose)
My MacBook is a nix-darwin machine. nix-darwin has the same option, it just runs the daemon under launchd. I only wanted it on the laptop, not the Mac Mini, so I dropped it straight into that machine’s config:
# machines/whiterose/default.nix
modules = [
{
nixpkgs.config.allowUnfree = true;
}
+
+ # MacBook-specific: Tailscale client to reach the homelab from anywhere
+ {
+ services.tailscale.enable = true;
+ }
./../../modules/darwin.nix
darwin-rebuild switch --flake .#whiterose
Log in
Enabling the service installs everything, but the node still has to join the tailnet. Once per machine:
sudo tailscale up
It prints a login URL, I approve it in the browser, and the box shows up in my
tailnet. On macOS I also set myself as operator so I stop needing sudo for
everyday commands:
sudo tailscale set --operator=$USER
Now tailscale status lists everything:
100.124.21.97 whiterose macOS -
100.109.246.23 dom linux active; direct 71.121.175.97:41641
direct is the word I wanted. No relay, ~12ms, straight laptop-to-server.
Point SSH at the tailnet
My ssh dom alias was still hardcoded to the LAN IP, so it only worked at home.
The whole point was reaching it from anywhere, so I swapped it for the MagicDNS
name. That name resolves on my LAN and from a coffee shop, Tailscale figures
out the route.
# modules/ssh.nix
"dom" = {
User = "pwn";
- Hostname = "192.168.1.213";
+ # Tailscale MagicDNS name — reachable on LAN and from anywhere.
+ Hostname = "dom";
};
After a rebuild, this just works, from my bed or from another country:
ssh dom
No ssh.pwnwriter.me. No tunnel daemon. No DNS records to manage.
I don’t want it running 24/7
Here’s the part I actually care about. I don’t want a VPN glued on all day. I want it up when I need the homelab and gone when I don’t.
Turns out you don’t fight the daemon for this:
tailscale down # off — disconnects from the tailnet
tailscale up # on — back in
tailscale down sets WantRunning=false, and that sticks across reboots.
Turn it off once and it stays off until I explicitly bring it up. The daemon is
still there, but parked, doing nothing.
Battery? I worried about this and it’s basically a non-issue. Connected and idle,
it’s a small keepalive every ~25s and one long-poll to a relay, nothing you’d
feel on a day’s charge. After tailscale down it’s effectively zero. The only
thing that actually costs you is using a box as an exit node, so don’t, unless
you mean to.
SSH bits worth stealing
Two lines in my Host * block that make daily SSH nicer over the tunnel:
# modules/ssh.nix, the "*" block
ServerAliveInterval = 30; # don't let idle sessions die on NAT
ControlMaster = "auto"; # reuse one connection
ControlPersist = "10m"; # 2nd+ ssh/scp/rsync are instant
Multiplexing means the second ssh dom rides the first tunnel, no re-auth,
near-instant. And the stuff I run constantly:
ssh dom 'docker ps' # one-off command, no shell
rsync -avP ~/dir/ dom:~/dir/ # resumable folder sync
ssh -L 8080:localhost:80 dom # reach dom's :80 at localhost:8080
That’s it
Cloudflared was a tunnel to one port behind a domain behind a company. Tailscale is my machines talking to each other, off when I say so, and it’s four lines of Nix across two hosts.
The old post stays up for history. This is what I run now.