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

Tailscale: The WireGuard Mesh VPN That Actually Works Everywhere

tailscalewireguardvpnnetworkinghomelabself-hostedsecurityheadscalezero-trust

WireGuard is the best VPN protocol ever written. It is also, fundamentally, a plumbing primitive: fast, minimal, and deliberately devoid of any mechanism to find peers, traverse NAT, exchange keys automatically, or handle the coordination that makes a network useful. You still need to copy public keys around, manage IP address assignments, open firewall ports, and configure routes manually.

Tailscale is the answer to the question “what if WireGuard had an operations layer?” It takes WireGuard’s cryptography and tunneling, wraps it with a coordination server, an automatic NAT traversal system, a relay network, and a policy engine, and delivers a mesh VPN where every device can reach every other device — no open ports, no static IPs, no manual key exchange, no router configuration.

The result is a VPN that works the first time, on every network, including hostile ones.


How Tailscale Works

Understanding the architecture explains every feature and limitation. Tailscale has two distinct planes that never interact directly: a control plane and a data plane.

The Control Plane: Coordination Server

The coordination server is Tailscale’s central brain. It does not carry any of your traffic — it only manages metadata:

  • Identity and authentication: Your device authenticates to the coordination server using an identity provider (Google, GitHub, Microsoft, Apple, email). The coordination server issues each device a node key that proves identity.
  • Key distribution: When device A wants to talk to device B, the coordination server gives A the public key for B’s WireGuard interface. No manual key copying — the coordination server is the key exchange protocol.
  • Address assignment: Every device on your tailnet (your Tailscale network) gets an IP in the 100.64.0.0/10 CGNAT range. These are stable, device-specific IPs that follow the device everywhere. Your homelab server might be 100.64.12.34 from a coffee shop, from your office, and from home — always the same address.
  • ACL distribution: The access control policy you define is distributed to every node, which enforces it locally using WireGuard’s existing allow/deny mechanism.
  • Endpoint exchange: The coordination server helps devices discover each other’s current real-world IP:port so they can attempt a direct WireGuard connection.

Critically, Tailscale’s coordination server never sees your traffic. It only brokers the setup. After two devices have exchanged keys and established a WireGuard session, the coordination server is completely out of the data path.

The Data Plane: WireGuard

Each Tailscale device runs a WireGuard interface (tailscale0 on Linux, a userspace implementation on other platforms). All traffic between nodes is WireGuard-encrypted end-to-end. Tailscale does not modify WireGuard’s cryptography — it’s the same ChaCha20-Poly1305 encryption, Curve25519 key exchange, and BLAKE2s MACs.

What Tailscale adds is automatic management of the WireGuard peer table: it adds and removes peers dynamically as devices join and leave your network, updates endpoints as devices move between networks, and handles the key rotation WireGuard requires every few minutes.

NAT Traversal: How Direct Connections Happen

Most devices live behind NAT — a home router, a corporate firewall, a cloud provider’s NAT gateway. Getting two NAT-ed devices to talk directly without a relay server is the hardest problem in peer-to-peer networking. Tailscale solves it using techniques from WebRTC and STUN/TURN:

1. Endpoint discovery: The coordination server collects each device’s externally-visible IP:port (from the NAT’s perspective) as well as any local IPs. It shares these candidate endpoints with the peer.

2. Direct connection attempt (hole punching): Both devices simultaneously send UDP packets to each other’s external endpoints. Because NAT tables are typically set up on outbound traffic, the two-directional simultaneous probe creates holes in both NAT tables at the same moment, allowing packets to flow in both directions. This technique, called UDP hole punching, succeeds in roughly 85-95% of real-world NAT configurations.

3. Path selection: Tailscale probes multiple paths in parallel — local network, IPv4 external, IPv6 external — and picks the lowest-latency path that works. The path can change as the network changes; if a better path becomes available, Tailscale switches to it.

4. Hard NAT handling: Some enterprise firewalls, carrier-grade NAT implementations, and symmetric NAT setups (where the external port changes with every destination) prevent direct hole punching. Tailscale has a fallback called Disco (direct in-stream connectivity) that uses additional techniques including hairpin NAT detection and port prediction.

DERP Relays: The Fallback

When direct connections fail — and they do fail, for some fraction of networks — Tailscale falls back to DERP relays (Detoured Encrypted Routing Protocol). DERP servers are distributed globally and handle relayed traffic over HTTPS (port 443).

Important properties of DERP relays:

  • Traffic is still end-to-end WireGuard encrypted. The DERP relay sees only WireGuard ciphertext. It cannot decrypt or inspect your data.
  • Relay selection is automatic. Tailscale picks the geographically nearest DERP server automatically.
  • DERP is a fallback, not a default. Tailscale always attempts a direct connection first. DERP only activates when direct paths fail.
  • Performance is degraded but functional. DERP adds latency (routing through a relay server) but is reliable through any network that allows HTTPS outbound.

You can see which path a connection is using with tailscale status — it shows direct or the DERP region for each peer.

The 100.64.0.0/10 Address Space

Tailscale uses the CGNAT range (100.64.0.0/10, RFC 6598) for all device IPs. This range was originally reserved for carrier-grade NAT and is not routed on the public internet, which means:

  • No conflicts with most private networks (192.168.x.x, 10.x.x.x, 172.16.x.x)
  • Globally unique within your tailnet — your homelab server is always 100.x.x.x regardless of which network it’s on
  • Not reachable from the internet, which reduces attack surface

Each device’s 100.x.x.x address is stable and does not change unless you explicitly remove and re-add the device.


Installation

Linux

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# One-line install script (fetches and runs the appropriate package manager steps)
curl -fsSL https://tailscale.com/install.sh | sh

# The install script supports:
# Debian/Ubuntu (apt), Fedora/RHEL/CentOS (dnf/yum), Arch (pacman),
# Alpine (apk), OpenSUSE (zypper), Amazon Linux, Raspbian, and more

# After installation, authenticate and bring the interface up:
sudo tailscale up

# This opens a URL to authenticate through your identity provider
# (Google, GitHub, Microsoft, Apple, or email)

For manual installation on Debian/Ubuntu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Add Tailscale's package repository
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg \
  | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null

curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.tailscale-keyring.list \
  | sudo tee /etc/apt/sources.list.d/tailscale.list

sudo apt update && sudo apt install tailscale

# Enable and start the service
sudo systemctl enable --now tailscaled

# Bring up and authenticate
sudo tailscale up

macOS

1
2
3
4
5
6
# Via Homebrew (recommended for servers / headless use)
brew install tailscale
sudo tailscale up

# Or download the Mac App Store app for menu-bar management
# (The App Store version uses the system extension; Homebrew uses the CLI daemon)

Windows

Download the installer from tailscale.com/download. The Windows client installs a system service and a system-tray application. Authentication happens through a browser popup.

For scripted deployment:

1
2
3
4
5
6
7
8
# Install silently
winget install tailscale.tailscale

# Or using Chocolatey
choco install tailscale

# Bring up with an auth key (for unattended deployment)
tailscale up --authkey=tskey-auth-xxxxxxxx --hostname=my-server

iOS and Android

Install from the App Store or Google Play Store. On first launch it will ask to authenticate through your identity provider. iOS uses a Network Extension; Android uses the WireGuard kernel module where available and falls back to userspace WireGuard.

Raspberry Pi / ARM

The install script handles ARM automatically. On Raspberry Pi OS (Debian-based):

1
2
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

Tailscale supports both armv7 (32-bit Pi) and arm64 (64-bit Pi OS). The 64-bit variant performs better.


The tailscale up Command

tailscale up is the primary configuration command. Run it with flags to configure the node’s role and capabilities. Without flags, it uses defaults.

 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
# Basic — authenticate and bring up with defaults
sudo tailscale up

# With a pre-generated auth key (for automated/unattended setup)
sudo tailscale up --authkey=tskey-auth-xxxxxxxx

# Set a custom hostname (defaults to system hostname)
sudo tailscale up --hostname=homelab-pi

# Advertise this node as a subnet router (exposes your LAN)
sudo tailscale up --advertise-routes=192.168.1.0/24

# Advertise multiple subnets
sudo tailscale up --advertise-routes=192.168.1.0/24,10.0.0.0/8

# Advertise this node as an exit node (routes all traffic through it)
sudo tailscale up --advertise-exit-node

# Accept routes advertised by subnet routers (required to use subnet routing)
sudo tailscale up --accept-routes

# Accept DNS settings pushed from the coordination server (MagicDNS)
sudo tailscale up --accept-dns

# Advertise this node as accessible via Tailscale SSH
sudo tailscale up --ssh

# Combine flags for a full-featured homelab gateway
sudo tailscale up \
  --advertise-routes=192.168.1.0/24 \
  --advertise-exit-node \
  --accept-routes \
  --accept-dns \
  --ssh \
  --hostname=homelab-gateway

# Log in to a specific Tailscale account (for Headscale or multiple accounts)
sudo tailscale up --login-server=https://headscale.yourdomain.com

# Unattended re-authentication (useful for long-lived servers)
sudo tailscale up --authkey=tskey-auth-xxxxxxxx --reset

Key Flags Explained

Flag What It Does
--authkey Use a pre-generated key instead of browser auth — essential for headless servers and automation
--hostname Override the machine hostname shown in the Tailscale admin console
--advertise-routes Announce that this node can route traffic to these CIDRs — this is subnet routing
--advertise-exit-node Announce that this node will forward all internet-bound traffic for other nodes
--accept-routes Accept and install routes advertised by subnet routers in your tailnet
--accept-dns Accept MagicDNS settings distributed by the coordination server
--ssh Enable Tailscale SSH (replaces sshd with Tailscale-managed SSH access)
--shields-up Block all incoming connections — useful for laptops on public Wi-Fi
--reset Reset all local state and re-authenticate
--login-server Connect to an alternative control server (required for Headscale)

Core Homelab Use Cases

Subnet Routing: Access Your Entire LAN

Subnet routing is the feature that turns Tailscale from a “connect to specific devices” tool into a “reach your whole homelab from anywhere” tool.

Without subnet routing, only devices that have Tailscale installed are reachable. With subnet routing, one Tailscale node acts as a gateway for an entire IP subnet. Devices on that subnet (switches, printers, NAS appliances, IoT devices, anything) become reachable through the gateway without needing Tailscale installed on each.

On the node that will act as the subnet router:

1
2
3
4
5
6
7
# Enable IP forwarding (required for routing)
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

# Advertise your LAN subnet
sudo tailscale up --advertise-routes=192.168.1.0/24

In the Tailscale admin console (app.tailscale.com):

Go to the Machines page, find the router node, click the three-dot menu, and select “Edit route settings.” Enable the advertised routes. Routes require explicit approval to prevent accidental network exposure.

Alternatively, approve routes via the API or set up auto-approvers in your ACL policy (see the ACL section below).

On client devices that want to use the subnet:

1
2
3
4
5
6
7
# Accept routes from subnet routers
sudo tailscale up --accept-routes

# Verify routes are installed
ip route show | grep tailscale
# or on macOS:
netstat -rn | grep 100.

Now from your laptop anywhere in the world, ssh 192.168.1.100 reaches your homelab server as if you were on the LAN. Your NAS at 192.168.1.50, your Proxmox host at 192.168.1.10, your router’s web interface at 192.168.1.1 — all accessible.

Multiple subnet routers for redundancy:

You can run multiple subnet routers for the same subnet. Tailscale will route through whichever one is reachable. If your primary gateway goes down, traffic automatically routes through the secondary.

1
2
# On a second machine on the same LAN
sudo tailscale up --advertise-routes=192.168.1.0/24

Exit Nodes: Route All Traffic Through Your Homelab

An exit node is a subnet router for 0.0.0.0/0 — all internet traffic from clients is sent through the exit node before going to the internet. This is equivalent to a traditional full-tunnel VPN.

Common use cases:

  • Appear to originate from home while traveling
  • Route through your Pi-hole for ad-blocking on all networks
  • Bypass geographic restrictions using your home IP
  • Protect against hostile Wi-Fi networks

Set up an exit node:

1
2
3
4
5
6
# Enable IP forwarding
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

# Advertise as an exit node
sudo tailscale up --advertise-exit-node

Approve the exit node in the admin console (Machines → your node → Edit route settings → toggle “Use as exit node”).

Use the exit node from a client:

1
2
3
4
5
6
7
8
9
# Route all traffic through a specific exit node
sudo tailscale up --exit-node=100.64.x.x         # by IP
sudo tailscale up --exit-node=homelab-gateway     # by hostname

# Route through exit node but keep local LAN access
sudo tailscale up --exit-node=100.64.x.x --exit-node-allow-lan-access

# Disable exit node
sudo tailscale up --exit-node=

On iOS/Android, the exit node is selectable from the app under “Exit Node.”

Exit node firewall configuration:

The exit node needs to masquerade outgoing traffic. Tailscale handles this automatically on Linux if iptables or nftables is available, but verify it’s working:

1
2
# Check that MASQUERADE rule was added
sudo iptables -t nat -L POSTROUTING -n -v | grep MASQUERADE

Tailscale SSH

Tailscale SSH completely replaces traditional SSH for accessing machines on your tailnet. Instead of managing SSH keys, authorized_keys files, or SSH certificates, Tailscale handles authentication using the same WireGuard-backed identity that secures all other tailnet traffic.

What Tailscale SSH provides:

  • No separate SSH keys to manage — authentication is via your Tailscale identity
  • ACL-controlled access — SSH access is governed by the same ACL policy as all other connections
  • Session recording — SSH sessions can be recorded and stored (available on Teams/Enterprise plans)
  • No sshd port exposure — Tailscale SSH doesn’t require port 22 to be reachable from the internet

Enable on a node:

1
sudo tailscale up --ssh

This installs a Tailscale SSH server that listens on the tailnet interface. Your existing sshd continues to work independently.

Connect via Tailscale SSH:

1
2
3
4
5
6
7
# The tailscale binary acts as an SSH client
tailscale ssh user@hostname
tailscale ssh ubuntu@100.64.12.34
tailscale ssh pi@raspberrypi

# Or use regular ssh through the tailscale proxy
ssh -o ProxyCommand='tailscale nc %h %p' user@hostname

Configure check mode (force re-authentication):

In the ACL policy, you can require re-authentication for SSH sessions to privileged users or sensitive machines:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "ssh": [
    {
      "action": "check",
      "src": ["autogroup:member"],
      "dst": ["tag:production"],
      "users": ["root", "ubuntu"]
    }
  ]
}

The check action requires the user to re-authenticate before being granted access, rather than using the device’s existing Tailscale session.

MagicDNS: Hostname Resolution Across Your Tailnet

MagicDNS automatically assigns short hostnames to every device on your tailnet. Instead of remembering 100.64.12.34, you can ssh homelab-pi or browse to http://grafana from anywhere on the tailnet.

How it works: When MagicDNS is enabled, Tailscale pushes a DNS configuration to every device. The DNS resolver for *.ts.net (or your custom CNAME domain) is handled by Tailscale, returning the 100.x.x.x address for the requested hostname.

Enable MagicDNS: In the Tailscale admin console → DNS → Enable MagicDNS.

Name format: By default, hostnames are <machine-name>.<tailnet-name>.ts.net. With MagicDNS, they resolve within your tailnet without specifying the full domain:

1
2
3
4
# These all work after MagicDNS is enabled
ssh homelab-pi
curl http://grafana:3000
ping proxmox

Custom domain suffix: In the admin console you can configure a custom domain (e.g., tailnet.yourdomain.com) so hostnames look like homelab-pi.tailnet.yourdomain.com.

Split DNS: You can push custom DNS servers for specific domains, so that internal names like .home.local or .corp.internal resolve via your internal DNS server (Pi-hole, AdGuard Home, Bind9), while all other DNS goes through your configured resolvers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// In ACL policy file
{
  "dnsConfig": {
    "resolvers": [
      {"addr": "1.1.1.1"},
      {"addr": "8.8.8.8"}
    ],
    "routes": {
      "home.local": [{"addr": "192.168.1.53"}],
      "internal": [{"addr": "192.168.1.53"}]
    },
    "domains": ["home.local"],
    "searchDomains": ["home.local"]
  }
}

With this configuration, ping nas.home.local resolves via your homelab DNS server, while ping google.com uses 1.1.1.1.

Sharing Nodes with Others

You can share individual nodes with people outside your tailnet — useful for sharing a file server with a friend, or giving a collaborator access to a specific service.

In the admin console → Machines → select a machine → “Share…” → enter the email address of the person to share with. They’ll receive an invitation and the machine will appear in their tailnet with a special tag indicating it’s from an external network.

Sharing is constrained by ACLs. Shared nodes can only be accessed according to the policy you’ve written.


Access Control Lists (ACLs)

The default Tailscale policy allows every device on your tailnet to reach every other device on every port. That’s fine for a personal network, but for a team, homelab with multiple users, or any security-conscious setup, you want explicit allow rules with an implicit deny-all default.

Tailscale’s ACL policy is written in HuJSON — a superset of JSON that allows comments (both // single-line and /* */ block) and trailing commas. This makes the policy files significantly more readable than strict JSON.

Policy File Structure

 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
{
  // Groups — named sets of users
  "groups": {
    "group:admins":    ["user@example.com", "admin@example.com"],
    "group:developers": ["dev1@example.com", "dev2@example.com"],
    "group:read-only":  ["readonly@example.com"]
  },

  // Tag owners — who can assign tags to machines
  "tagOwners": {
    "tag:server":      ["group:admins"],
    "tag:database":    ["group:admins"],
    "tag:gateway":     ["group:admins"],
    "tag:dev-server":  ["group:admins", "group:developers"],
    "tag:ci":          ["group:admins"]
  },

  // ACL rules — evaluated top to bottom, first match wins
  "acls": [
    // Admins can reach everything on any port
    {
      "action": "accept",
      "src":    ["group:admins"],
      "dst":    ["*:*"]
    },
    // Developers can SSH into dev servers
    {
      "action": "accept",
      "src":    ["group:developers"],
      "dst":    ["tag:dev-server:22"]
    },
    // Developers can access web services on servers
    {
      "action": "accept",
      "src":    ["group:developers"],
      "dst":    ["tag:server:80", "tag:server:443", "tag:server:8080", "tag:server:8443"]
    },
    // All users can access Grafana and monitoring
    {
      "action": "accept",
      "src":    ["autogroup:member"],
      "dst":    ["tag:server:3000", "tag:server:9090"]
    },
    // CI/CD runners can deploy to servers via SSH
    {
      "action": "accept",
      "src":    ["tag:ci"],
      "dst":    ["tag:server:22"]
    },
    // Servers can reach databases
    {
      "action": "accept",
      "src":    ["tag:server"],
      "dst":    ["tag:database:5432", "tag:database:3306", "tag:database:6379"]
    }
    // Everything else is implicitly denied
  ],

  // SSH-specific rules (for Tailscale SSH)
  "ssh": [
    // Admins can SSH to any machine as any user
    {
      "action": "accept",
      "src":    ["group:admins"],
      "dst":    ["autogroup:self", "tag:server", "tag:database"],
      "users":  ["autogroup:nonroot", "root"]
    },
    // Developers can SSH to dev servers as non-root
    {
      "action": "accept",
      "src":    ["group:developers"],
      "dst":    ["tag:dev-server"],
      "users":  ["autogroup:nonroot"]
    }
  ]
}

Tags

Tags are labels applied to machines that decouple identity from ownership. Instead of writing ACL rules tied to specific user emails (which breaks when people leave the team), tags let you write rules like “servers can accept SSH from CI runners.”

A tagged machine’s access policy is independent of who owns it. Tags are assigned by admins.

1
2
3
4
# Tag a machine when bringing it up
sudo tailscale up --authkey=tskey-auth-xxxxx-TAGS=tag:server,tag:database

# Or tag via the admin console: Machines → machine → three-dot menu → Edit ACL tags

Autogroups

Tailscale has built-in autogroups that automatically include devices based on their relationship to the tailnet:

Autogroup Who’s Included
autogroup:admin Users with admin role in the tailnet
autogroup:member All authenticated tailnet members
autogroup:self The device itself (for allowing self-access)
autogroup:nonroot Any non-root Linux user account
autogroup:tagged All tagged machines
autogroup:internet The internet (for Funnel rules)

Auto-Approvers

By default, subnet routes and exit nodes require manual approval in the admin console. Auto-approvers let you define rules in the ACL policy that automatically approve routes from specific machines:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "autoApprovers": {
    // Automatically approve subnet routes from machines tagged 'gateway'
    "routes": {
      "192.168.1.0/24": ["tag:gateway"],
      "10.0.0.0/8":     ["tag:gateway"],
      "0.0.0.0/0":      ["tag:exit-node"]
    },
    // Automatically approve exit node status for tagged machines
    "exitNode": ["tag:exit-node"]
  }
}

With auto-approvers, a new subnet router tagged tag:gateway immediately starts routing without requiring admin console interaction. This is essential for automated deployments.

Testing Your ACL Policy

Before deploying a policy change, test it in the admin console’s Policy page. The “Test” tab lets you enter a source, destination, and port to verify what the policy allows or denies — without touching production.


Tailscale Funnel

Tailscale Funnel exposes services on your tailnet to the public internet through Tailscale’s infrastructure. It’s similar in concept to Cloudflare Tunnels: your service gets a publicly accessible HTTPS URL without any open firewall ports.

Key properties:

  • The URL is https://<machine-name>.<tailnet-name>.ts.net — automatically gets a valid TLS certificate
  • Traffic goes through Tailscale’s infrastructure to your machine
  • The service on your machine can be HTTP, HTTPS, or TCP
  • No registration, no custom domain required (though custom domains are supported)
  • Free on all plans (with limits on the free tier)

Enable Funnel in the admin console under DNS → HTTPS Certificates → Enable (you need HTTPS certificates enabled first), then enable Funnel under the Machines section or via the CLI.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Serve a local HTTP service (port 3000) publicly via HTTPS
sudo tailscale funnel 3000

# Serve a specific service
sudo tailscale funnel --bg 8080   # Background mode — persists across reboots

# Serve with a specific path
sudo tailscale funnel --bg 8080

# Show current funnel configuration
tailscale funnel status

# Stop funnel on a port
sudo tailscale funnel --bg 8080 off

Funnel use cases for homelabs:

  • Share a self-hosted app with someone outside your tailnet without giving them Tailscale access
  • Receive webhooks from GitHub, Stripe, or other services during development
  • Run a public portfolio site from your homelab
  • Share a demo of a project without deploying to a cloud provider

Funnel limitations:

  • Maximum connections and bandwidth are limited on the free tier
  • The URL is controlled by Tailscale (though you can CNAME your own domain to it)
  • Not intended for high-traffic production workloads

Tailscale Serve

Tailscale Serve is Funnel’s internal counterpart — it makes a local service available to other devices on your tailnet under a consistent hostname and port, without exposing it to the internet.

The difference from just accessing the raw port is convenience and TLS. Serve gives you:

  • A consistent URL (https://machine-name.ts.net) that works from anywhere on the tailnet
  • Automatic TLS certificate from Tailscale (valid Let’s Encrypt cert)
  • HTTP-to-HTTPS redirect handling
  • Port multiplexing — serve multiple services on the same machine under different paths
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Serve a local HTTP service on the tailnet HTTPS endpoint
sudo tailscale serve 3000

# Serve with a path prefix
sudo tailscale serve --set-path /grafana 3000
sudo tailscale serve --set-path /prometheus 9090

# Serve a static directory
sudo tailscale serve --bg /var/www/html

# Serve HTTPS with an existing local HTTPS service
sudo tailscale serve --bg https://localhost:8443

# Show current serve configuration
tailscale serve status

# Remove a serve configuration
sudo tailscale serve --bg 3000 off

Serve configuration file (stored in /etc/tailscale/serve.json):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "TCP": {
    "443": {
      "HTTPS": true
    }
  },
  "Web": {
    "${TS_CERT_DOMAIN}:443": {
      "Handlers": {
        "/": {
          "Proxy": "http://127.0.0.1:3000"
        },
        "/api": {
          "Proxy": "http://127.0.0.1:8080"
        },
        "/metrics": {
          "Proxy": "http://127.0.0.1:9090"
        }
      }
    }
  }
}

Self-Hosting the Control Plane with Headscale

Tailscale’s control plane is proprietary and hosted by Tailscale, Inc. If you need full self-sovereignty — whether for compliance, air-gapped networks, or philosophical preference — Headscale is the answer.

Headscale is an open-source, self-hosted implementation of the Tailscale control plane. It implements the same Tailscale client protocol, which means you use the official Tailscale clients on all devices, but your coordination server runs on your own infrastructure instead of Tailscale’s servers.

What Headscale gives you:

  • No dependency on Tailscale’s servers
  • Full control over device enrollment, ACLs, and key distribution
  • Works in air-gapped networks
  • No data sent to Tailscale’s coordination server
  • Can run completely offline after initial client setup

What Headscale lacks vs Tailscale’s hosted offering:

  • No Funnel (Headscale has no edge infrastructure)
  • No session recording for SSH
  • No Tailscale Serve (you can configure this manually)
  • More operational burden — you run and maintain the control plane
  • No mobile push notifications for device authorization

Deploying Headscale

Headscale requires a public-facing server (or one reachable by all your devices) with a domain name and TLS certificate.

1
2
3
# Install on Debian/Ubuntu
wget https://github.com/juanfont/headscale/releases/latest/download/headscale_linux_amd64.deb
dpkg -i headscale_linux_amd64.deb

Configuration file (/etc/headscale/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
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
# /etc/headscale/config.yaml

# The server's public URL — all clients connect to this
server_url: https://headscale.yourdomain.com

# Listen address for the gRPC and HTTP API
listen_addr: 0.0.0.0:8080
grpc_listen_addr: 0.0.0.0:50443

# Metrics endpoint
metrics_listen_addr: 0.0.0.0:9090

# TLS — either provide certs directly or use Let's Encrypt
tls_letsencrypt_hostname: headscale.yourdomain.com
tls_letsencrypt_cache_dir: /var/lib/headscale/acme
tls_letsencrypt_challenge_type: TLS-ALPN-01
# OR: provide existing certs:
# tls_cert_path: /etc/ssl/headscale.crt
# tls_key_path: /etc/ssl/headscale.key

# Database — SQLite for small deployments, PostgreSQL for production
db_type: sqlite3
db_path: /var/lib/headscale/db.sqlite
# For PostgreSQL:
# db_type: postgres
# db_host: localhost
# db_port: 5432
# db_name: headscale
# db_user: headscale
# db_pass: yourpassword

# IP address pool for clients — use the same 100.64.x.x range
ip_prefixes:
  - 100.64.0.0/10
  - fd7a:115c:a1e0::/48  # IPv6

# DERP — either use Tailscale's public DERP servers or self-host
derp:
  server:
    enabled: false   # Set to true to run your own DERP server
    region_id: 999
    region_code: "headscale"
    region_name: "Headscale Embedded DERP"
    stun_listen_addr: "0.0.0.0:3478"
  urls:
    - https://controlplane.tailscale.com/derpmap/default  # Tailscale's DERP map

# MagicDNS
dns_config:
  override_local_dns: true
  nameservers:
    - 1.1.1.1
    - 8.8.8.8
  domains: []
  magic_dns: true
  base_domain: headscale.yourdomain.com

# OIDC authentication (allows users to log in with an external IdP)
oidc:
  only_start_if_oidc_is_available: false
  issuer: https://auth.yourdomain.com
  client_id: headscale
  client_secret: your-oidc-secret
  scope:
    - openid
    - profile
    - email
  extra_params:
    domain_hint: yourdomain.com

# Log level
log:
  level: info

Start Headscale:

1
2
3
4
sudo systemctl enable --now headscale

# Verify
sudo systemctl status headscale

Create a user (namespace):

1
2
3
4
5
# Create a user/namespace for device enrollment
sudo headscale users create homelab

# List users
sudo headscale users list

Generate auth keys for devices:

1
2
3
4
5
# Generate a pre-auth key for automated device enrollment
sudo headscale preauthkeys create --user homelab --reusable --expiration 24h

# For one-time keys (more secure)
sudo headscale preauthkeys create --user homelab --expiration 1h

Connect clients to Headscale:

1
2
3
4
5
6
# On each client device — point to your Headscale server instead of Tailscale's
sudo tailscale up --login-server=https://headscale.yourdomain.com --authkey=<headscale-authkey>

# Or authenticate interactively
sudo tailscale up --login-server=https://headscale.yourdomain.com
# This gives you a URL to register the device via the API

Headscale CLI management:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# List all registered nodes
sudo headscale nodes list

# Show routes (subnet routers)
sudo headscale routes list

# Approve a route
sudo headscale routes enable --route <route-id>

# Delete a node
sudo headscale nodes delete --identifier <node-id>

# Generate an API key for programmatic access
sudo headscale apikeys create

# Show ACL policy
sudo headscale policy get

Headscale with Docker Compose:

 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
# docker-compose.yml
services:
  headscale:
    image: headscale/headscale:latest
    restart: unless-stopped
    container_name: headscale
    ports:
      - "8080:8080"     # HTTP/gRPC
      - "9090:9090"     # Metrics
      - "3478:3478/udp" # STUN (if using embedded DERP)
    volumes:
      - ./config:/etc/headscale
      - headscale-data:/var/lib/headscale
    command: serve

  headscale-ui:
    image: ghcr.io/gurucomputing/headscale-ui:latest
    restart: unless-stopped
    container_name: headscale-ui
    ports:
      - "8443:443"
    environment:
      - HEADSCALE_SERVER=http://headscale:8080

volumes:
  headscale-data:

Command Reference

tailscale status

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
tailscale status

# Example output:
# 100.64.1.1     homelab-gateway  user@example.com  linux   -
# 100.64.1.2     laptop           user@example.com  macOS   -
# 100.64.1.3     phone            user@example.com  iOS     -
# 100.64.1.4     raspi-router     user@example.com  linux   idle, tx 1.4MB rx 892KB

# Show JSON output for scripting
tailscale status --json

# Show peers and self in a compact format
tailscale status --peers=false  # Show only self
tailscale status --self=false   # Show only peers

Key columns: IP address, hostname, user, OS, and connection state (direct, relay via <region>, idle, offline).

tailscale ping

Unlike ICMP ping, tailscale ping sends a Tailscale-layer ping that tests the WireGuard data path specifically. It shows whether you have a direct connection or are going through a DERP relay, and measures the round-trip latency.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Ping by hostname
tailscale ping homelab-gateway

# Ping by IP
tailscale ping 100.64.1.1

# Example output:
# pong from homelab-gateway (100.64.1.1) via DERP(nyc) in 45ms
# pong from homelab-gateway (100.64.1.1) via DERP(nyc) in 44ms
# pong from homelab-gateway (100.64.1.1) via 192.168.1.100:41641 in 1.2ms
# Direct connection established; stopping
#
# (First two pings through DERP relay while direct path is being established,
# then switches to direct connection at 1.2ms — typical LAN latency)

# Ping count
tailscale ping --c 5 homelab-gateway

# Ping until direct connection is established
tailscale ping --until-direct homelab-gateway

# Force use of DERP (for testing relay path)
tailscale ping --tsmp homelab-gateway

tailscale netcheck

Diagnoses your current network’s Tailscale connectivity: NAT type, available DERP servers, and whether direct connections are possible.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
tailscale netcheck

# Example output:
# Report:
#   * UDP: true
#   * IPv4: yes, 203.0.113.42:41641
#   * IPv6: no
#   * MappingVariesByDestIP: false  (good — means direct connections work)
#   * HairPinning: false
#   * PortMapping: UPnP
#   * CaptivePortal: false
#
# DERP latency:
#   - nyc: 12ms (closest)
#   - sfo: 68ms
#   - lhr: 110ms

MappingVariesByDestIP: true means you’re behind a symmetric NAT — direct connections will be difficult and most traffic will relay through DERP. This is common on some corporate networks and mobile carrier NATs.

tailscale ssh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# SSH to a tailnet device
tailscale ssh user@hostname
tailscale ssh ubuntu@homelab-pi
tailscale ssh root@100.64.1.1

# Run a remote command
tailscale ssh user@host 'systemctl status nginx'

# Forward ports
tailscale ssh -L 8080:localhost:3000 user@homelab-pi

tailscale serve

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Serve a local port on the tailnet HTTPS endpoint
sudo tailscale serve 3000

# Serve with path routing
sudo tailscale serve --set-path /app 3000
sudo tailscale serve --set-path /api 8080

# Serve in background (survives reboots)
sudo tailscale serve --bg 3000

# Show current serve configuration
tailscale serve status

# Remove a serve handler
sudo tailscale serve --bg 3000 off

tailscale funnel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Expose a local port to the public internet
sudo tailscale funnel 8080

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

# Show funnel status
tailscale funnel status

# Stop a funnel
sudo tailscale funnel --bg 8080 off

Other Useful Commands

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Show this device's Tailscale IP
tailscale ip

# Show both IPv4 and IPv6
tailscale ip -4  # IPv4 only
tailscale ip -6  # IPv6 only

# Lock/unlock the tailnet key (tailnet lock — guards against coordination server compromise)
tailscale lock status
tailscale lock init
tailscale lock unlock

# Show version
tailscale version

# Logout
sudo tailscale logout

# Switch between accounts
sudo tailscale switch <tailnet-name>

# Debug info (for bug reports)
tailscale bugreport

Tips, Tricks, and Advanced Patterns

Raspberry Pi as a Homelab Gateway

The canonical homelab Tailscale setup: a Raspberry Pi (or any always-on Linux box) acts as the gateway, running both subnet routing and an exit node. Everything on your LAN becomes reachable, and you can route all travel traffic through home.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# On the Pi
sudo apt install tailscale

# Enable forwarding
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

# Bring up as gateway + exit node
sudo tailscale up \
  --advertise-routes=192.168.1.0/24 \
  --advertise-exit-node \
  --accept-routes \
  --accept-dns \
  --ssh \
  --hostname=homelab-pi

# Verify routing
tailscale status
ip route show

Approve the advertised routes in the admin console (or configure auto-approvers in the ACL policy). The Pi now serves as a persistent always-on gateway to your home network.

Tailscale in Docker (Sidecar Pattern)

For containerized services that need to be reachable on your tailnet without exposing ports on the host:

 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
# docker-compose.yml — Tailscale sidecar
services:
  tailscale:
    image: tailscale/tailscale:latest
    container_name: tailscale
    hostname: my-service-ts
    restart: unless-stopped
    network_mode: host   # Needs host networking for proper integration
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    devices:
      - /dev/net/tun
    volumes:
      - tailscale-state:/var/lib/tailscale
      - /dev/net/tun:/dev/net/tun
    environment:
      - TS_AUTHKEY=tskey-auth-xxxxxxxx
      - TS_HOSTNAME=my-service
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_USERSPACE=false    # Use kernel WireGuard if available

  app:
    image: my-app:latest
    network_mode: service:tailscale   # Share network namespace with Tailscale
    depends_on:
      - tailscale

volumes:
  tailscale-state:

With network_mode: service:tailscale, the app container shares the Tailscale container’s network namespace. The app is reachable on your tailnet at the Tailscale sidecar’s IP without any host port bindings.

For Kubernetes, use Tailscale as a sidecar container or use the Tailscale Kubernetes Operator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Kubernetes — Tailscale operator approach
apiVersion: v1
kind: Service
metadata:
  name: my-service-tailscale
  namespace: default
  annotations:
    tailscale.com/expose: "true"
    tailscale.com/hostname: "my-k8s-service"
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP

Exit Node for Travel

When traveling through airports, hotels, and coffee shops — especially in countries with invasive surveillance or blocking — routing through a home exit node gives you your home IP and access to all home services simultaneously.

1
2
3
4
5
6
7
8
9
# On your laptop before a trip
sudo tailscale up --exit-node=homelab-pi --exit-node-allow-lan-access

# Verify your exit IP
curl https://ipinfo.io/ip
# Should show your home IP address

# When you need local network access at the destination, disable briefly:
sudo tailscale up --exit-node=

The --exit-node-allow-lan-access flag is critical when traveling — without it, your local hotel network is also routed through home, which prevents you from accessing hotel captive portals and local devices.

Split DNS for Internal Services

Configure Tailscale to use a specific DNS server for your internal domains, while routing public DNS normally. This lets grafana.home.local resolve via your Pi-hole, while google.com resolves via 1.1.1.1.

In your ACL policy file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "dnsConfig": {
    "resolvers": [
      {"addr": "1.1.1.1"},
      {"addr": "8.8.8.8"}
    ],
    "routes": {
      "home.local": [{"addr": "192.168.1.53"}],
      "homelab.internal": [{"addr": "192.168.1.53"}]
    },
    "searchDomains": ["home.local"]
  }
}

Replace 192.168.1.53 with the IP of your internal DNS server (Pi-hole, AdGuard Home, Unbound, etc.). After pushing this policy, ping nas.home.local from any tailnet device resolves to your homelab NAS.

Auth Keys for Automation

For adding machines without browser-based authentication — CI runners, cloud instances, automated deployments:

In the admin console → Settings → Keys → Generate auth key.

Key options:

  • Reusable: Can be used to add multiple machines (vs single-use)
  • Ephemeral: Device is automatically removed from the tailnet when it goes offline (good for CI runners)
  • Pre-authorized: Device is added without requiring admin approval
  • Tags: Machine is automatically tagged on enrollment
1
2
3
4
5
6
7
8
9
# Enroll a CI runner (ephemeral, so it's removed after the job)
sudo tailscale up \
  --authkey=tskey-auth-xxxxxxxx-TAGS=tag:ci \
  --hostname=ci-runner-${BUILD_ID}

# Enroll a production server (persistent, tagged)
sudo tailscale up \
  --authkey=tskey-auth-yyyyyyyy-TAGS=tag:server,tag:production \
  --hostname=prod-web-01

Tailscale + Pi-hole Integration

Run Pi-hole on a Tailscale-connected host and use it as the DNS resolver for your entire tailnet. Every device on your tailnet gets ad-blocking automatically.

1
2
3
# On the Pi-hole host, enable Tailscale and get its tailnet IP
sudo tailscale up
tailscale ip   # Note this IP, e.g., 100.64.1.5

In Tailscale admin console → DNS:

  • Add nameserver: 100.64.1.5 (your Pi-hole’s Tailscale IP)
  • Check “Override local DNS” to force all tailnet devices to use Pi-hole

Now every device on your tailnet — including mobile devices — gets Pi-hole ad-blocking when connected to your tailnet, from anywhere in the world.


Competitors and Honest Comparisons

WireGuard (Manual)

WireGuard is what Tailscale is built on. Choosing raw WireGuard over Tailscale means doing manually what Tailscale automates: key exchange, peer discovery, NAT traversal, IP assignment, routing, and policy.

Tailscale WireGuard (manual)
Setup time 5 minutes 30–60 minutes minimum
NAT traversal Automatic (DERP fallback) Manual relay or hub-and-spoke required
Key management Automatic Manual
IP assignment Automatic (100.x.x.x) Manual
Works behind CGNAT Yes Requires relay server
Control plane Hosted (or Headscale) None — you are the control plane
Policy/ACLs Built-in Implemented via AllowedIPs + firewall
Privacy Tailscale sees metadata You control everything
Cost Free tier + paid plans Free and open source

Choose WireGuard (manual) when: You want zero external dependencies, need full control of every component, have an existing VPN infrastructure, or are in an air-gapped environment without internet access. The operational burden is real but manageable for small networks.

Choose Tailscale when: You want to spend time on your infrastructure rather than your VPN, need to work through CGNAT or restrictive firewalls, or are adding devices regularly.

ZeroTier

ZeroTier is the closest architectural analog to Tailscale — a managed overlay network with a coordination server and direct P2P tunnels. Key differences:

Tailscale ZeroTier
Underlying protocol WireGuard Custom (similar goals)
Encryption WireGuard’s fixed suite Custom crypto
NAT traversal Excellent (DERP fallback) Good (relay fallback)
ACL model Policy file (HuJSON) Flow rules (more complex)
Self-hosting Headscale (mature) ZeroTier Network Controller (supported)
Performance Higher (kernel WireGuard) Good
Client maturity Excellent Good
Free tier 3 users, 100 devices 25 devices
Layer L3 only L2 (Ethernet-level) or L3

ZeroTier’s L2 mode is its unique feature — it can emulate an Ethernet segment, which Tailscale cannot. If you need devices to be on the same broadcast domain (required for some legacy protocols, Wake-on-LAN, multicast), ZeroTier is the better choice. For most homelab and remote access use cases, the difference is irrelevant, and Tailscale’s WireGuard-based performance and simpler model wins.

Netbird

Netbird is a newer open-source alternative that is almost entirely self-hostable — including the relay infrastructure and management server. Its architecture closely mirrors Tailscale (WireGuard + coordination server + STUN/TURN for NAT traversal).

Tailscale Netbird
Underlying protocol WireGuard WireGuard
Control plane self-host Headscale (third-party) Official (management server)
TURN relay self-host No (Tailscale’s DERP) Yes (Coturn)
License Proprietary client Open source
ACL model HuJSON policy Management UI / API
Maturity Very mature Maturing rapidly
Client quality Excellent Good
Free tier 3 users, 100 devices 5 users, unlimited devices

Netbird is the best choice if you want the Tailscale experience with full self-hosting control, including the relay servers. It’s more operationally complex than just running Headscale, but gives you complete independence.

Nebula

Nebula was created by Slack for their internal network. It is fully self-hosted by design — there is no managed option.

Tailscale Nebula
Architecture Managed + optional self-host Fully self-hosted only
Underlying crypto WireGuard Custom (Noise protocol, similar)
NAT traversal Automatic Lighthouse servers (your own)
Certificate model Public key (device identity) Certificate authority
Configuration Minimal More complex (certificates, CA)
Scalability Tens of thousands of nodes Very large networks

Nebula shines for large organizations that need complete self-hosting, a certificate-based trust model, and can absorb the operational complexity. For a homelab or small team, Tailscale’s operational simplicity wins.

Traditional VPNs (OpenVPN, IPsec)

OpenVPN and IPsec are the previous generation — they work, they’re widely deployed, and they’re widely understood. What they lack:

  • NAT traversal: Both require a server with a publicly reachable port
  • Performance: OpenVPN is CPU-intensive userspace code; WireGuard is a kernel module
  • Connection time: TLS negotiation in OpenVPN takes 1–3 seconds; WireGuard/Tailscale connects in milliseconds
  • Complexity: OpenVPN configurations for complex scenarios (certificate management, CRL, LDAP auth) are genuinely complex
  • Mobile experience: OpenVPN on mobile has poor reconnect behavior; Tailscale reconnects silently and instantly

The main reasons to choose OpenVPN or IPsec today:

  • Enterprise requirements for certificate-based authentication with LDAP integration
  • Environments that have standardized on them with existing infrastructure
  • TCP mode (OpenVPN TCP/443 traverses aggressive firewalls that block UDP)
  • Legacy client support requirements

For new deployments without legacy constraints, there’s no technical reason to choose OpenVPN over Tailscale or raw WireGuard.


Pricing

As of early 2026, Tailscale’s pricing tiers are:

Free (Personal)

  • 3 users
  • 100 devices
  • All core features: subnet routing, exit nodes, MagicDNS, Tailscale SSH
  • Tailscale Funnel and Serve
  • HTTPS certificates
  • No ACL policy complexity restrictions
  • Suitable for solo homelabbers and small families

Personal Pro

  • Roughly $5–8/month (check current pricing at tailscale.com/pricing)
  • Supports up to 6 users
  • All Free features

Teams

  • Per-user pricing (roughly $5–6/user/month)
  • Unlimited devices
  • SSO integration with major identity providers
  • ACL tags and auto-approvers
  • Audit logging
  • Admin console for organization management
  • Suitable for small teams and businesses

Enterprise

  • Custom pricing
  • SAML/SCIM provisioning
  • Session recording
  • Priority support
  • Custom contract terms
  • Large-scale deployments

For homelab use: The free tier is almost always sufficient. 3 users and 100 devices covers the vast majority of home/homelab scenarios. If you exceed 3 users (family members, collaborators), Personal Pro is the most economical step up.


Limitations and Gotchas

CGNAT address space collisions: Tailscale uses 100.64.0.0/10. Some ISPs and corporate networks use this range for CGNAT. If your ISP uses 100.x.x.x addressing internally, you may have route conflicts. This is uncommon but real — check with ip route show if things seem off.

Windows DNS behavior: On Windows, --accept-dns can sometimes conflict with existing DNS configurations. If you have DNS issues on Windows, check if Tailscale’s DNS is winning over corporate DNS or vice versa. The tailscale dns command and Windows’ DNS debug tools help diagnose this.

No built-in TCP fallback for WireGuard: Like all WireGuard-based tools, Tailscale uses UDP. Networks that drop non-HTTPS UDP traffic (some corporate networks, some hotels) will fail direct connections. DERP handles this by relaying over HTTPS, but if even HTTPS is proxied or inspected by a corporate firewall, DERP may fail too. In those environments, a WireGuard-over-TCP wrapper or a traditional VPN with TCP mode may be necessary.

Subnet routing performance: Traffic to subnet-routed devices goes through the subnet router node. If the router is a Raspberry Pi 3, it becomes the bottleneck — the Pi 3’s limited CPU and 100 Mbps Ethernet cap throughput well below what WireGuard can do on faster hardware. A Pi 4 or Pi 5 handles typical homelab loads easily.

Tailscale client must stay authenticated: By default, Tailscale requires periodic re-authentication (every 90–180 days for servers, configurable in the admin console). If a server’s session expires without a human to re-auth, it drops off the tailnet. For servers, use auth keys and configure key expiry appropriately, or enable “Disable key expiry” for server nodes in the admin console.

Ephemeral node cleanup: Ephemeral nodes are removed when they go offline, which is ideal for CI runners. But if a node crashes without properly disconnecting, it may linger in the admin console for up to 30 minutes before being cleaned up.

ACL complexity at scale: Tailscale’s ACL model is powerful but can become difficult to reason about in large deployments with many tags, groups, and complex rules. Testing tools in the admin console help, but there is no local ACL simulator — you need a live tailnet to test.

Headscale limitations: Headscale lacks Funnel, session recording, and some newer Tailscale features. Headscale version compatibility with the official Tailscale clients can lag by a version or two. Always check the Headscale compatibility matrix before upgrading either component.

Privacy considerations: Even when using the hosted coordination server, Tailscale cannot see your traffic — it’s WireGuard-encrypted end-to-end. What Tailscale can see is metadata: which devices are on your tailnet, their IP addresses, their connection times, and which DERP regions are used. If metadata privacy is a concern, Headscale eliminates even this.

MTU and fragmentation: WireGuard adds overhead to packets (encapsulation headers). Tailscale automatically manages MTU settings to prevent fragmentation, but in some edge cases (double-encapsulation, unusual MTU environments) you may see TCP performance issues. tailscale netcheck helps diagnose this; adjusting MTU with tailscale up --mtu is the fix.

No layer-2 / broadcast: Tailscale is strictly layer-3. You cannot use it for Wake-on-LAN, mDNS/Bonjour across the tailnet, or any protocol that depends on broadcast or multicast. For WoL specifically, you need a local node that can receive the WoL request and forward it as a unicast packet to the subnet router.


Quick Reference Card

 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
# Install
curl -fsSL https://tailscale.com/install.sh | sh

# First connection
sudo tailscale up
sudo tailscale up --authkey=tskey-auth-xxxxx     # Unattended

# Common node roles
sudo tailscale up --advertise-routes=192.168.1.0/24   # Subnet router
sudo tailscale up --advertise-exit-node               # Exit node
sudo tailscale up --accept-routes                     # Use subnet routes
sudo tailscale up --accept-dns                        # Use MagicDNS
sudo tailscale up --ssh                               # Enable Tailscale SSH

# Status and diagnostics
tailscale status                                      # All peers + connection state
tailscale status --json                               # Machine-readable
tailscale ip                                          # Your tailnet IP
tailscale ping <hostname>                             # Test connection path
tailscale netcheck                                    # Network diagnostics

# SSH
tailscale ssh user@hostname

# Serve and Funnel
sudo tailscale serve 3000                             # Tailnet HTTPS serve
sudo tailscale funnel --bg 8080                       # Public internet expose
tailscale serve status
tailscale funnel status

# Exit node (client side)
sudo tailscale up --exit-node=<hostname>
sudo tailscale up --exit-node=                        # Disable exit node

# Headscale
sudo tailscale up --login-server=https://headscale.yourdomain.com
sudo headscale nodes list
sudo headscale preauthkeys create --user homelab --reusable --expiration 24h
sudo headscale routes enable --route <route-id>

# Logging out / resetting
sudo tailscale logout
sudo tailscale up --reset

Tailscale’s most underrated quality is that it disappears. You install it, connect your devices, and forget it exists — until you’re in an airport or on a locked-down corporate network and you realize you still have full access to your homelab, your services are still reachable, and you didn’t have to open a single firewall port to make it happen. For homelabbers, that’s the whole point.

Comments