Skip to content
Go back

Tailscale Deep Dive: Mesh VPN That Just Works (and Why That's Suspicious)

· Updated:
By SumGuy 11 min read
Tailscale Deep Dive: Mesh VPN That Just Works (and Why That's Suspicious)
Contents

WireGuard Is Perfect, Actually, If You Enjoy Configuration Suffering

Let me be clear: WireGuard is technically excellent. The codebase is tiny, the cryptography is modern, the performance is great. Setting it up involves generating keypairs on every device, distributing public keys, defining allowed IPs, configuring endpoints, handling NAT traversal yourself, and repeating the whole process every time you add a device or your home IP changes.

Calling Tailscale “just WireGuard, but easy” is a bit like calling a car “just an engine, but with wheels.” Technically true. Also missing the entire point. I spent two years running WireGuard by hand across a handful of boxes, and the week a laptop got stolen and I had to rotate every key on every peer at 2 AM is the week I stopped being precious about “real” networking and installed Tailscale instead.

Tailscale wraps WireGuard in a coordination layer that handles all of that automatically. Every device gets a 100.x.x.x address that works no matter where either end is. No port forwarding, no key distribution, no NAT headaches. It just works, which is either a blessing or deeply suspicious depending on your paranoia level.

This article assumes you’ve already run tailscale up and thought “cool, what else can this do?” The answer: quite a bit.

The Control Plane vs. The Data Plane

A distinction that clears up most of the confusion people have about Tailscale’s trust model:

Data plane: the actual WireGuard tunnels between your devices. Traffic flows directly peer to peer (or through relay servers when a direct connection isn’t possible). Tailscale never touches your actual traffic. This part is verifiable, it’s just WireGuard.

Control plane: the coordination server that distributes public keys, manages device registration, enforces ACLs, and keeps the network topology in sync. This runs at login.tailscale.com by default, and Tailscale does see metadata about your devices here (hostnames, IPs, who’s connected to what).

When people worry about Tailscale “seeing their traffic,” they’re conflating the two planes. Your packets are still end-to-end WireGuard. What you’re actually trusting Tailscale with is the control plane, and if that bothers you, Headscale (covered below) lets you run your own.

ACL Policies: Actually Control Who Talks to What

By default, every device in your tailnet can reach every other device on every port. Fine for one person and a laptop. Increasingly uncomfortable once you’re mixing work laptops, home servers, and IoT junk on the same network.

ACL policies live in the Access Controls section of the admin console, written in HuJSON (JSON with comments allowed, mercifully).

{
"groups": {
"group:admin": ["[email protected]"],
"group:family": ["[email protected]", "[email protected]"]
},
"tagOwners": {
"tag:server": ["group:admin"],
"tag:media": ["group:admin"],
"tag:homelab": ["group:admin"]
},
"acls": [
{
"action": "accept",
"src": ["group:admin"],
"dst": ["*:*"]
},
{
"action": "accept",
"src": ["group:family"],
"dst": ["tag:media:32400"]
},
{
"action": "accept",
"src": ["tag:homelab"],
"dst": ["tag:homelab:*"]
}
]
}

Without an explicit accept rule, traffic is denied. The default policy that lets everyone reach everything is a single "*:*" -> "*:*" rule you can and should delete once you’ve written real rules.

Tags are assigned per device and let you write policy against device categories instead of specific users:

Terminal window
# Apply a tag at registration time
tailscale up --advertise-tags=tag:server
# Or set it later in the admin console

That way when you rotate the human running a server, you don’t have to rewrite every ACL that mentioned them by name.

Test before you lock yourself out. The admin console has a policy tester built in. Use it before you save a change. Getting locked out of your own tailnet is fixable but embarrassing, and it’s always at the worst possible time.

Subnet Routers: Bring Your Whole LAN Into the Mesh

Not every device can run Tailscale, printers, NAS boxes, smart switches, whatever legacy appliance is still running in a closet. Subnet routing solves this by letting one Tailscale device advertise routes for the rest of the LAN behind it.

On the device that will act as the router:

Terminal window
# Enable IP forwarding first
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
# Advertise your home LAN (multiple subnets, comma-separated, also works)
sudo tailscale up --advertise-routes=192.168.1.0/24

Then approve the route in the admin console: click the device, click the three dots, approve the subnet. This is a deliberate speed bump, you don’t want an accidental route advertisement quietly opening a hole into your network.

On client devices, accept routes:

Terminal window
tailscale up --accept-routes

Now your laptop, wherever it physically is, can reach 192.168.1.x devices that have never heard of Tailscale. Your old NAS, your printer, your router’s admin page, all reachable as if you were sitting on the couch at home.

You can also target subnet CIDRs directly in ACL rules if you want to control who gets that access at the policy level, not just at the router.

Exit Nodes: Route All Your Traffic Through Home (Or a VPS)

Exit nodes turn a Tailscale device into a full VPN egress point. Everything else you do on the internet routes through that device and appears to come from its public IP.

On the device that will be the exit node:

Terminal window
sudo tailscale up --advertise-exit-node

Approve it in the admin console, same as a subnet route.

On the client, you can point at it two ways. The classic form re-runs up with the flag:

Terminal window
tailscale up --exit-node=100.x.x.x --exit-node-allow-lan-access

But if Tailscale is already running, tailscale set changes just the exit node without touching anything else up configured, faster and less likely to reset flags you forgot you set:

Terminal window
tailscale set --exit-node=100.x.x.x
# And to turn it back off:
tailscale set --exit-node=

Good use cases: coffee shop Wi-Fi you don’t trust, dodging regional content locks, presenting your home IP to a service while traveling. One real limitation: your home upload speed becomes everyone’s bottleneck. If your uplink is 50Mbps, that’s the ceiling for anything routed through it.

Tailscale SSH: SSH Without Opening Port 22

Tailscale SSH lets devices on your tailnet SSH into each other over the Tailscale connection, no exposed port 22, no separate key management for machines already on the mesh.

Terminal window
# Enable on the server
tailscale up --ssh
# Connect from any device on the tailnet
# or with MagicDNS
ssh user@myserver

ACLs control who’s allowed in, and you can require re-authentication for sensitive boxes:

{
"ssh": [
{
"action": "accept",
"src": ["group:admin"],
"dst": ["tag:server"],
"users": ["root", "ubuntu"]
},
{
"action": "check",
"src": ["*"],
"dst": ["tag:server"],
"users": ["*"]
}
]
}

The "check" action forces identity re-verification before the connection completes. Reserve it for anything you’d rather not have someone’s stale browser session walk into.

MagicDNS: Stop Memorizing 100.x.x.x Addresses

MagicDNS gives every device in your tailnet a resolvable hostname (devicename.tailnetname.ts.net). Turn it on in the admin console under DNS, and ping mydesktop, ssh homeserver, and curl nextcloud.tailnetname.ts.net all just work.

Split DNS lets you send specific domains to your own resolver while everything else goes wherever you’d normally send it:

{
"dnsConfig": {
"nameservers": ["1.1.1.1"],
"routes": {
"internal.example.com": ["100.x.x.10"],
"home.arpa": ["192.168.1.1"]
}
}
}

Queries for internal.example.com and home.arpa go to your internal DNS server over the tailnet. Everything else goes to Cloudflare. No split-brain DNS setup required.

Taildrive and Taildrop: File Sharing You Didn’t Have to Build

Need to get a file from one machine to another without spinning up Syncthing or emailing yourself a zip? Taildrop is peer-to-peer file transfer, built into the client:

Terminal window
tailscale file cp ~/document.pdf alice-laptop: # send (note the trailing colon)
tailscale file get ~/Downloads/ # pull anything queued for you

The file goes straight to the recipient over the mesh. No intermediate server, no size limits you didn’t set yourself.

Taildrive is the newer sibling: it lets you mount a directory on one device as a network share visible from another.

Terminal window
# On the machine sharing files
tailscale drive share Documents
# On the machine that wants access
tailscale drive list
# then browse it or mount it under /mnt/tailscale/...

It’s a lightweight replacement for NFS or SMB when the only thing you actually need is “let this other device see that folder,” without opening a real file-sharing protocol to the whole network.

Headscale: Own Your Control Plane

Headscale is an open source, self-hosted reimplementation of the Tailscale coordination server. Your devices register with your instance instead of Tailscale’s. The data plane is still plain WireGuard either way, only the control plane moves.

What you get: no external dependency, no data about your topology leaving your infrastructure, and it keeps working even if Tailscale the company has a bad day or changes its pricing.

What you trade: some client features (Funnel/Serve, a few MagicDNS conveniences, the polished web console), plus the operational overhead of running and patching another service yourself.

Running it with Docker, pinned to v0.28.0 for everything below:

docker-compose.yml
services:
headscale:
image: headscale/headscale:0.28.0
container_name: headscale
volumes:
- ./config:/etc/headscale
- ./data:/var/lib/headscale
ports:
- "8080:8080"
- "9090:9090" # metrics
command: headscale serve
restart: unless-stopped
config/config.yaml
server_url: https://headscale.yourdomain.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
db_type: sqlite3
db_path: /var/lib/headscale/db.sqlite
dns_config:
nameservers:
- 1.1.1.1
magic_dns: true
base_domain: headscale.internal

Put this behind a reverse proxy (Traefik, Caddy, nginx) with TLS. Headscale requires HTTPS for client registration to work at all.

Create a user and a pre-auth key for automated device enrollment:

Terminal window
docker exec headscale headscale users create myuser
docker exec headscale headscale preauthkeys create --user myuser

Two commands changed shape as Headscale matured toward 0.28, and it’s easy to find stale docs mixing old and new syntax. Approving a subnet route now goes through nodes approve-routes (the older routes enable -r ID was removed in 0.26):

Terminal window
docker exec headscale headscale nodes list-routes
docker exec headscale headscale nodes approve-routes --identifier NODE_ID --routes 192.168.1.0/24

Registering a node that connected without a pre-auth key now goes through the auth command group instead of the old nodes register:

Terminal window
docker exec headscale headscale nodes list # find the pending node's ID
docker exec headscale headscale auth register --user myuser --key NODE_KEY

Those are two different operations (approving a route vs. approving a new device), not two ways of doing the same thing, so you’ll likely use both.

On the client, point it at your instance:

Terminal window
tailscale up --login-server=https://headscale.yourdomain.com \
--authkey=YOUR_PREAUTHKEY \
--hostname=myserver

Skip the auth key and tailscale up prints a registration URL instead, which you approve with the auth register command above.

Tailscale vs Headscale: When to Use Which

FeatureTailscale (Cloud)Headscale (Self-hosted)
Setup complexityMinimalModerate
Control plane ownershipTailscale Inc.You
Admin UIPolished web consoleCLI only (unofficial UIs exist)
Tailscale SSH ACLsFull supportLimited
Funnel/ServeYesNo
Taildrop / TaildriveYesPartial, client-dependent
Mobile clientsYesYes (same clients)
Free tierUp to 100 devicesUnlimited devices
Paid team tierPer-user monthly billing, adds SSO and centralized adminN/A, you run the billing (i.e., there is none)
Uptime dependencyTailscale’s serversYour server

For personal use and small home labs, cloud Tailscale is the right call. The free tier covers most single-user setups, the data plane is the same WireGuard either way, and the admin console saves you real time. If you outgrow the free tier and need team seats, Tailscale’s paid plan adds SSO and centralized device management for a per-user monthly fee, check the current pricing page before you budget, since it’s moved more than once and this article will age faster than the software will.

For teams with compliance requirements, a hard no on third-party control planes, or a genuine need for air-gapped networking, Headscale earns its operational overhead. And because Headscale speaks the same protocol, you can point your existing Tailscale clients at it and migrate without touching a single device’s install.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
DNS Troubleshooting from the Command Line
Next Post
Podman Quadlets: Running Containers Without the Docker Daemon (or Your Sanity)

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts