LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Tailscale for Homelab Networking

tailscalewireguardvpnhomelabnetworkingheadscalezerotrust

There is a particular kind of homelab frustration that every operator eventually hits. You have a NAS, a Proxmox cluster, a self-hosted Gitea instance, a monitoring stack, maybe a few Raspberry Pis scattered around — and you want to reach all of it from your laptop at a coffee shop or your phone on a cellular connection. The naive solution is to open ports. Port 22 for SSH, port 443 for the reverse proxy, maybe a non-standard port here or there to feel safer. You set up a DDNS record so your dynamic residential IP stays resolvable. You add fail2ban. You tell yourself it’s fine.

It is not fine.

Every open port is a surface. The internet will find it within minutes, bots will probe it relentlessly, and one misconfigured service or unpatched CVE away from a very bad day. The DDNS record leaks your residential IP. The port exposures grow over time as you add services — entropy wins eventually. The right answer is to keep all of your services off the public internet entirely and use a secure tunnel to reach them. Tailscale makes that so low-friction that there is no longer a good excuse for the port-forwarding approach.

This post is a thorough walk through every part of Tailscale that matters for a homelab: how it actually works at the protocol level, how to set up subnet routers to reach devices that can’t run Tailscale, how exit nodes work, how to get split DNS and MagicDNS to make your internal services reachable by name, how to write proper ACL policy files, and when you might want to run Headscale as a self-hosted control plane instead. We will also be honest about the trust model — what Tailscale the company can see, and what they cannot.


The Homelab Connectivity Problem

The old approach to remote homelab access was built around two assumptions that no longer hold: that you have a static IP, and that open ports are acceptable. Neither is really true for most residential setups.

Dynamic DNS patches the IP problem, barely. You register a hostname with a DDNS provider, run a client that updates the record whenever your ISP reassigns your address. The latency between your IP changing and the record updating can leave your connection broken for minutes. More importantly, your home IP is now semi-publicly associated with your domain — something ISPs, data brokers, and anyone running passive DNS collection can correlate. This is a real concern if your homelab hosts anything sensitive, or if you simply value privacy.

The port forwarding problem is worse. Every service you expose requires a firewall hole. Your router’s NAT table gets entries. Anything with a vulnerability scanner — which is every internet-connected device within a week of being stood up — finds those ports and starts throwing exploits at them. SSH on port 22 gets hundreds of login attempts per hour from botnets. Your Nextcloud on port 443 gets probed for known PHP CVEs. Your “secure” admin panel on port 8443 with HTTP basic auth is one credential stuffing attack away from compromise. You are playing permanent defense against the entire internet.

Traditional VPN solutions like OpenVPN or IPsec do solve the exposure problem, but they introduce their own friction. You need a server with a static IP to act as the VPN endpoint. You need to provision and distribute certificates or pre-shared keys. Clients need to be configured with that server’s address. If the server goes down, everyone loses access. Key rotation is a manual operation. Adding a new device means generating credentials, distributing them, and updating server configuration. None of this is catastrophic, but it is operational overhead that compounds over time — and most homelabbers are running these things in their spare time, not as a day job.

Tailscale’s pitch is direct: install a package, log in with your identity provider, and every device gets a stable IP in the 100.64.0.0/10 range. Every device can reach every other device. No central server to maintain. No manual key exchange. No firewall rules to punch. Close all your exposed ports.

That pitch is accurate. The question is what it costs and how it actually works.


How WireGuard Works

Tailscale is built on WireGuard, and understanding WireGuard’s design is necessary to understand both what Tailscale adds and what limitations it’s working around.

WireGuard is a modern VPN protocol built into the Linux kernel (since 5.6) and available as a userspace implementation (wireguard-go) everywhere else. Its design philosophy is radical simplicity: a single UDP socket, no TCP, no complex state machine, no certificate hierarchy. Authentication is pure public-key cryptography — each peer has a Curve25519 keypair, and routing decisions are made based on which public keys are in the configuration.

The core concept in WireGuard is cryptokey routing. Every WireGuard interface (wg0, wg1, etc.) has a list of peers. Each peer entry contains a public key and an AllowedIPs list. When a packet arrives on the WireGuard interface, WireGuard checks which peer entry claims that source IP — if the packet decrypts successfully with that peer’s key, it’s accepted and routed to the IP stack. When a packet is sent, WireGuard looks up the destination IP against all peers’ AllowedIPs lists, finds the matching peer, encrypts the packet with that peer’s key, and sends it as a UDP datagram to the peer’s endpoint.

This is elegant, stateless, and fast. The kernel integration means packet processing happens in kernel space with very low overhead. WireGuard’s cryptography (Curve25519, ChaCha20-Poly1305, BLAKE2s) is modern and well-audited.

But WireGuard’s simplicity is also its limitation for a dynamic deployment. WireGuard itself is a pure data plane. It does not know how to discover peers, distribute keys, or handle peers whose IP addresses change. Every peer endpoint must be statically configured. If you have ten devices and you want all-to-all connectivity, you need ten configuration files, each listing nine peers with their public keys, allowed IPs, and endpoints. Adding an eleventh device means updating all ten existing configurations. Key rotation means coordinating updates across all peers simultaneously. This is manageable at small scale; it becomes untenable fast.

The second problem is NAT traversal. WireGuard uses UDP exclusively. This is a deliberate choice — UDP has much lower latency than TCP for VPN tunneling because there is no kernel-level retransmission logic fighting with the application’s own transport protocol. But UDP and NAT are historically difficult. NAT (Network Address Translation) maintains a state table mapping internal IP:port pairs to external IP:port pairs. For outbound connections, NAT works fine — the client sends a packet, the NAT device records the mapping, and replies are correctly forwarded back. For inbound connections, there is no existing mapping, so the NAT device drops the packet.

WireGuard itself has no NAT traversal logic. The PersistentKeepalive option helps keep an existing NAT mapping alive, but only if the connection was initiated from behind the NAT. Two peers both behind NAT — a common situation for any two home users trying to connect — cannot establish a direct WireGuard connection without external assistance.


Tailscale’s Architecture: What It Adds on Top of WireGuard

Tailscale solves both of WireGuard’s limitations — key distribution and NAT traversal — by adding a control plane and a relay infrastructure on top of WireGuard’s data plane. Understanding the separation between these two layers is the most important thing to grasp about Tailscale’s trust model.

Control plane vs. data plane: The control plane is Tailscale’s coordination server (or Headscale, if you self-host). It handles device registration, public key distribution, IP address assignment, and policy (ACLs). When a new device joins your tailnet, it authenticates to the coordination server, which assigns it a 100.x.x.x address, records its public key, and distributes that key plus address mapping to all other devices in your tailnet. Devices periodically check in with the coordination server to get updated peer information. This is the only part of the system that involves Tailscale’s infrastructure for ongoing operation.

The data plane — the actual encrypted traffic between your devices — flows directly between nodes over WireGuard. Tailscale’s servers are not in the path for data. This is a critical design choice and the basis for Tailscale’s security guarantees: even if Tailscale’s coordination server were compromised or compelled to cooperate with an adversary, they could not decrypt traffic between your nodes.

NAT traversal: Tailscale implements an ICE-like hole-punching mechanism to establish direct connections through NAT. The process roughly works as follows. Both peers report their local network addresses and their observed external addresses (gathered from STUN servers) to the coordination server. The coordination server relays these address candidates to each peer. The peers then simultaneously send probe packets to each other’s addresses — this simultaneous sending is the “hole punch.” Many NAT types, including most residential routers using full-cone or address-restricted cone NAT, will allow a reply to pass through if an outbound packet from that address was recently seen. When both sides punch simultaneously, both see a reply, and a direct UDP path is established.

Symmetric NAT — common in some corporate environments and certain ISPs — assigns a different external port for each destination, making hole-punching unreliable. When direct connection fails, Tailscale falls back to DERP.

DERP — Designated Encrypted Relay for Packets: DERP servers are Tailscale’s relay infrastructure. They are geographically distributed and are used as a fallback when direct connections cannot be established. Crucially, DERP relays are not decryption points. Traffic relayed through DERP is still WireGuard-encrypted end-to-end. A DERP server sees only ciphertext — it cannot read the content of your traffic. Tailscale operates DERP servers in multiple regions; you can also run your own.

As of early 2026, Tailscale has introduced Peer Relays (generally available since February 2026) as a complement to DERP. Peer relays run on your own infrastructure — they are WireGuard-based relay nodes you operate within your tailnet, designed for high-throughput scenarios where you need lower latency than a Tailscale-operated DERP server can provide and want to keep traffic fully on your own hardware. Peer relays require Tailscale 1.86 or later.

NAT traversal and relay fallback:

  [Laptop]                                    [Home Server]
  100.64.x.1                                  100.64.x.2
     |                                            |
  [NAT/Router A]                           [NAT/Router B]
     |                                            |
     |------- STUN: learn external addr -------->|
     |<------ STUN: learn external addr ---------|
     |                                            |
     |   Coordination server exchanges           |
     |   address candidates for both peers       |
     |                                            |
     |------- UDP hole punch attempt ----------->|
     |<------ UDP hole punch attempt ------------|
     |                                            |
     SUCCESS (most residential NAT types)
     |<=========== Direct WireGuard ===========>>|

  If hole punch fails (symmetric NAT, corporate firewall):

     |----> DERP relay (ciphertext only) ------->|
     |<---- DERP relay (ciphertext only) --------|
     (Tailscale can see metadata; NOT content)

  After reconnect or when path improves, Tailscale
  continuously retries direct connection in background.

The tailscaled daemon: On Linux, Tailscale runs as tailscaled, a userspace service that manages the WireGuard keys, maintains the WireGuard interface (via wireguard-go or the kernel module), communicates with the coordination server, and handles DNS if MagicDNS is enabled. Configuration persists in a state directory (default /var/lib/tailscale/). The daemon exposes a local socket that the tailscale CLI uses to send commands.

The 100.64.0.0/10 address space: Tailscale assigns addresses from the CGNAT (Carrier-Grade NAT) range defined in RFC 6598. This range was standardized for use by ISPs deploying large-scale NAT but is otherwise non-routable on the public internet and unlikely to conflict with typical RFC 1918 private address ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x). Your tailnet IPs are stable — they don’t change when you move between networks. This is what makes them useful as stable identifiers.

What Tailscale the company can and cannot see:

This question deserves a direct answer rather than marketing language. The coordination server, operated by Tailscale, sees:

  • The list of nodes in your tailnet and their 100.x.x.x IP assignments
  • Every node’s WireGuard public key (by definition — it distributes them)
  • The hostnames and metadata of your devices
  • Your ACL policy file
  • Connection metadata: which nodes are online, approximately when they last connected
  • Your DERP relay usage patterns (which DERP region, not content)

Tailscale cannot see:

  • The actual traffic between your nodes (WireGuard-encrypted, end-to-end)
  • The content of any application data
  • What services you’re running on your nodes (unless you tell them via node metadata)

This is a meaningful distinction. Tailscale’s threat model is similar to that of a DNS provider: they see metadata about your infrastructure, not the contents of your communications. For a homelab operator, this is an acceptable trade-off for most people. If your requirement is zero trust of any external party — including the control plane operator — that is the use case for Headscale, covered later.


Installation and Basic Setup

Linux installation is a single command followed by authentication:

1
2
3
4
5
6
7
8
# Install (works on Debian, Ubuntu, RHEL, Fedora, Arch, etc.)
curl -fsSL https://tailscale.com/install.sh | sh

# Bring up the interface and authenticate
sudo tailscale up

# Headless / server authentication using an auth key
sudo tailscale up --authkey=tskey-auth-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx

The tailscale up command opens a URL in a browser (or prints one for headless systems) for OAuth login via Google, GitHub, Microsoft, or any configured OIDC provider. Once authenticated, the device appears in your admin console at login.tailscale.com/admin.

macOS, Windows, iOS, and Android all have app store clients. The macOS and Windows clients are full system integrations with a menu bar icon. The iOS and Android clients use the OS’s VPN framework. All use the same underlying WireGuard data plane.

As of mid-2026, the current stable Tailscale client version is in the 1.96.x series. The client updates approximately every four weeks on the stable track; a release-candidate track is available for those who want earlier access.

Essential diagnostic commands:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Show all nodes in your tailnet and their status
tailscale status

# Test connectivity to a peer — shows DERP vs direct and RTT
tailscale ping homeserver
tailscale ping --until-direct homeserver  # keep trying until direct path

# Diagnose NAT type, DERP server latency, port availability
tailscale netcheck

# Show current DNS configuration
tailscale dns status

# Dump a bug report (safe to share — sanitizes keys)
tailscale bugreport

The tailscale ping output is particularly useful. It will tell you whether a connection is using a DERP relay and which region, or whether it has achieved a direct WireGuard path:

pong from homeserver (100.64.22.5) via DERP(nyc) in 45ms
pong from homeserver (100.64.22.5) via 203.0.113.45:41392 in 8ms

The second line shows a direct path established — the public endpoint and a sub-10ms round trip typical of a direct connection within the same geographic area.

Auth keys and device provisioning: The admin console lets you generate auth keys with varying properties:

Key Type Description
One-time Single device registration, expires after use
Reusable Multiple devices can use the same key
Pre-authorized Device joins without requiring admin approval
Tagged Device joins with a specific tag: applied
Ephemeral Device is automatically removed when it goes offline

Ephemeral keys are the right choice for CI/CD runners, containers, and any workload that should not accumulate as stale entries in your tailnet. Tagged + pre-authorized keys are ideal for automated infrastructure — a cloud-init script or Ansible role can bring up a new VM and have it fully joined to your tailnet without human interaction.


Subnet Routers

Not every device in your homelab can run Tailscale. Printers, NAS boxes with locked-down firmware, managed switches, smart home hubs, older embedded devices, IP cameras — these exist on your LAN but have no mechanism for installing a VPN client. Subnet routers solve this cleanly.

A subnet router is a Tailscale node that advertises one or more of your LAN subnets to the rest of your tailnet. Packets destined for 192.168.1.0/24 get routed through the subnet router and emerge on your LAN. From the perspective of the device you’re reaching, traffic looks like it’s coming from the subnet router’s LAN IP — no Tailscale configuration required on the target.

Subnet Router Architecture:

  [Laptop at coffee shop]
  Tailscale IP: 100.64.10.5
         |
         | WireGuard tunnel (direct or via DERP)
         |
  [Proxmox VM / Raspberry Pi]  <-- Subnet Router
  Tailscale IP: 100.64.10.1
  LAN IP:       192.168.1.50
         |
         | Normal LAN routing
         |
    +----+----------+----------+----------+
    |               |          |          |
  [NAS]       [Printer]  [IP Camera]  [Old NVR]
  192.168.1.10 192.168.1.20 192.168.1.30 192.168.1.40
  (no Tailscale required on any of these)

Setting up a subnet router:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# On the Linux node that will act as subnet router:

# 1. Enable IP forwarding (required for routing to work)
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1

# Persist across reboots
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf

# 2. Advertise the routes
sudo tailscale up --advertise-routes=192.168.1.0/24,192.168.2.0/24

After running this command, the routes appear as “pending” in the admin console. You must approve them — either manually by clicking in the console, or automatically via autoApprovers in your ACL policy file (covered in the ACL section).

On every client that should be able to use the subnet routes:

1
sudo tailscale up --accept-routes

This flag is not set by default for a deliberate reason: accepting routes is a meaningful change to your machine’s routing table, and a device should opt in explicitly. On a managed fleet you would set this via ACL tags and a configuration management system.

High-availability subnet routing works by having two separate nodes advertise the same subnet. Both must be approved. Tailscale automatically selects the best path — if the primary subnet router is offline or unreachable, traffic is routed through the secondary. This requires no additional configuration beyond both nodes advertising the same prefix.

1
2
# On the second subnet router (same subnet advertised):
sudo tailscale up --advertise-routes=192.168.1.0/24

A practical note on firewall rules: The subnet router’s host firewall needs to allow forwarded traffic. On systems using nftables or iptables with a default DROP policy for forwarding, you will need to explicitly allow forwarding between the Tailscale interface and your LAN interface:

1
2
3
# nftables example — allow forwarding between tailscale0 and eth0
sudo nft add rule inet filter forward iif "tailscale0" oif "eth0" accept
sudo nft add rule inet filter forward iif "eth0" oif "tailscale0" ct state related,established accept

Exit Nodes

Exit nodes extend the subnet router concept to cover all internet-bound traffic. When you configure a Tailscale node as an exit node and route through it, your internet traffic appears to originate from that node’s public IP rather than wherever you physically are.

The use cases for homelab operators are practical: using your home router or a trusted VPS as an exit node on untrusted public WiFi, routing around ISP-level filtering, and having your traffic appear to come from a stable known IP for services that block residential IP ranges (certain APIs, databases, streaming content for testing, etc.).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# On the node you want to use as an exit node:
sudo tailscale up --advertise-exit-node

# Approve in admin console, or via autoApprovers in ACL policy

# On a client, use the exit node by hostname:
sudo tailscale up --exit-node=homeserver

# Or set it after initial connection:
sudo tailscale set --exit-node=homeserver

# Allow reaching your local LAN while using the exit node
# (without this, LAN access is blocked while the exit node is active)
sudo tailscale set --exit-node=homeserver --exit-node-allow-lan-access

The difference between an exit node and a traditional VPN is architectural. A traditional VPN requires dedicated server infrastructure, a static IP, and active management of the VPN service. An exit node is just any Tailscale peer that has been designated as an exit — it can be your home Raspberry Pi, your Proxmox cluster’s gateway VM, or a $5/month VPS. No VPN server software to manage, no certificates to rotate, no separate tunnel configuration. It’s the same WireGuard mesh you already have, with one additional routing flag.

Note that exit node traffic routes through Tailscale’s coordination server for control plane purposes but the data plane still follows the WireGuard paths — direct to the exit node if a direct connection is available, through DERP if not. In both cases the traffic between your device and the exit node is encrypted.


MagicDNS and Split DNS

DNS is where homelab Tailscale setups either work beautifully or become a constant source of friction. Getting it right is worth spending time on.

MagicDNS is Tailscale’s automatic DNS feature. When enabled in the admin console (DNS tab), every node in your tailnet gets a DNS name in the format <hostname>.<tailnet-name>.ts.net. The tailscaled daemon runs a local DNS resolver (listening on 100.100.100.100, a special Tailscale-controlled address) that handles resolution of these names. Queries for *.ts.net return the appropriate 100.x.x.x Tailscale IP.

This means you can reach homeserver.my-tailnet.ts.net from any device in your tailnet, anywhere in the world, without knowing the 100.x.x.x IP. It also works for services: if your Proxmox node is named pve1, you reach it at pve1.my-tailnet.ts.net — Tailscale handles the DNS translation.

Split DNS is the more powerful feature for homelabbers. It lets you point specific DNS domains at your own resolver — typically a Pi-hole, AdGuard Home, or an authoritative DNS server running internally — while everything else resolves normally.

Split DNS flow:

  [Tailscale Client]
         |
         | DNS query for nas.home.arpa
         v
  [100.100.100.100 - tailscaled local resolver]
         |
         | Domain "home.arpa" matches split DNS rule
         | Route to Pi-hole at 100.64.10.3
         v
  [Pi-hole / Internal DNS at 100.64.10.3]
         |
         | Returns 192.168.1.10 (local LAN IP)
         v
  [Client has 192.168.1.10]
         |
         | 192.168.1.0/24 matches advertised subnet route
         | Route through subnet router
         v
  [NAS at 192.168.1.10] <-- reached by name, from anywhere

  For everything else (google.com, github.com, etc.):
         |
         | No split DNS match
         v
  [Normal upstream resolvers]

Configuration is done in the admin console under DNS > Nameservers. You add a nameserver IP (your internal DNS server’s Tailscale IP) and specify which domains it should handle. The nameserver must be reachable via the tailnet — either because it has Tailscale installed or because it’s accessible via a subnet router.

A typical homelab split DNS setup might look like:

Domain Resolver Purpose
home.arpa 100.64.10.3 (Pi-hole) All internal hostnames
internal.corp 100.64.10.3 Internal services
ts.net Tailscale-handled MagicDNS (automatic)
Everything else Default upstream Public internet

Override local DNS is an option that replaces your device’s system DNS with Tailscale’s resolver entirely. This is useful when you want Pi-hole ad blocking to apply to all your traffic regardless of which network you’re on. The caveat is that it can break captive portals — the landing pages on hotel and airport WiFi that require you to accept terms before Internet access is granted. Those portals depend on DNS hijacking, which conflicts with Tailscale’s DNS override. Keep this in mind when troubleshooting “my VPN works but I can’t connect to hotel WiFi.”

1
2
3
4
5
6
7
8
# Verify current DNS configuration on the client
tailscale dns status

# Example output:
# Resolver: 100.100.100.100
# Domains: home.arpa -> [100.64.10.3], internal.corp -> [100.64.10.3]
# Override local DNS: true
# MagicDNS: enabled

ACL Policy Files

Tailscale’s default access control model is permissive: every node in your tailnet can reach every other node on any port. For a single-person homelab where you own every device, this is probably fine. For tailnets that include family members’ phones, work laptops that Tailscale installed on, guest devices, or CI/CD systems, the default is too open.

ACL policy files are Tailscale’s answer to zero-trust network access. They are written in HuJSON (JSON with comments and trailing commas allowed) and define exactly which sources can reach which destinations on which ports. The policy file lives in your admin console but can also be managed via Tailscale’s API for GitOps workflows.

Tags are the primary organizational primitive. Instead of writing rules that reference individual email addresses or IPs (which change), you tag devices and write rules against tags. A server gets tag:server, your personal devices get tag:client, CI runners get tag:ci. Tags are applied when generating auth keys or can be assigned from the admin console.

Groups let you organize users. group:admins might contain the email addresses of people who should have full access, while group:family contains accounts for household members who should reach only media services.

Here is a complete, realistic ACL policy file for a homelab:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// Tailscale ACL policy for homelab
// Last updated: 2026-05-28
{
  // Define groups of users
  "groups": {
    "group:admins": ["user@example.com"],
    "group:family": ["user@example.com", "partner@example.com"]
  },

  // Define reusable host aliases
  "hosts": {
    "homeserver":    "100.64.10.1",
    "proxmox-pve1":  "100.64.10.5",
    "proxmox-pve2":  "100.64.10.6",
    "proxmox-pve3":  "100.64.10.7",
    "pbs":           "100.64.10.8",
    "pihole":        "100.64.10.3",
    "lan":           "192.168.1.0/24"
  },

  // ACL rules: evaluated top-to-bottom, first match wins
  "acls": [
    // Admins get full access to everything
    {
      "action": "accept",
      "src":    ["group:admins"],
      "dst":    ["*:*"]
    },
    // Family devices can reach media server and nothing else
    {
      "action": "accept",
      "src":    ["group:family"],
      "dst":    ["tag:media:80,443,8096,32400"]  // Jellyfin, Plex
    },
    // Tagged servers can talk to each other on specific ports
    {
      "action": "accept",
      "src":    ["tag:server"],
      "dst":    ["tag:server:22,9100,9090,3100"]  // SSH, node-exporter, Prometheus, Loki
    },
    // CI runners can reach internal git and registry
    {
      "action": "accept",
      "src":    ["tag:ci"],
      "dst":    ["tag:gitea:22,443", "tag:registry:5000,443"]
    },
    // All devices can reach Pi-hole for DNS
    {
      "action": "accept",
      "src":    ["*"],
      "dst":    ["pihole:53"]
    }
  ],

  // Automatically approve subnet routes and exit nodes
  // from tagged devices — no manual click required
  "autoApprovers": {
    "routes": {
      "192.168.1.0/24": ["tag:subnet-router"],
      "192.168.2.0/24": ["tag:subnet-router"]
    },
    "exitNode": ["tag:exit-node"]
  },

  // Tailscale SSH: let Tailscale handle SSH auth
  // Users authenticate via Tailscale identity, not SSH keys
  "ssh": [
    {
      "action": "accept",
      "src":    ["group:admins"],
      "dst":    ["tag:server"],
      "users":  ["root", "ubuntu", "admin"]
    },
    {
      "action": "accept",
      "src":    ["tag:ci"],
      "dst":    ["tag:gitea"],
      "users":  ["git"]
    }
  ],

  // Policy tests — validated in admin console
  // Ensures your rules do what you think they do
  "tests": [
    {
      "src":    "user@example.com",
      "accept": ["proxmox-pve1:22", "pbs:443", "pihole:53"]
    },
    {
      "src":    "partner@example.com",
      "accept": ["tag:media:8096"],
      "deny":   ["proxmox-pve1:22", "pbs:443"]
    }
  ]
}

The tests block is executable in the admin console — Tailscale evaluates your policy against the test cases and tells you whether the expected allow/deny decisions match. This is invaluable before pushing a policy change that could lock yourself out.

GitOps for ACL management: Tailscale exposes an API endpoint for reading and writing the policy file. A practical pattern is to keep the policy file in a Git repository, run a CI pipeline that validates it with the Tailscale API’s --dry-run equivalent, and then apply it on merge to main. This gives you version history, review workflow, and rollback capability for network policy changes.

1
2
3
4
5
6
# Push ACL policy via Tailscale API (requires API key)
curl -s -X POST \
  -H "Authorization: Bearer $TAILSCALE_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @policy.hujson \
  "https://api.tailscale.com/api/v2/tailnet/<tailnet>/acl"

Headscale: Self-Hosted Control Plane

Headscale is an open-source reimplementation of the Tailscale coordination server. It gives you full ownership of the control plane — you run the server, you hold the data, and you owe nothing to Tailscale’s SaaS infrastructure.

The current stable release as of this writing is v0.28.0 (February 2026), with a v0.29.0 beta available that adds SSH check actions, policy testing as a first-class feature, and full grants support for application-level capabilities. The minimum supported Tailscale client version for Headscale 0.28+ is v1.80.0.

Headscale connects to official, unmodified Tailscale clients. You do not need a forked or special client — the same package you would install for Tailscale SaaS works with Headscale; you just point it at your server.

Why you might want Headscale:

  • Regulatory or compliance requirements that prohibit sending device metadata to a third-party SaaS
  • Airgapped environments where nodes should never reach Tailscale’s servers
  • Distrust of any external coordination server, even one that can’t read your traffic
  • Organizations that need full audit control over tailnet join/leave events
  • No Tailscale account required — useful for scenarios where Google/GitHub/Microsoft SSO is not acceptable

What you give up by running Headscale:

  • The Tailscale admin console UI (Headscale has a basic headscale-ui third-party frontend)
  • Tailscale SSH session recording
  • Tailscale Funnel (exposing services to the public internet via Tailscale’s infrastructure)
  • Some newer ACL features that Headscale tracks with a lag of a quarter or two
  • Tailscale’s DERP infrastructure (you must run your own DERP servers or lose relay fallback)
  • Automatic client updates and the Tailscale support relationship

Docker Compose installation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# docker-compose.yml for Headscale
services:
  headscale:
    image: headscale/headscale:0.28.0
    restart: unless-stopped
    command: headscale serve
    volumes:
      - ./config:/etc/headscale
      - ./data:/var/lib/headscale
    ports:
      - "8080:8080"   # HTTPS (put behind reverse proxy)
      - "9090:9090"   # Prometheus metrics
    environment:
      - TZ=UTC

Minimal config/config.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
server_url: https://headscale.yourdomain.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
grpc_listen_addr: 0.0.0.0:50443
grpc_allow_insecure: false

private_key_path: /var/lib/headscale/private.key
noise:
  private_key_path: /var/lib/headscale/noise_private.key

ip_prefixes:
  - 100.64.0.0/10

derp:
  server:
    enabled: false     # enable if you want an embedded DERP
  urls:
    - https://controlplane.tailscale.com/derpmap/default  # use Tailscale's DERP
  auto_update_enabled: true

db_type: sqlite3
db_path: /var/lib/headscale/db.sqlite

log:
  level: info

acl_policy_path: /etc/headscale/acls.yaml

dns_config:
  magic_dns: true
  base_domain: headscale.internal
  nameservers:
    - 1.1.1.1

Basic Headscale operations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Create a user (Tailscale calls these "tailnets")
headscale users create homelab

# Generate a pre-auth key for a user
headscale preauthkeys create --user homelab --reusable --expiration 24h

# List nodes
headscale nodes list

# Rename a node
headscale nodes rename --identifier 1 homeserver

# List routes and approve a subnet route
headscale routes list
headscale routes enable --route 1

Connecting a Tailscale client to Headscale:

1
2
3
4
5
6
7
# On Linux client:
sudo tailscale up --login-server=https://headscale.yourdomain.com

# With a pre-auth key (headless):
sudo tailscale up \
  --login-server=https://headscale.yourdomain.com \
  --authkey=<preauthkey>

The coordination server handshake is redirected to your Headscale instance. The client’s WireGuard key exchange, IP assignment, and peer discovery all flow through Headscale rather than Tailscale’s servers.


Comparison: Tailscale SaaS vs Headscale vs Plain WireGuard

Feature Tailscale SaaS Headscale Plain WireGuard
Control plane Tailscale-operated Self-hosted Manual (you manage all keys)
Setup complexity Very low Medium High
Peer discovery Automatic Automatic (via Headscale) Manual per-peer config
NAT traversal Yes (DERP fallback) Yes (requires DERP) No (manual PersistentKeepalive)
MagicDNS Yes Yes (basic) No
ACL / policy Yes (full feature set) Yes (trailing feature parity) Via AllowedIPs only
Tailscale SSH Yes No N/A
Funnel Yes No No
Subnet routers Yes Yes Manual routing config
Exit nodes Yes Yes Manual default route config
Peer Relays Yes No N/A
Admin UI Full web console Limited (third-party) wg show / wg-quick
Max free devices 100 (Personal plan) Unlimited Unlimited
Data sent to vendor Metadata (keys, IPs, policy) None None
Traffic privacy E2E encrypted E2E encrypted E2E encrypted
Your availability SLA Tailscale’s uptime Yours Yours (no control plane)
DERP relay fallback Tailscale-operated DERPs Self-hosted or Tailscale DERPs None

The honest recommendation: use Tailscale SaaS for most homelabs. The free Personal plan supports 100 devices, which is more than enough for any homelab short of a medium-sized datacenter. The metadata exposure is acceptable for personal use. The operational overhead you avoid by not running Headscale is real — you have no coordination server to update, monitor, back up, or troubleshoot. Headscale is the right call when you have specific compliance requirements, are building something that must not depend on any external SaaS, or have an airgapped environment where Tailscale’s servers are not reachable at all.

Plain WireGuard remains the right choice when you have a small, static set of peers and want zero external dependencies — a site-to-site tunnel between two servers you control is a case where setting up a full tailnet is overkill.


Practical Homelab Patterns

Pattern 1: Laptop anywhere, reach any LAN device. Install Tailscale on your laptop and on one Linux machine on your home LAN (a Proxmox VM, a Raspberry Pi, an old mini PC). Configure the Linux machine as a subnet router advertising 192.168.1.0/24. Run tailscale up --accept-routes on your laptop. You now have complete access to every device on your LAN from anywhere — including your NAS, printer, smart home hub, and anything else that has a LAN IP. No port forwarding. No exposed services.

Pattern 2: Proxmox cluster + PBS via Tailscale. Install Tailscale on each Proxmox host and on Proxmox Backup Server. Your Proxmox nodes can reach PBS for backups without any external network path. You can access the PBS and Proxmox web UIs from your laptop via their Tailscale IPs. VM replication between nodes traverses the Tailscale mesh. Add ACL rules so that Proxmox hosts can reach PBS on port 8007 and the backup storage ports, but external devices cannot reach the Proxmox nodes directly.

Pattern 3: VPS exit node. Spin up a minimal VPS (Hetzner, Vultr, DigitalOcean) with a static IP. Install Tailscale, advertise it as an exit node. When traveling or on a network you do not trust, route through your VPS. Your traffic appears to come from the VPS IP. For services that block residential IP ranges — certain SaaS APIs, some media services, authentication flows with IP reputation checks — routing through a datacenter IP solves the problem without buying a dedicated VPN subscription.

Pattern 4: Pi-hole + split DNS for all clients. Run Pi-hole on a machine that has Tailscale installed. Configure split DNS in the Tailscale admin console to route queries for your internal domain to Pi-hole. Enable “Override local DNS” if you want Pi-hole’s ad blocking to apply to all traffic from all tailnet clients. Your phone gets ad blocking at the DNS layer even on cellular — the DNS queries tunnel through Tailscale to your home Pi-hole, which filters ads before resolving. The combined benefit is internal name resolution and network-level ad blocking from any location.

Pattern 5: CI/CD runners accessing internal services. GitHub Actions runners (or Gitea Actions, Woodpecker CI, etc.) need to reach internal resources — a private container registry, an internal Maven proxy, a staging database, a Kubernetes cluster. Install Tailscale in your CI workflow using the official Tailscale GitHub Action, authenticate with an ephemeral tagged key. The runner joins your tailnet for the duration of the job, accesses internal resources directly, and the ephemeral node is automatically removed when the runner disconnects. No inbound ports on your internal services; no special network configuration on the CI platform.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# GitHub Actions workflow snippet: ephemeral Tailscale in CI
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Connect to Tailscale
        uses: tailscale/github-action@v2
        with:
          authkey: ${{ secrets.TAILSCALE_AUTHKEY }}  # ephemeral + tagged
          args: "--accept-routes"

      - name: Deploy to internal Kubernetes
        run: |
          kubectl --server=https://k8s.internal.corp:6443 apply -f deploy/

tailscale serve — private TLS without certificates. The serve command lets you expose a local service on your tailnet with automatic TLS, routed through your Tailscale hostname. No certificate management, no reverse proxy configuration, no open ports on the host firewall. The TLS certificate is issued by Tailscale’s own CA, trusted by other tailnet members.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Expose a local web service at port 3000 on the tailnet
tailscale serve 3000

# The service is now reachable at:
# https://hostname.my-tailnet.ts.net
# with valid TLS, accessible only within your tailnet

# Serve a specific path or a static directory
tailscale serve --set-path /api http://localhost:8080
tailscale serve --set-path /static /home/user/www

# Make it persistent across tailscaled restarts
tailscale serve --bg 3000

tailscale funnel — careful public exposure. Funnel extends serve to the public internet — your service becomes accessible at https://hostname.your-tailnet.ts.net to anyone on the internet. This runs through Tailscale’s infrastructure, so it has bandwidth implications and the traffic is visible to Tailscale (as a relay, not encrypted end-to-end for the public HTTPS portion). Use it for webhooks, temporary demos, development testing. Do not use it for sensitive internal services or as a long-term production exposure mechanism — that is not its design intent.

1
2
3
4
5
6
7
8
# Expose port 8080 publicly via Tailscale Funnel
tailscale funnel 8080

# Background mode (persists after terminal closes)
tailscale funnel --bg 8080

# Check what is currently being served/funneled
tailscale serve status

Monitoring and observability. A few things to track in a mature homelab Tailscale setup:

1
2
3
4
5
6
7
8
9
# Check whether connections are using DERP (relay) vs direct
# A high DERP ratio may indicate NAT or firewall issues
tailscale status --json | jq '.Peer[] | {Hostname: .HostName, Relay: .Relay, Active: .Active}'

# Tailscaled logs (journald)
journalctl -u tailscaled -f

# Tailscaled exposes a Prometheus metrics endpoint (if enabled)
# tailscaled --debug=... or check /var/run/tailscale/tailscaled.sock stats

If you are running Peer Relays for high-throughput internal routing, the metrics tailscaled_peer_relay_forwarded_packets_total and tailscaled_peer_relay_forwarded_bytes_total are available for scraping with Prometheus and visualization in Grafana.


Closing Thoughts

Tailscale is one of the few pieces of infrastructure that genuinely delivers on its promise without hidden complexity. The WireGuard foundation is solid. The control plane adds exactly the automation that WireGuard deliberately left out. The result is a mesh VPN that scales from one device to a hundred without any per-peer configuration work.

For a homelab operator, the right path is clear: close your exposed ports, stand up a subnet router, configure split DNS against your internal resolver, and write a modest ACL policy that reflects who actually owns what in your infrastructure. The operational savings are permanent — you stop playing defense against the internet and start building things instead.

The trust question is the only nuanced decision. Tailscale’s architecture means the company cannot read your traffic, but they do hold metadata about your infrastructure. For personal use, that trade-off is reasonable. For anything where that metadata is sensitive — enterprise environments, regulated industries, or high-threat-model personal setups — Headscale puts the control plane in your hands at the cost of running and maintaining it yourself.

Whichever direction you choose, the WireGuard data plane beneath everything is the same. Your traffic is encrypted. Your devices talk directly. The ports are closed.

Comments