How to setup Tailscale SSH on macOS
How to setup Tailscale SSH on macOS
This tutorial walks through configuring Tailscale SSH on a new Mac so you can ssh user@hostname to it from anywhere on your tailnet without managing SSH keys or opening firewall ports.
Background and gotchas: seeRE - Remote Machine Setup for AI Agents.
Why the brew formula matters
Tailscale on macOS comes in three flavours and only one supports the SSH server:
| Install | Tailscale SSH | Why |
|---|---|---|
| App Store app | ❌ | fully sandboxed, can’t spawn login shells |
brew install --cask tailscale-app (or pkg from tailscale.com) | ❌ | runs as a NetworkExtension, partial sandbox |
brew install tailscale (formula) | âś… | unsandboxed CLI daemon |
The first two are GUI apps with menu bar icons. The formula is a daemon with no GUI: you control everything from the terminal. To get Tailscale SSH you must use the formula.
1. Install the brew formula
If you already have a GUI Tailscale install, remove it first:
brew uninstall --cask tailscale-app # prompts for sudo password
Then install and start the daemon:
brew install tailscale
sudo brew services start tailscale
If a previous Tailscale install left a system extension behind, systemextensionsctl list | grep tailscale will show a terminated waiting to uninstall on reboot line. Reboot before continuing — the lingering extension fights the new daemon and breaks peer-to-peer.
2. Log in to your tailnet
tailscale up --ssh --accept-routes
This prints a https://login.tailscale.com/a/<token> URL and blocks. Open the URL in a browser, sign in, and the command returns. Run tailscale status to confirm the node appears.
Flags:
--sshenables the Tailscale SSH server on this node.--accept-routeslets this node use subnet routes advertised by other nodes (harmless if there are none).
3. Fix hostname collisions
If the tailnet already had a node with this machine’s hostname (e.g. from a previous install), Tailscale registers the new one as hostname-1. To fix:
- Open https://login.tailscale.com/admin/machines and remove the old offline node.
- Rename the new node:
tailscale set --hostname=<desired-name>
tailscale status should now show the clean name.
4. Connect over SSH
From any other machine on your tailnet:
ssh <user>@<hostname>
MagicDNS resolves the bare hostname to the tailnet IP. No SSH keys needed: Tailscale authenticates the connection using your tailnet identity.
If your tailnet ACL is set to "action": "check" (the default for SSH), the first connection prints a one-time auth URL:
# To authenticate, visit: https://login.tailscale.com/a/<id>
Open it once and the auth is cached for ~24 hours. To skip this prompt entirely, change the ACL to "action": "accept" in the admin console.
Troubleshooting peer-to-peer
If tailscale ping <host> shows via DERP(<region>) instead of via <ip>:<port>, the connection is going through a relay and SSH will likely time out. Tailscale needs UDP to establish a direct WireGuard tunnel.
Check, in order:
- VPN on either side: any third-party VPN intercepts UDP and breaks NAT traversal. Run
tailscale netcheck | grep IPv4— if the reported public IP isn’t your real WAN IP, a VPN is in the path. Disable it. - macOS firewall:
/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate. If enabled, allowtailscaledor disable the firewall. - Little Snitch / other L7 firewalls: switch to Silent Mode → Allow Connections to test. If that fixes it, add a permanent allow rule for
tailscaled(UDP, any direction). - Sleeping target: a sleeping Mac responds to Tailscale’s protocol pings via DERP but drops real TCP. Wake it. Long-term, enable “Wake for network access” in Energy Settings.
- Stale endpoints:
tailscale status --self --json | jq '.Self.Addrs'. If empty, restart the daemon:sudo brew services restart tailscale.
Diagnostic commands
tailscale netcheck # UDP/IPv4/IPv6 + DERP latency
tailscale ping -c 5 <peer> # direct vs DERP, endpoint
tailscale status --self --json | jq '.Self.Addrs' # advertised endpoints
systemextensionsctl list | grep tailscale # stale extensions
sudo brew services restart tailscale # daemon restart
Reverting to the GUI app
sudo brew services stop tailscale
brew uninstall tailscale
brew install --cask tailscale-app
open /Applications/Tailscale.app
This restores the menu bar UI but loses the Tailscale SSH server.