Skip to content

Tailscale homelab reference: overlay as second path and management plane

How Tailscale runs in this homelab: as a parallel overlay alongside the site-to-site IPsec/GRE tunnels (covered in Magic WAN interop), with its own jobs - out-of-band management, a third path when LAN and tunnel routes are both unavailable, an identity layer for SSH, and since 2026-08-21 a subnet router that reaches VLAN-only devices from any network. Covers the NixOS authkey pattern, the subnet-router cutover, the Unraid plugin, the multi-path SSH alias scheme, and every failure mode observed in production. Hostnames, addresses, and paths are anonymised; the patterns are exactly as built.

TL;DR: Tailscale earns its place for OOB access, per-device identity, and - since the 2026-08-21 cutover - a subnet router that puts every local VLAN one tunnel away from any network; not for bulk site-to-site traffic (IPsec/GRE wins there). The configurations to copy are the NixOS authKeyFile pattern (a rebuilt node rejoins unattended), SSH Host pattern lists (one identity block, three addresses), and the scoped subnet-router firewall rule (tailnet source in, local destinations only, never the WAN). The failures to avoid: placeholder authkeys, stale tailnet entries in a WSL-generated /etc/hosts, an overlay node that was never authenticated in the first place, and routes that are advertised but not forwarded.


Four jobs, in order of how much they matter day to day:

  1. Out-of-band management. When the edge router, the IPsec/GRE tunnel, or local DNS is broken, the tailnet is the path that still works - it shares no fate with any of them. PiKVMs and the second-site Pi stay reachable even when the primary network does not.
  2. A third path for SSH. Every host alias carries a 10.0.x address (LAN or site-to-site), a 10.68.x address (cross-tunnel), and the MagicDNS name. The tailnet path works from arbitrary networks - a hotel line, a phone hotspot - where neither RFC1918 path exists.
  3. Identity for SSH. Tailscale SSH on the Unraid box replaces authorized_keys distribution with tailnet policy, and check mode adds browser re-authentication for high-risk connections.1
  4. Reaching devices that cannot run tailscaled. The router advertises every local VLAN as a subnet route, so the switch, BMCs, and other VLAN-only gear are reachable from any network a tailnet client sits on - enabled 2026-08-21 (see subnet routing).

It is not for bulk traffic between sites: the IPsec/GRE tunnels move more data at lower latency because they are routed, not relayed, and do not depend on fallback via DERP (Designated Encrypted Relay for Packets, Tailscale’s relay network) when NAT traversal fails.

DecisionPayoffCost
Tailscale alongside IPsec/GRE, not instead ofIndependent failure domain for OOBTwo overlays to reason about
Subnet routing on the router, every local VLANVLAN-only devices (switch, BMCs) reachable from any client networkConsole approval after each re-advertisement; per-client route acceptance
Tailscale SSH on Unraid onlyKills key-distribution toil where it hurt mostCheck-mode re-auth URLs interrupt automation
Authkey file on the NixOS applianceRebuilt node rejoins unattendedKey material on disk; 90-day max key lifetime2

Site 1Site 2tailnet100.64.0.0/10NixOS edge routersubnet router, all local VLANsIoT Pi (RPi 4B, NixOS)authkey-provisionedIPsec/GRE site-to-siteUnraid serverTailscale SSHPiKVMs (OOB)dev boxWindows + WSL2

Until 2026-08-21 every node was box-level: tailnet devices reached tailnet devices, nothing more. The router now also advertises every local VLAN as subnet routes, so tailnet clients reach VLAN-only devices too (the switch, BMCs, anything without tailscaled) - the cutover story is in subnet routing. Its input firewall still treats tailscale0 as a management interface - SSH, the management UI port, and ping - and its forward chain carries exactly one rule for subnet-routed traffic, scoped to local destinations only.


ClassExamplesHow it joinsVerified state
NixOS routeredge routerInteractive tailscale up once; node key persists under /var/lib/tailscaleBackendState: Running, home DERP relay #3
NixOS applianceIoT PiauthKeyFile + tailscaled-autoconnect unitRunning; autoconnect unit inert after registration
Unraidmedia/automation serverPlugin manager, interactive login onceRunning, Tailscale 1.102.2
Windows + WSL2dev boxWindows GUI client; WSL2 has no tailscaled-
OOBPiKVMsStock tailscaled on the PiKVM OS-

Two consequences of the split. First, the authkey-joined node can rebuild and rejoin without a browser; the interactively-joined nodes cannot, but their node keys survive reboots under /var/lib/tailscale. Second, anything on the Windows box resolves MagicDNS through the Windows client - WSL2 inherits that DNS but runs no daemon of its own, which produces two of the failure modes below.


The router gets the management-plane form - enable the service with the subnet-router flags, open the management ports on tailscale0 in the input chain, authenticate interactively once:

# router: management plane + subnet router. First auth is interactive:
# `sudo tailscale up` + browser URL (one-time; node key persists
# under /var/lib/tailscale). The advertise-routes flags take effect
# on the next `tailscale up`, not on rebuild alone.
services.tailscale = {
enable = true;
useRoutingFeatures = "server";
extraUpFlags = [ "--advertise-routes=<every local VLAN /24, comma-separated>" ];
};
# nftables input: management plane from the tailnet
# iifname "tailscale0" tcp dport { 22, <mgmt-ui-port> } accept
# iifname "tailscale0" icmp type echo-request accept

The IoT Pi gets the unattended form. The nixpkgs services.tailscale module generates a tailscaled-autoconnect systemd unit when authKeyFile is set; the unit polls tailscale status --json and, on NeedsLogin, runs tailscale up --auth-key "$(cat <keyfile>)" plus any extraUpFlags, then exits once the state reaches Running:3

# IoT Pi: Tailscale is the second way in when the LAN path breaks.
# The auth key is placed by hand, out-of-repo like the other secrets;
# tailscaled consumes it on first start and ignores it once the node
# is registered:
# printf %s '<key from the admin console>' > /etc/tailscale-authkey
# chmod 600 /etc/tailscale-authkey
services.tailscale = {
enable = true;
authKeyFile = "/etc/tailscale-authkey";
};

Verified live after provisioning: BackendState: Running, and tailscaled-autoconnect is inactive - the unit is a one-shot that did its job on first boot and now sits inert. The key file on disk is ignored from then on; the node’s identity lives in /var/lib/tailscale.

Authkeys expire after 1-90 days (90 default), so the key on disk is a consumable: if the node ever needs to re-register after the key lapses, generate a new one and drop it in place. An already-registered node stays authorised until its node key expires.2 Note that re-registration also means a new tailnet IP - when the Pi was re-registered, its old 100.x address was gone and anything pinned to it (including a stale /etc/hosts entry, below) dialed a dead node.


Box-level reachability ended 2026-08-21, mid-servarr-cutover: the dev box gave its wired switch port to the new server and moved to the wireless VLAN, which has no route to the admin VLAN by design. The switch, the new server’s BMC, every VLAN-only device became unreachable. The router now advertises every local VLAN as a subnet route, and the tailnet is the always-on bypass for any network a client sits on. The config is the router variant above; four things make the routes actually carry traffic:

  1. Approval. Advertised routes are inert until approved in the admin console (Machines -> the router -> Edit route settings -> Save). The tailnet policy is not in git, so this is a manual step after every re-advertisement; autoApprovers.routes in the policy would remove the click.

  2. Forwarding. The nft forward chain (policy drop) needs its own rule - the input rules that define the management plane do nothing for transit traffic:

    # tailnet -> local VLANs only; no WAN egress via the tailnet
    # (this box is not an exit node).
    iifname "tailscale0" ip saddr 100.64.0.0/10 ip daddr 10.0.0.0/8 accept

    Source-scoped to the tailnet CGNAT range, destination-scoped to local space: a tailnet client can reach VLAN devices but cannot egress to the WAN through the router. “Use as exit node” stays off in the console.

  3. Re-approval on every change to the advertised set. Adding or renumbering a VLAN (2026-09-04: the IoT segment moved to a new /24 and the router’s --advertise-routes list changed with it) puts the router back at step 1: the new route sits unapproved until the console click, and clients keep the old one until then.

  4. Client acceptance. Off by default, per client: the Windows tray toggle (“Use Tailscale subnets”) or tailscale up --accept-routes. WSL2 has no tailscaled - it inherits accepted routes through the Windows client’s NAT.

Each row below cost time during the enablement, each with a signal that made the step look done when it was not:

ObservationWhat it provesWhere to look when it fails
tailscale ping <vlan-host> answersControl plane only - the route is advertised, approved, and acceptedTest TCP; ping passing while TCP fails points at the forward chain
TCP to a VLAN host times outThe forward chain dropped itnft list chain inet filter forward - is the tailscale0 rule there?
Console machine page lists the route as availableNothing - the Edit dialog’s checkboxes do nothing until SaveRe-open Edit route settings, tick, Save; the machine page’s Approved list is the truth
Get-NetRoute shows the VLAN route on WindowsNothing - the OS installs the route before the tunnel accepts ittailscale status --json AllowedIPs on the router peer: VLAN ranges present means accepted, a lone /32 means not

tailscale up on the router prints a UDP GRO warning for its NIC - a throughput note, not an error; the ethtool fix is optional.


Unraid gets Tailscale from the plugin manager (the tailscale plugin plus unraid-tailscale-utils), not from a container. Enabling Tailscale SSH is one command on the host:

Terminal window
tailscale set --ssh

That claims port 22 on the tailnet IP only - the Unraid SSH daemon, sshd_config, and authorized_keys are untouched, and SSH over the LAN IP keeps working exactly as before.1

The default tailnet policy puts SSH to your own devices in check mode: the first connection of the day returns a login.tailscale.com/a/... URL and waits for browser re-authentication, then stays authorised for the 12-hour check period.1 Two operational notes:

  • Deploy scripts that SSH to the box (mine takes the tailscale IP as an argument) fail on the first run of the day until someone opens the re-auth URL. Run one manual ssh first, then let automation ride the 12-hour window.
  • Check rules win over accept rules when both match, so adding a permissive accept rule does not bypass check mode - you must scope the accept rule more precisely than the check rule.1

The SSH config gives each host one block, many addresses. The Host line is a pattern list covering every way the host is addressed; the block below it carries the user and identity file. You pick the path by what you type:

Host unraid 10.0.x.2 10.68.x.2 unraid.<tailnet>.ts.net
User root
IdentityFile ~/.ssh/id_unraid
IdentitiesOnly yes

ssh unraid resolves through local DNS (LAN) or MagicDNS; ssh 10.0.x.2 forces the primary path; ssh 10.68.x.2 forces the cross-tunnel backup. Same identity either way, no duplicate blocks to drift.

The hard rules that keep this survivable:

  • IdentitiesOnly yes on every block. Without it, ssh-agent offers every key in turn and locks accounts (Unraid root after a handful of offers) before the right key is tried.
  • MagicDNS names are patterns, never HostName. Pin HostName to the LAN address. The tailnet name stays an alias you can type, not a resolution the config depends on.
  • Tailnet names do not override local DNS. The router’s own zones win for short names; the tailnet name is always the FQDN form.

The idioms in use, all from the policy-file syntax reference:5

  • autogroup:internet as a dst is what permits exit-node use. A device with no grant to autogroup:internet cannot use any exit node, and ACLs cannot restrict which exit node - that needs grants with via.
  • autoApprovers.routes removes the manual approval click now that subnet routing is live: the tailnet policy is not in git, so every re-advertisement currently needs a console approval. Auto-approval only applies when the route is first advertised - editing the policy file does not retroactively approve a pending route; remove and re-advertise it.
  • Tags beat users for servers. A tagged node authenticates with an authkey, gets key expiry disabled by default,2 and its access is governed by the tag rather than by whoever happened to log it in.
  • tests. A deny test on a sensitive subnet fails the policy-file save if a future edit accidentally opens it.

FailureObservationRoot causeFixEvidence
Placeholder authkeynixos-rebuild switch fails; NeedsLoginVerbatim paste of the README line wrote literal placeholder text into two of three secret filesprintf %s the real key; verify byte countinstall README
Newline in key filetailscale up rejects the keyecho appends \nprintf %sinstall README
No second path during LAN incidentNIC naming trap broke LAN; tailnet unreachable tooTailscale on the box had never been authenticatedauthkey provisioning so a rebuild can never leave it un-authenticatedconfig comment
Alias dialed a dead nodessh iot-pi hung while the LAN IP workedWSL-generated /etc/hosts held a stale tailnet entry for the previous OS installPin HostName to the LAN addressssh config comment
Dead tailnet IP after re-registrationOld 100.x address goneRe-registering a node allocates a new tailnet IPNever pin configs to tailnet IPs; use MagicDNS namesssh config comment
ping shows tx 1404 rx 0 to dev boxTailnet healthy, all other peers fineWindows-side Tailscale broken, not the tailnetCheck tailscale status --json BackendState, not just pingsession, 2026-07
WSL2: ping fails, SSH worksping <alias> no replies, ssh <alias> fineWindows does not answer tailnet ICMP on the dev boxTest with the protocol you actually usesession, 2026-07
Monitoring pipeline died mid-runPostgres at 100.x:5433 unreachable; 8300-row run losttailscaled restarted on the DB host; the tailnet had silently become a production dependencyTreat tailnet IPs in connection strings as dependencies to monitor, not conveniencessession, 2026-08
Subnet routes advertised but TCP deadtailscale ping to a VLAN host answered, TCP to the same host timed outnft forward chain (policy drop) had no tailscale0 rule - the control plane resolved the route, the forwarding plane dropped the packetsOne scoped forward rule: tailnet source, local destinations onlysession, 2026-08-21
Client ignored advertised routesVLAN ranges absent from the router peer’s AllowedIPs (its own /32 only); Windows Get-NetRoute showed the routes anywayPer-client route acceptance off (Windows tray setting)Enable “Use Tailscale subnets” / tailscale up --accept-routes; verify AllowedIPs, not the OS route tablesession, 2026-08-21

After touching anything Tailscale-related on a node:

CheckCommandExpected
Daemon statetailscale status --json | jq -r .BackendStateRunning
Authkey file is realwc -c /etc/tailscale-authkeyKey length, no trailing newline; not 15-ish placeholder bytes
Autoconnect consumedsystemctl is-active tailscaled-autoconnectinactive after first registration (one-shot, done)
Router management planessh router from a tailnet deviceConnects over the tailnet IP
Tailscale SSHssh unraid from a tailnet deviceConnects, or returns a check-mode URL (re-auth, then retry)
Path selectionssh 10.0.x.2 vs ssh 10.68.x.2Same identity, both land on the same host
MagicDNSgetent hosts <host>.<tailnet>.ts.netCurrent 100.x address (compare against the admin console after any re-registration)
DERP vs directtailscale netcheck; tailscale ping <peer>Nearest relay region; direct after a few tries
Subnet routes advertisedtailscale debug prefs | grep -A8 AdvertiseRoutes (on the router)Every local VLAN /24
Route acceptance (client)tailscale status --json | jq '.Peer[] | {h: .HostName, a: .AllowedIPs}'Router peer lists the VLAN ranges, not just its own /32
Subnet forwarding planeTCP to a VLAN-only host, e.g. Test-NetConnection <ip> -Port 22Connects - tailscale ping alone proves only the control plane
  1. Tailscale, “Tailscale SSH.” https://tailscale.com/kb/1193/tailscale-ssh 2 3 4

  2. Tailscale, “Auth keys.” https://tailscale.com/kb/1085/auth-keys 2 3

  3. nixpkgs, “services.tailscale module (tailscaled-autoconnect).” https://github.com/NixOS/nixpkgs/blob/nixos-unstable/nixos/modules/services/networking/tailscale.nix

  4. Tailscale, “tailscale up command.” https://tailscale.com/docs/reference/tailscale-cli/up

  5. Tailscale, “Syntax reference for the tailnet policy file.” https://tailscale.com/docs/reference/syntax/policy-file

  6. Tailscale, “Subnet routers.” https://tailscale.com/kb/1019/subnets