# ADR 000: Tailscale

- HTML version: https://robbiepalmer.me/projects/homelab/adrs/000-tailscale
- Project: Home Lab (https://robbiepalmer.me/projects/homelab.md)
- Status: Accepted
- Date: 2026-08-02

# Context

The home lab spans several machines in different roles — an always-on hub, a
small always-on sidekick, and desktop-class hardware I expect to bring in
later — and I want to reach all of them from any of my personal devices
(laptops, phones) with no port forwarding, no public exposure, and no
per-machine VPN configuration. The machines are behind a home router doing
NAT; some of them are only ever on the tailnet.

Requirements:

* **No public exposure.** None of the lab services should be reachable from
  the internet. No ports forwarded, no public DNS, no dashboards exposed.
* **Zero-config joining.** A new node (a desktop, a laptop, a phone) should
  join the network in one command and be reachable by name, not by
  remembering IP addresses.
* **Survives heterogeneous networks.** The lab has to work from home Wi-Fi,
  mobile data, and other people's networks without reconfiguration.
* **Name-based addressing.** I want to `ssh` to any node and open the hub's
  GUIs by hostname, from anywhere.
* **Reaching machines that sleep.** I plan to add higher-powered machines
  that are only used occasionally. Those should sleep when idle rather than
  burn electricity and make noise around the clock, so the network has to
  give me a way to wake them on demand from anywhere.

# Decision

Use **Tailscale**, a WireGuard-based mesh VPN, to overlay all lab nodes and
personal devices on a single private tailnet.

Each node runs the Tailscale client and joins the tailnet; the control plane
(coordinated through Tailscale's service) handles NAT traversal and hands out
stable `100.x.y.z` addresses. MagicDNS resolves those to short device names,
which is what makes the name-based access above work — I reach nodes as
`hostname`, never as an IP. My router stays closed: no inbound ports, no
dynamic DNS, no port forwarding.

Waking a sleeping machine takes two hops, because a Wake-on-LAN magic packet
is a Layer 2 broadcast and can't route over the tailnet: the tailnet carries
the wake *request* to an always-on node on the same physical LAN, and that
node broadcasts the magic packet locally. The always-on hub is what makes
this work from anywhere.

The tailnet is the lab's trust boundary. Nodes treat traffic arriving over it
as trusted rather than filtering service by service — the mesh is what keeps
things private, so anything joining the tailnet is inside the boundary by
definition.

# Alternatives

## Self-hosted WireGuard + static config

* **Pros**: No third-party coordination service; full control.
* **Cons**: Manual key exchange and endpoint management on every node, no
  NAT traversal, painful on changing networks, and it doesn't give me
  name-based discovery. For a handful of devices it's all maintenance and no
  benefit.
* **Decision**: Rejected. The lab's goal is boring reliability, not running
  a VPN control plane.

## Other mesh providers (ZeroTier, Netbird)

* **Pros**: ZeroTier and Netbird are viable open-source meshes.
* **Cons**: Tailscale has the simplest join flow, is built on WireGuard
  (auditable, fast), and is first-class packaged on every operating system
  the lab runs.
* **Decision**: Rejected. Tailscale wins on ecosystem fit and simplicity.

## Cloudflare Tunnel / other reverse proxies

* **Pros**: Exposes specific HTTP services via the edge.
* **Cons**: Still creates public surface and requires a domain + access
  policy per service. The lab services don't need to be public at all.
* **Decision**: Rejected. A mesh VPN covers every service uniformly without
  per-service edge configuration.

# Consequences

### Pros

* **One network for everything**: every lab machine, laptop, and phone
  reachable by hostname with no port forwarding.
* **Inspectable from any device**: The entire lab is reachable from any
  machine on the tailnet, which is the point of the project.
* **Wake-on-demand stays possible**: machines that sleep to save power and
  noise can still be woken from anywhere, via a request over the tailnet
  relayed as a broadcast on the local network.
* **No public attack surface**: Every dashboard, service port, and agent
  endpoint is tailnet-only.

### Cons

* **Third-party control plane**: Node-to-node traffic is end-to-end
  encrypted, but the coordination service is a dependency. If it's
  unavailable, existing connections may still work, but new ones can't join.
* **Dependency on a free-tier account**: The personal tailnet relies on
  Tailscale's free tier for coordination.
* **Another credential**: Nodes authenticate to the tailnet, adding one more
  trust boundary to keep secure.

---

Markdown index of this site: https://robbiepalmer.me/llms.txt
