Tailscale
Tailscale SSH from your phone: every machine, without keys
Import your tailnet into TermRover with a Tailscale API key and log in with Tailscale SSH. No SSH keys to generate, copy, or rotate.
I already use Tailscale as the network between my phone and the machines I actually work on. The missing piece was always the host list: copy a 100.x address out of the admin console, paste it into TermRover, attach a key, repeat.
You might wonder what Tailscale SSH actually is: isn’t that just normal SSH over Tailscale? Not quite. Normal SSH authenticates with a keypair: the server’s sshd checks your public key against authorized_keys before it opens a session. Tailscale SSH replaces that check with your tailnet identity. Both devices are already signed into the same tailnet, so Tailscale authenticates the connection itself. No keypair to generate, copy, or rotate. Enable it on a server once, and any device on your tailnet can log in. Remove a device from the tailnet, and its access goes with it.
TermRover now handles both halves of the setup:
- The host list builds itself. Paste a Tailscale API key into Settings, and every non-phone machine in your tailnet shows up under Hosts with its
100.xaddress and an online/offline dot. Machines running Tailscale SSH get a green SSH chip. - The login is keyless. For machines with Tailscale SSH enabled, TermRover logs in with your tailnet identity: no stored key, no password. Machines that don’t run Tailscale SSH still import, but without the chip, and use a key or password like any other host.
The Tailscale app on the phone stays the tunnel. TermRover is still just the terminal.
What you need
- Tailscale on each server, already signed in. Same as before.
- The Tailscale app on the phone, left connected. TermRover does not embed a VPN. If the app is off,
100.xaddresses are unreachable and connections time out. - A Tailscale API key from Admin Console → Settings → Keys. This only lists machines. It does not put the phone on the tailnet. You need an Owner, Admin, IT admin, or Network admin role, and the key expires in 1–90 days.
tailscale set --sshon each Linux server you want to reach without a key. This is the Tailscale SSH feature, not “SSH over Tailscale”. It claims port 22 on the tailnet IP only. Your normalsshdon the LAN is untouched. (Macs need a different Tailscale build; see Tailscale SSH on macOS.)
Windows, Synology, and QNAP cannot run Tailscale SSH. They still import into the host list, but they will not have the green SSH chip, and they use a normal key or password over the same tailnet path once you switch their auth method (see If a host still wants a key).
Tailscale SSH on macOS
On Linux, tailscale set --ssh is one command. On a Mac it depends on which Tailscale build you run. The official GUI app (the one from tailscale.com or the Mac App Store) ships its own CLI, but neither GUI variant can act as a Tailscale SSH server; only the open-source tailscaled build can. Tailscale’s macOS variants docs compare all three builds; the Can be a Tailscale SSH server row is the one that matters here.
Making a Mac a keyless SSH target is therefore an advanced-user path: quit the GUI app, install the open-source daemon, and sign in again. The GUI app still works fine as a client for reaching the tailnet; it just can’t host Tailscale SSH.
- Quit the Tailscale GUI app, and turn off its start-at-login setting while you’re there. Two Tailscale daemons on one Mac fight over the same interface.
- Install the open-source formula and sign in:
brew install --formula tailscale
sudo brew services start tailscale
sudo tailscale up
- Check the daemon:
sudo tailscale status
- Turn on the SSH server:
sudo tailscale set --ssh
The admin console then shows a green SSH badge on the machine, the same one Linux boxes get:

Two notes from doing this on my own Mac. The open-source daemon keeps its own state, so the GUI app’s login does not carry over: sign in again, and if the console ends up with a duplicate machine entry, delete the old one.
In TermRover
Open Settings → Tailscale.
- API key: paste
tskey-api-… - Tailnet: leave blank for your default (
-), or the organization name from the admin console - Default SSH user:
your_username, applied to every imported host
Tap Save. TermRover imports every non-phone device in the tailnet:
- Hosts land under the Tailscale heading with their
100.xaddress and an online/offline dot. - Machines running Tailscale SSH show a green SSH chip.
- Windows, NAS boxes, and Linux hosts without
tailscale set --sshstill import, but without the chip; switch their Authentication to an SSH key or password before they connect. - Phones are skipped, and the list refreshes every time you open Hosts.
Imported machines are ordinary host rows — [email protected]:22, a live online/offline dot, and, on any machine running tailscale set --ssh, the green SSH badge. In this list anh-1p has no badge and uses a normal key or password; archlinux and Anhs-MacBook-Pro do, so their login is keyless:

Importing never touches what you already have: your manual hosts, keys, auth choices, drag order, themes, and settings stay as they are, and removing the API key removes only the imported entries.
Imported hosts start on the Tailscale auth method (no key, no password) and the default SSH user. Both are only defaults: edit any host and the change sticks, because later syncs refresh only the 100.x address. The same editor switches a host to an SSH key or password.
Keep the Tailscale app on, tap a host, you’re in.
What Tailscale SSH actually checks
Nothing in authorized_keys. The TCP connection has to arrive over the tailnet, from a node that’s already authenticated to Tailscale: that’s your phone, via the Tailscale app. The SSH server Tailscale runs on the box then accepts the SSH none method.
That’s why the stored host address is the 100.x IP, not a LAN name and not MagicDNS. Third-party apps on iOS and Android often miss Tailscale’s resolver. The raw tailnet IP always works.
If you point TermRover at the machine’s LAN IP, OpenSSH answers instead, and it will demand a key. That’s expected: Tailscale SSH only takes over port 22 on the tailnet address.
Which key does what
“Keyless” describes the SSH layer only. Four keys exist around this setup, and exactly one of them is an SSH key:
| Key | What it is for | Where it lives |
|---|---|---|
Auth key (tskey-auth-…) |
An invitation for a machine to join your tailnet without a browser sign-in. Can carry a tag. | You create it in the admin console and paste it into tailscale up on the server |
API key (tskey-api-…) |
Read-only access to your machine list | TermRover’s settings |
| SSH key | The classic authorized_keys login |
Only needed on hosts that don’t run Tailscale SSH |
| Node key | How Tailscale proves a device is really the one that joined | Managed by the Tailscale app; you never touch it |
An auth key is not an SSH key, and TermRover never asks for one: the app cannot add a machine to your tailnet, and doesn’t try. If you have ever pasted an auth key, it was to enroll a server, which is what the tags section below is about.
Check mode
New tailnets default to "action": "check" for SSH to your own devices. The first login on each machine in each 12 hours prints a URL:
# Tailscale SSH requires an additional check.
# To authenticate, visit: https://login.tailscale.com/a/…
Open it, then the session continues. On a laptop the Tailscale app often pops the browser for you. On a phone it’s easy to miss, and some clients just hang.
TermRover surfaces the check wherever the connection is made, so it never turns into a spinner:
- Opening or reattaching a terminal shows the check dialog with Open in browser and Cancel. The SSH handshake stays parked and the session continues by itself the moment you confirm.
- Herdr Agents Fleet probes every selected machine at once, so a day or two away can expire several checks at the same time. Each expired machine is marked sign-in needed with a Sign in with Tailscale action, and a bar at the top offers Sign in all, which opens every pending URL in the browser, so you confirm them in one trip and the parked probes connect when you come back.
- Test connection in the host editor reports the check and its link; confirm, then Test again.
- Port-forward tabs reuse the terminal’s check dialog.
The check is per machine, so three expired machines means three browser confirmations; Tailscale has no batch confirmation. If the repetition gets old, set the rule to "action": "accept" so identity on the tailnet is enough:
"ssh": [
{
"action": "accept",
"src": ["autogroup:member"],
"dst": ["autogroup:self"],
"users": ["autogroup:nonroot", "root"]
}
]
You can also keep check and stretch checkPeriod up to 168h.
Tags, and making access one-way
A fresh tailnet is wide open: every device can reach every other device, on every port, in both directions. That includes the reverse of what many of us want. Any server you SSH into can also open connections back to your phone at its 100.x address, and servers are the machines most likely to get compromised.
The fix is one-way access: the phone can reach the server, the server cannot start a connection back to the phone. Rules are allow-only and directional, so there is no “deny” rule to write; allow the direction you want and leave the other one out. Replies on the session you opened still flow, so SSH feels identical.
A tag is how you name “my servers” for that rule. It is a machine identity, not a person: the server belongs to tag:server instead of your account, so one rule covers every tagged server no matter how many you add, and the server survives its owner leaving the tailnet. Tailscale’s docs are adamant that tags are for servers and routers, never for laptops or phones, which need their user identity.
Two steps to tag a machine:
- Define the tag and who may use it, in Admin Console → Access controls:
"tagOwners": { "tag:server": ["[email protected]"] }
- In Admin Console → Machines, open ⋯ → Edit tags on the server, add
tag:server, Save. Its100.xaddress does not change.
On a Linux box you can instead run sudo tailscale login --advertise-tags=tag:server. Auth keys can carry tag:server too, so a machine joins already tagged, which is nicer when you provision. The tag alone changes nothing until a rule references it, but it has two side effects: the server is owned by the tag rather than a person, and its key expiry is disabled by default.
Now the rule. It has two blocks: grants controls the network connection, and ssh is the extra Tailscale SSH check on top. This lets members reach tagged servers on port 22 and writes nothing in the other direction:
{
"grants": [
// src: who may connect, dst: to what, ip: on which ports
{ "src": ["autogroup:member"], "dst": ["tag:server"], "ip": ["tcp:22"] }
],
"ssh": [
// who (src) may use Tailscale SSH where (dst), as whom (users)
{ "action": "accept", "src": ["autogroup:member"],
"dst": ["tag:server"], "users": ["autogroup:nonroot"] }
]
}
| Field | Meaning |
|---|---|
src |
Who may connect. autogroup:member is every member of your tailnet. |
dst |
What they may reach. A tag names a group of servers. |
ip (in grants) |
Ports, here TCP 22 only. |
action (in ssh) |
accept connects straight away; check asks for the browser confirmation from the previous section. |
users (in ssh) |
Which local accounts on the server the session may become. autogroup:nonroot is any account except root. |
users is not about who may connect (that is src). Tailscale never creates accounts, so it only logs into ones that already exist. Because the destination is a tag, autogroup:nonroot is broad: anyone allowed by src can log in as any non-root account. If your servers use one login, name it ("users": ["ubuntu"]) and add "root" only if you need it.
This rule allows only TCP 22, so it blocks Mosh’s UDP range too. If you use Mosh to these boxes, add "udp:60000-61000" to the ip list.
Tagged machines add one more safeguard here: Tailscale SSH only lets a tagged device log into other tagged devices. Even if a later rule accidentally allowed server → phone, the tagged server cannot use Tailscale SSH to log into a user-owned device.
None of this changes anything in TermRover. A tagged host imports like any other and shows the same green SSH chip. One-way access is a tailnet-policy decision; the app just connects.
If a host still wants a key
That’s fine. Edit the host, switch Authentication to SSH key, pick or generate a key, Test. The tailnet path is unchanged; only the SSH proof changes. Use that for NAS boxes, Windows, or any machine where you didn’t run tailscale set --ssh.
Mosh works on top of the same SSH hop, including Tailscale SSH, as long as mosh-server is installed and UDP can travel over the tailnet.
What I didn’t build
TermRover does not join the tailnet by itself. An API key cannot do that. Embedding Tailscale (so you wouldn’t need the second app) is a real product, and a much larger one. Leaving the Tailscale app connected is the straightforward setup, and it’s the one I use.
The older TermRover + Tailscale post is still the network story: install Tailscale, don’t expose port 22, use tmux so work survives a drop. This post is the TermRover-side piece that used to be copy-paste.
Tailscale integration is available since TermRover v1.2.0.