juri.dev / notes
← all notes

How to setup Tailscale SSH on macOS

🌱 seedling

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:

InstallTailscale SSHWhy
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:

  • --ssh enables the Tailscale SSH server on this node.
  • --accept-routes lets 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:

  1. Open https://login.tailscale.com/admin/machines and remove the old offline node.
  2. 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:

  1. 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.
  2. macOS firewall: /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate. If enabled, allow tailscaled or disable the firewall.
  3. 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).
  4. 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.
  5. 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.