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 job - out-of-band management, a third path when LAN and tunnel routes are both unavailable, and an identity layer for SSH. Covers the NixOS authkey pattern, the Unraid plugin, the multi-path SSH alias scheme, and every failure mode observed in production. Hostnames, addresses, and paths are anonymized; the patterns are exactly as built.

TL;DR: Tailscale earns its place for OOB access and per-device identity, not for bulk site-to-site traffic (IPsec/GRE wins there). The two configurations to copy are the NixOS authKeyFile pattern (a rebuilt node rejoins unattended) and SSH Host pattern lists (one identity block, three addresses). The failures to avoid: placeholder authkeys, stale tailnet entries in a WSL-generated /etc/hosts, and an overlay node that was never authenticated in the first place.


Three 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

What 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 DERP fallback when NAT traversal fails.

DecisionPayoffCost
Tailscale alongside IPsec/GRE, not instead ofIndependent failure domain for OOBTwo overlays to reason about
Box-level tailscale on the router (subnet routing deferred)Management plane without touching production routingDevices without tailscaled stay unreachable from the tailnet for now
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 router(box-level; subnet routing planned)IoT Pi (RPi 4B, NixOS)authkey-provisionedIPsec/GRE site-to-siteUnraid serverTailscale SSHPiKVMs (OOB)dev boxWindows + WSL2

Every node is box-level today: tailnet devices reach tailnet devices, nothing more. Subnet-router advertising from the router (the site VLAN ranges) is a planned cutover step, deliberately not enabled - the router’s Tailscale role is management plane, not production routing. Its firewall treats tailscale0 as a management interface: SSH, the management UI port, and ping, nothing else.


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 minimal form - enable the service, open the management ports on tailscale0, authenticate interactively once:

# router: box-level management plane. First auth is interactive:
# `sudo tailscale up` + browser URL (one-time; node key persists
# under /var/lib/tailscale).
services.tailscale.enable = true;
# nftables: 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 authorized 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.


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 authorized 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 will matter at the subnet-router cutover: it lets advertised subnets skip the admin-console approval click. 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

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
  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