Get Moshi
Moshi with Tailscale
network guide

Moshi with Tailscale

Reach a Mac or Linux box with no public IP from anywhere — put both devices on a tailnet, Easy Pair over it with a rotation-proof MagicDNS address, and understand exactly when Tailscale SSH helps and when it silently breaks key auth.

updated 2 days ago12 min readpage 8 / 9

The machine you want your agents on is usually the one with no public IP: the Mac mini in the closet, the desktop at home, the homelab box behind your router's NAT. Tailscale fixes that without opening a single port — every device you sign in gets a stable private address on your tailnet, reachable from anywhere, end-to-end encrypted over WireGuard.

Moshi needs no integration for this, and that's the point. Tailscale runs at the OS layer on iOS; Moshi just sees a host it can reach. SSH, mosh, key auth, password auth — everything behaves exactly as it would on your living-room Wi-Fi, except now "your living room" follows you onto the train.

model

Mental model. A tailnet is your LAN, everywhere. Moshi treats tailnet hosts as ordinary SSH or mosh targets — there is no Tailscale toggle anywhere in Moshi, and the tunnel is brought up or down in the iOS Tailscale app, not here.

What you'll learn

  • Why a tailnet beats port forwarding and a rented VPS for agent hosts
  • Put the host and your phone on the same tailnet, and pick the right address form
  • Easy Pair over Tailscale — and why the QR bakes in your MagicDNS name
  • Tailscale SSH vs SSH over Tailscale: the fork that causes the infamous 60-second hang
  • Run mosh through the tunnel, and what happens when a Mac host sleeps
  • Debug the handful of failures that look like Moshi problems but aren't

Why a tailnet

There are three common ways to reach a home machine from a phone. Two of them have sharp edges.

Approach
Setup
Exposure
Catch
Tailscale
Install + sign in, twice
Nothing listens on the internet
One more app on the phone
Port forwarding
Router config, dynamic DNS
Port 22 open to the world
Bots hammer it day one
Rented VPS
Provision, harden, pay monthly
Public by design
Your repos live off-site

For a personal agent host, the tailnet wins on every axis that matters: no inbound firewall holes, no dynamic-DNS dance when your ISP rotates your IP, and the same address works from home Wi-Fi, cellular, and hotel networks alike.

Part 1 — Put both ends on the tailnet

Install Tailscale on the host and sign in, then do the same on your phone with the same account:

host
# macOS
$brew install tailscale
$sudo tailscale up
# what to put in Moshi's Host field
$tailscale ip -4
$tailscale status
Phone and host on the same tailnet — a stable private address from anywhere, nothing exposed to the internet.
Reach hosts with no public IPPhone and host on the same tailnet — a stable private address from anywhere, nothing exposed to the internet.

Three address forms work in Moshi's Host field, in rough order of preference:

  1. MagicDNS namemac-mini or the full mac-mini.tail-scales.ts.net. Names survive IP rotation, so this is the most durable choice.
  2. Tailscale IPv4 — the 100.x.y.z address from tailscale ip -4.
  3. Tailscale IPv6 — the fd7a:115c:… address, if that's how you roll.
warn

Do not use the host's LAN IP (192.168.x.x), its public IP, or the Tailscale web SSH proxy URL. Moshi connects directly through the OS-level tunnel — anything that isn't a tailnet address bypasses it.

Part 2 — Easy Pair over Tailscale

You could now add a connection by hand, but Easy Pair does it better — and it's Tailscale-aware. On the host:

host
$moshi-hook host setup
found tailscale: mac-mini.tail-scales.ts.net (100.84.2.17)

Before printing the QR, moshi-hook host setup probes the local tailscale CLI. If the daemon is running, it bakes your tailnet address into the QR — preferring the MagicDNS name over the raw IP, because names survive address rotation. No Tailscale? It falls back to a LAN address or the Mac's Bonjour .local name. You don't have to know which address form to pick; the host already did.

Scanning the QR from Moshi's onboarding then does the rest:

  • Checks the host's prerequisites — Remote Login on, tmux and mosh-server reachable over a non-interactive shell
  • Generates an Ed25519 keypair on the phone — the private key never leaves the device
  • Sends only the public half to the host's setup session, which installs it in ~/.ssh/authorized_keys
  • Saves the connection with the tailnet address, key auth, and transport on Auto
keys

Treat the Easy Pair QR like a temporary access token. Anyone who scans it before it expires can claim SSH access to that host.

One more thing the setup command does for you: if it detects Tailscale SSH enabled on the host, it refuses to pair and tells you why. That deserves its own section, because it's the single most confusing failure mode in this whole setup.

Part 3 — Tailscale SSH is not SSH over Tailscale

Two features sound alike and work in completely different ways. Mixing them up produces a connection that hangs for about a minute and then fails with a misleading auth error — with a key you know is authorized.

  • SSH over Tailscale — everything this guide has described so far. Moshi connects to the host's normal OpenSSH server (sshd) through the tunnel. Keys, passwords, SSH, and mosh all behave exactly as on a LAN. This is the path Easy Pair sets up.
  • Tailscale SSH — a separate, Tailscale-managed SSH server inside tailscaled, enabled with tailscale set --ssh=true. When it's on, Tailscale grabs port 22 for every tailnet connection and authenticates with your Tailscale identity — bypassing the OS sshd and authorized_keys entirely.
Behavior
SSH over Tailscale
Tailscale SSH
Who answers port 22
The OS sshd
tailscaled
Auth
Your SSH key / password
Tailnet identity + ACLs
authorized_keys
Honored
Ignored entirely
Easy Pair
mosh bootstrap
Moshi auth fields
Key or password
Leave both empty

The failure looks like this: Easy Pair (or you) installed a key in authorized_keys, but with Tailscale SSH on, traffic to port 22 never reaches the OS sshd — so the key is never even checked. The connection stalls for ~60 seconds, then reports a generic auth error. The tell-tale sign: the host is reachable on every port except the one that matters — a dev server on 8080 loads instantly over the same tailnet address while port 22 hangs.

Pick one path

Key auth (recommended). Hand port 22 back to the OS sshd:

host
$sudo tailscale set --ssh=false
$moshi-hook host setup

Key auth then works for Moshi and every other SSH client, mosh can bootstrap, and Easy Pair runs without complaint. This is the simpler, more reliable choice for an agent host — it's why moshi-hook host setup checks tailscale debug prefs for RunSSH: true and stops you up front instead of letting you hit the 60-second hang later.

Tailscale SSH instead. If you deliberately run Tailscale SSH — say, your tailnet ACLs are how you govern access — Moshi can use it: leave the password and key fields empty in the connection. Tailscale authenticates through its identity layer, so Moshi has nothing to send (that's what the in-app hint about leaving the password blank refers to). Two hard limits:

  • It only works with an accept-mode ACL. A check-mode rule ("action": "check") demands a browser approval mid-handshake that a non-interactive client can't complete.
  • mosh cannot bootstrap through it — Tailscale SSH isn't real OpenSSH, so you're limited to plain SSH transport.

Part 4 — mosh through the tunnel

mosh works fine over Tailscale: the SSH bootstrap and the UDP session both ride the tunnel. Leave the connection type on Auto and Moshi prefers mosh with SSH fallback — which is what makes a tailnet host shrug off you walking out the door mid-session.

Connections that don't dropmosh over the tailnet — network changes, sleep, and signal loss don't end the session.

If mosh fails where SSH works, it's almost never Tailscale — the usual culprit is mosh-server missing from the non-interactive PATH. The troubleshooting doc has the checklist.

link

Agent events don't ride the tailnet at all. moshi-hook keeps its own outbound WebSocket to Moshi, so approvals and turn completions reach your phone even when the Tailscale app on iOS is off — you only need the tunnel up to open the terminal itself.

Part 5 — Tailnet hosts that sleep

A closed-lid Mac on the tailnet has one quirk worth knowing. mosh runs over UDP, and UDP traffic does not prevent macOS from drifting into low-power sleep — even with pmset tweaks — so an idle session can go "Unreachable (auto-retrying)" after a few minutes. Reconnecting is fast: Moshi sends a wake-up probe when starting a new mosh connection, which pulls the Mac back up.

If you want sessions to stay live without manual reconnects, block sleep at the source — lid open, caffeinate, or the full always-on treatment in Moshi with an always-on Mac.

Separately, Tailscale occasionally wants device re-authentication (key expiry, ACL changes). That prompt appears in the Tailscale app, not in Moshi — so if a host that worked yesterday is suddenly unreachable, check Tailscale first.

Troubleshooting

tailscale ping works but Moshi can't connect

Tailscale connectivity alone isn't enough — the host still needs SSH listening. On macOS, turn on System Settings → General → Sharing → Remote Login. Then confirm the tunnel from another tool (a desktop terminal, tailscale ping) before debugging Moshi settings.

Hangs ~60 seconds, then an auth error, with a key that's definitely authorized

That's Tailscale SSH hijacking port 22 — the key never reaches the OS sshd. Either sudo tailscale set --ssh=false (recommended) or clear the password/key fields and rely on tailnet identity. See Part 3.

"Unable to authorize"

The auth type doesn't match what the host expects — password vs key — even when Tailscale itself is fine. Match the Authentication setting in the connection to what the host's sshd actually accepts.

OS Error 4

The SSH handshake was interrupted. Re-check Remote Login, confirm you're using the host's tailnet address (not a stale LAN entry), and that the device is still signed in to the tailnet.

Where to go next