Tailscale on macOS against a self-hosted headscale
A practical reference for getting a macOS host onto a headscale-managed
Tailscale mesh, with the macOS-specific traps that aren't called out in either
the Tailscale docs (which assume the public control server) or the headscale
docs (which assume Linux clients).
Quick reference
| Item |
Value |
| Recommended client distribution |
Homebrew Cask: brew install --cask tailscale |
| Do NOT also install |
Homebrew formula tailscale (its bundled daemon fights the cask's NetworkExtension) |
| App location |
/Applications/Tailscale.app |
| Bundled CLI binary |
/Applications/Tailscale.app/Contents/MacOS/Tailscale |
| Operator-friendly CLI shim |
/usr/local/bin/tailscale — installed via Tailscale menu bar → Preferences → "Install command line tool" |
Daemon process name (in ps) |
IPNExtension (inside the app bundle) |
| Daemon socket |
macOS-internal IPC managed by the NetworkExtension subsystem; not a Unix socket the operator interacts with directly |
| Headscale preauth key minting |
headscale preauthkeys create --user <numeric-id> --expiration 1h |
| Required permissions before daemon starts |
Network Extension allow, VPN configuration approve, system extension activation |
Symptoms and where to look
| Symptom |
Cause |
Read |
failed to connect to local tailscale service; blank menu-bar icon after a fresh install |
NetworkExtension daemon not activated or not yet granted its permissions |
installing-tailscale-on-macos.md, "Permission dance" |
Tailscale.app stuck on "Starting..."; tailscale up hangs on "joining " |
brew formula tailscale installed alongside the cask, both fighting for the IPC socket |
installing-tailscale-on-macos.md, mixed-install cleanup |
CLI runs but cannot drive the daemon through tailscale up |
join from the app instead of the CLI |
joining-headscale-from-macos.md, deep-link fallback |
headscale preauthkeys create --user <name> rejected |
recent headscale builds expect the numeric user id |
joining-headscale-from-macos.md, user-id pitfall |
Reference files
| File |
Read when |
| installing-tailscale-on-macos.md |
Installing Tailscale fresh, cleaning up a mixed cask+formula install, walking the operator through the macOS permission grants required for the NetworkExtension daemon to start. |
| joining-headscale-from-macos.md |
Connecting a working Tailscale.app to a headscale coordinator — preauth-key CLI flow, deep-link fallback when the CLI cannot reach the daemon, headscale's --user <id> vs --user <name> pitfall, and verification commands. |
Common tasks
Install Tailscale for the first time on macOS
brew install --cask tailscale (the cask, not the formula — see installing-tailscale-on-macos.md for why).
- Open Tailscale.app once:
open -a Tailscale.
- Step through the macOS permission grants (Network Extension activation, VPN configuration approval). Detailed paths in installing-tailscale-on-macos.md.
- Confirm the daemon is up:
pgrep -fl IPNExtension.
Join a headscale-controlled mesh with a preauth key
- Mint a single-use preauth key on the headscale host (numeric user ID — see joining-headscale-from-macos.md for the user-id gotcha).
sudo /Applications/Tailscale.app/Contents/MacOS/Tailscale up --login-server=https://<headscale-host> --auth-key=<key> --accept-routes --ssh --hostname=<this-host>.
- Verify:
tailscale status shows the host plus peers.
Recover from "failed to connect to local tailscale service"
This means the CLI is fine but the NetworkExtension daemon either has not
been activated or has not been granted permission. Procedure in
installing-tailscale-on-macos.md under
"Permission dance".
Recover from a mixed cask + formula install
The brew formula tailscale ships its own tailscaled that competes with
the cask's NetworkExtension for the local IPC socket. Symptom: Tailscale.app
GUI stuck on "Starting..." and tailscale up hangs indefinitely. Full
cleanup steps in installing-tailscale-on-macos.md.
Persist Tailscale across reboots
The cask installs a LaunchAgent that auto-starts Tailscale.app on login.
Nothing extra to configure. If using the formula (not recommended), see the
formula's own caveats — not covered here.
Related skills in this marketplace
Cross-skill references in this marketplace use the form
<skill-name>@<plugin-name> (e.g., glinet-slate7@aeo-infra means the
glinet-slate7 skill living in the aeo-infra plugin).
glinet-slate7@aeo-infra — if Tailscale is being layered on top of a
WireGuard underlay terminated by a GL-iNet Slate 7 (or other sdk4
firmware) router, that skill covers the underlay side: client wg0.conf
patterns, the wg-server admin API, peer rotation, and the leak-fix rules
for Linux clients running Tailscale on top of WireGuard. On macOS the
leak-fix rules do not apply (macOS routing model differs from Linux
ip rule policy routing), but the WG client config and server
provisioning are the same.
lima-vm-operations@aeo-infra — if the macOS host is acting as a Lima
hypervisor and the actual Tailscale client is a Linux VM inside Lima, the
procedures here do not apply; install Tailscale inside the VM per the
Linux path instead.
Keep this skill current
When a macOS or headscale release changes a step here (permission flow, preauth key flags, cask contents), correct the step and the symptom table in the same session. Replace the superseded text; do not append a note.
1---2name: tailscale-macos-headscale3description: |4 Use when adding a macOS host (macOS 26 Tahoe and later) to a self-hosted headscale mesh as a5 Tailscale client, or when Tailscale on macOS misbehaves: "failed to connect to local tailscale6 service", Tailscale.app stuck on "Starting...", `tailscale up` hanging on "joining", a blank7 menu-bar icon after install, or the Homebrew cask and formula fighting over the daemon.8 Covers the Homebrew Cask install, the NetworkExtension permission grants, `tailscale up9 --login-server` with a headscale preauth key, the deep-link fallback when the CLI cannot10 reach the daemon, headscale's numeric `--user` id pitfall, and reach verification. Not for11 official Tailscale onboarding, Linux or Windows clients, or headscale server install.12license: MIT13metadata:14 author: AeyeOps15 version: "0.6.8"16 summary: Onboard and repair a macOS Tailscale client against a self-hosted headscale control plane, including the Homebrew cask-versus-formula trap.17---1819# Tailscale on macOS against a self-hosted headscale2021A practical reference for getting a macOS host onto a headscale-managed22Tailscale mesh, with the macOS-specific traps that aren't called out in either23the Tailscale docs (which assume the public control server) or the headscale24docs (which assume Linux clients).2526---2728## Quick reference2930| Item | Value |31|------|-------|32| Recommended client distribution | Homebrew Cask: `brew install --cask tailscale` |33| Do NOT also install | Homebrew formula `tailscale` (its bundled daemon fights the cask's NetworkExtension) |34| App location | `/Applications/Tailscale.app` |35| Bundled CLI binary | `/Applications/Tailscale.app/Contents/MacOS/Tailscale` |36| Operator-friendly CLI shim | `/usr/local/bin/tailscale` — installed via Tailscale menu bar → Preferences → "Install command line tool" |37| Daemon process name (in `ps`) | `IPNExtension` (inside the app bundle) |38| Daemon socket | macOS-internal IPC managed by the NetworkExtension subsystem; not a Unix socket the operator interacts with directly |39| Headscale preauth key minting | `headscale preauthkeys create --user <numeric-id> --expiration 1h` |40| Required permissions before daemon starts | Network Extension allow, VPN configuration approve, system extension activation |4142### Symptoms and where to look4344| Symptom | Cause | Read |45|---|---|---|46| `failed to connect to local tailscale service`; blank menu-bar icon after a fresh install | NetworkExtension daemon not activated or not yet granted its permissions | [installing-tailscale-on-macos.md](installing-tailscale-on-macos.md), "Permission dance" |47| Tailscale.app stuck on "Starting..."; `tailscale up` hangs on "joining <coordinator>" | brew formula `tailscale` installed alongside the cask, both fighting for the IPC socket | [installing-tailscale-on-macos.md](installing-tailscale-on-macos.md), mixed-install cleanup |48| CLI runs but cannot drive the daemon through `tailscale up` | join from the app instead of the CLI | [joining-headscale-from-macos.md](joining-headscale-from-macos.md), deep-link fallback |49| `headscale preauthkeys create --user <name>` rejected | recent headscale builds expect the numeric user id | [joining-headscale-from-macos.md](joining-headscale-from-macos.md), user-id pitfall |5051---5253## Reference files5455| File | Read when |56|------|-----------|57| [installing-tailscale-on-macos.md](installing-tailscale-on-macos.md) | Installing Tailscale fresh, cleaning up a mixed cask+formula install, walking the operator through the macOS permission grants required for the NetworkExtension daemon to start. |58| [joining-headscale-from-macos.md](joining-headscale-from-macos.md) | Connecting a working Tailscale.app to a headscale coordinator — preauth-key CLI flow, deep-link fallback when the CLI cannot reach the daemon, headscale's `--user <id>` vs `--user <name>` pitfall, and verification commands. |5960---6162## Common tasks6364### Install Tailscale for the first time on macOS65661. `brew install --cask tailscale` (the cask, not the formula — see [installing-tailscale-on-macos.md](installing-tailscale-on-macos.md) for why).672. Open Tailscale.app once: `open -a Tailscale`.683. Step through the macOS permission grants (Network Extension activation, VPN configuration approval). Detailed paths in [installing-tailscale-on-macos.md](installing-tailscale-on-macos.md).694. Confirm the daemon is up: `pgrep -fl IPNExtension`.7071### Join a headscale-controlled mesh with a preauth key72731. Mint a single-use preauth key on the headscale host (numeric user ID — see [joining-headscale-from-macos.md](joining-headscale-from-macos.md) for the user-id gotcha).742. `sudo /Applications/Tailscale.app/Contents/MacOS/Tailscale up --login-server=https://<headscale-host> --auth-key=<key> --accept-routes --ssh --hostname=<this-host>`.753. Verify: `tailscale status` shows the host plus peers.7677### Recover from "failed to connect to local tailscale service"7879This means the CLI is fine but the NetworkExtension daemon either has not80been activated or has not been granted permission. Procedure in81[installing-tailscale-on-macos.md](installing-tailscale-on-macos.md) under82"Permission dance".8384### Recover from a mixed cask + formula install8586The brew formula `tailscale` ships its own `tailscaled` that competes with87the cask's NetworkExtension for the local IPC socket. Symptom: Tailscale.app88GUI stuck on "Starting..." and `tailscale up` hangs indefinitely. Full89cleanup steps in [installing-tailscale-on-macos.md](installing-tailscale-on-macos.md).9091### Persist Tailscale across reboots9293The cask installs a LaunchAgent that auto-starts Tailscale.app on login.94Nothing extra to configure. If using the formula (not recommended), see the95formula's own caveats — not covered here.9697---9899## Related skills in this marketplace100101Cross-skill references in this marketplace use the form102`<skill-name>@<plugin-name>` (e.g., `glinet-slate7@aeo-infra` means the103`glinet-slate7` skill living in the `aeo-infra` plugin).104105- `glinet-slate7@aeo-infra` — if Tailscale is being layered on top of a106 WireGuard underlay terminated by a GL-iNet Slate 7 (or other sdk4107 firmware) router, that skill covers the underlay side: client `wg0.conf`108 patterns, the wg-server admin API, peer rotation, and the leak-fix rules109 for Linux clients running Tailscale on top of WireGuard. On macOS the110 leak-fix rules do not apply (macOS routing model differs from Linux111 `ip rule` policy routing), but the WG client config and server112 provisioning are the same.113- `lima-vm-operations@aeo-infra` — if the macOS host is acting as a Lima114 hypervisor and the actual Tailscale client is a Linux VM inside Lima, the115 procedures here do not apply; install Tailscale inside the VM per the116 Linux path instead.117118## Keep this skill current119120When a macOS or headscale release changes a step here (permission flow, preauth key flags, cask contents), correct the step and the symptom table in the same session. Replace the superseded text; do not append a note.