Join Tailscale
Install and join
On the Spark:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale upIt prints a login URL: open it in a browser on the Mac and log in. Then, in the admin console: Machines → the Spark → Disable key expiry. A headless server whose key expires silently drops off the tailnet.
Turn on MagicDNS and HTTPS
In the admin console, turn on MagicDNS and HTTPS certificates. Once issued, the certificate’s name appears in public certificate-transparency logs — the name only, nothing else.
ACL grants are the firewall
ufw can’t see tailscale0, so the ACL policy is the only firewall tailnet traffic gets. The policy lives in the admin console (https://login.tailscale.com/admin/acls). Keep the real policy in the vault, never in this repo: it names your home subnet.
Start from what’s there. A new tailnet has one allow-all grant ("src": ["*"], "dst": ["*"], "ip": ["*"]). While it exists, every device reaches every port on the Spark, and nothing below restricts anything. Replace it with the grants below, which keep today’s access working.
Create the tag. In the JSON editor, add a top-level
tagOwnersblock besidegrants(or, in the visual editor: Tags → Create tag →spark, ownerautogroup:admin):"tagOwners": { "tag:spark": ["autogroup:admin"] },Set three grants, turning the allow-all grant into the first one and adding the other two:
"grants": [ // Your own devices reach each other, as before. {"src": ["autogroup:member"], "dst": ["autogroup:member"], "ip": ["*"]}, // The home LAN through the subnet router, as before. {"src": ["autogroup:member"], "dst": ["<home-subnet>/24"], "ip": ["*"]}, // The Spark: SSH, and 443 for `tailscale serve` (Phase 1). {"src": ["autogroup:member"], "dst": ["tag:spark"], "ip": ["22", "443"]} ]If you use an exit node, add
{"src": ["autogroup:member"], "dst": ["autogroup:internet"], "ip": ["*"]}.autogroup:memberdoesn’t include people you’ve shared devices with; give them their own grant if they need one. LiteLLM’s port joins the Spark’s grant in Phase 3.Save. The editor refuses a policy with a syntax error.
Tag the Spark, on the Spark:
sudo tailscale up --advertise-tags=tag:sparkRepeat any other flags you gave
tailscale upwhen joining, becauseupresets the ones you leave out. A tagged device is owned by the tag, not by you, and its key doesn’t expire, which also covers the key-expiry step above.Test from the Mac and the phone: SSH to the Spark still works, and so do your usual home-LAN services, such as the NAS, from away. If something breaks, add a grant for it.
Confirm the route home
In the admin console, confirm how the tailnet reaches the home LAN — the subnet router and its advertised route — and record it in the vault. Never paste tailscale status output anywhere public.
Today there is none (2026-09-24), by choice. Every device Dan uses runs Tailscale, so they reach each other without one. The only gap is devices that can’t run Tailscale, such as the home router’s admin page, which is reachable only from home. The Spark doesn’t need a route either way: it sits on the LAN.
When a device like that is needed from away, add a subnet route for that device’s address only, not the whole LAN, so nothing else on the home network becomes reachable through the tailnet:
On that home Linux machine (always-on, running Tailscale — not the Spark, which stays single-purpose), allow forwarding and advertise the one address:
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-tailscale.conf sudo sysctl -p /etc/sysctl.d/99-tailscale.conf sudo tailscale set --advertise-routes=<device-address>/32In the admin console: Machines → that machine → Edit route settings → approve the route.
In the policy, point the home-LAN grant at the same address, and at its ports if you want to be tighter:
{"src": ["autogroup:member"], "dst": ["<device-address>/32"], "ip": ["*"]}.Test from the phone, off Wi-Fi. On the Mac, the Tailscale app’s Use Tailscale subnets must be on.
Record the machine and the route in the vault.
Rebuilding the box
Delete the old device in the admin console before rejoining. Otherwise the box comes back as brightroar-1, and every client that points at brightroar breaks.