# 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 needs boring reliability. Running a VPN
  control plane would work against that goal.

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