Chapter 06 · Build iOS

The iPhone client

The phone is the only device here that is purely a client — nothing listens on it, nothing is served from it. That makes its security story unusually simple, and concentrates it almost entirely on one question: where does the private key live?

Everything in this chapter exists to make one gesture work: you tap a saved host and a live pane appears. Five things have to happen in order before that keystroke lands, and each one has a phone-shaped way of going wrong.

How the phone is set up
WHAT HAS TO HAPPEN, IN ORDER WHAT THE PHONE IS DOING 1 · Bring the tunnel up the Tailscale Network Extension 2 · Resolve the name MagicDNS, inside the tunnel 3 · Sign the handshake where the private key actually is 4 · Start the session mosh over UDP, or ssh over TCP 5 · Twenty minutes in a pocket iOS suspends whatever is running IF THIS PHONE IS LOST TONIGHT WHAT LIVES ON THIS PHONE Tailscale · a WireGuard node key, in the keychain Blink · saved hosts, ports, users, and key names the SSH key · enclave-bound, or a file you can copy known_hosts · the fingerprints you accepted on day one nothing listening · no port is open on this device the phone is a pure client — nothing listens on it, and that is most of why its security story is short up before you type one word: macbook Face ID, in-chip mosh · UDP 60001 still attached up before you type one word: macbook a file, in memory mosh · UDP 60001 still attached same tunnel, fine you type it in full a file, in the VM ssh · TCP 22 connection reset nothing to steal · revoke one line and move on an exportable key · rotate it on all three hosts the same exportable key, one emulator further in a saved host, and a key you cannot export Settings → Hosts holds the name, port, user and key the key is a handle to the enclave, not a file the command is mosh macbook -- tmux new -A -s phone a saved host, and a key file in the app Blink generated the pair and stored the private half it is a real file: exportable, and backup-able everything else about this connection is identical an x86 emulator running Alpine, and ssh no saved hosts, no key manager, no mosh client the key is a file inside an emulated filesystem the whole thing is one app iOS may kill at any time the Network Extension is already up connect-on-demand raises it before Blink asks only tailnet addresses go through it, not all traffic the VPN badge is not an exit node unless you set one the same tunnel, and it does not care Tailscale is a system extension, not a Blink feature every app on the phone reaches the tailnet through it this rung is the one thing iSH gets for free macbook resolves to 100.x, on the phone MagicDNS answers inside the tunnel, not on café DNS the name survives a device being removed and re-added the 100.x address does not — never save that instead you type the whole thing, every time iSH has no host manager, so there is nothing saved MagicDNS still works — the tunnel is the same tunnel this rung only costs you typing, on a phone keyboard the enclave signs · the key never moves Blink hands the challenge to the Secure Enclave Face ID or the passcode gates the signing operation the private key has no exportable form, by design the file is read into the app's memory a real ed25519 private key, sitting on the filesystem anything that can read the app's data can copy it including an unencrypted local backup of the phone a key file inside an emulated Linux the same exportable file, one layer further away no Face ID gate, no enclave, no hardware anything and no passphrase prompt at all unless you set one ssh authenticates, then hands over to mosh mosh-server starts, prints a one-time key, ssh exits the session moves to UDP and stops caring about IPs mosh --install-static put that binary there, no root plain ssh, on one TCP connection the session is bound to this IP and this socket Wi-Fi to LTE ends it, ServerAliveInterval or not there is no mosh client here to upgrade to you unlock, and the pane is where you left it iOS suspended Blink; mosh reconnects from the new IP tmux new -A -s phone attached rather than started Low Power Mode makes that suspend far more aggressive Connection reset by peer, and the work is gone iSH is an emulator, killed early under memory pressure the reset in the server log proves the fault was here and with no tmux, the remote job died with the shell
  1. 0 · Before you tap anythingOne saved host holds the MagicDNS name, the port, your username and the key — and the key here is a handle to something in the Secure Enclave rather than a file. The whole interface is the command on that host: mosh macbook -- tmux new -A -s phone.
  2. 0 · Before you tap anythingIdentical setup, one difference: Blink generated an ed25519 pair in software and kept the private half as a file in its own storage. Every rung below behaves the same — it is only the consequences of losing the phone that change, and they change a lot.
  3. 0 · Before you tap anythingiSH emulates x86 and runs Alpine Linux, so you get a real ssh, a real ~/.ssh, and none of the iOS-specific machinery. No saved hosts, no key manager, no Secure Enclave, and — the part that matters — no Mosh client.
  4. 1 · The tunnel comes up firstTailscale on iOS runs as a Network Extension, which is why the phone shows a VPN badge. Leave connect-on-demand enabled so the tunnel is already up before you open a terminal. Only tailnet addresses route through it — it is not carrying your ordinary traffic unless you have picked an exit node.
  5. 1 · The tunnel comes up firstThis rung is free for every client, because Tailscale is a system extension rather than something a terminal app provides. iSH reaches the tailnet exactly as Blink does. It is the only rung on this chain where the two are genuinely equal.
  6. 2 · The name resolves inside the tunnelMagicDNS answers macbook from within the tailnet, not from whatever DNS the café hands you. Save the MagicDNS name in the host, never the 100.x address — the name survives a device being removed and re-added, and the address does not.
  7. 2 · You type the whole thing, every timeMagicDNS still works, but with no host manager there is nothing to save it in, so it is ssh you@macbook.your-tailnet.ts.net -p 2222 on a phone keyboard at every connection. Small on its own; it is the reason people stop using the phone.
  8. 3 · The enclave signs, and the key stays putBlink passes the authentication challenge to the Secure Enclave, Face ID or your passcode gates the operation, and a signature comes back. The private key was generated inside a separate coprocessor and has no exportable form — not for Blink, not for iOS, not for malware, not for you. This is the single choice the chapter is built around.
  9. 3 · The file is read into memory to signA software key is an ordinary ed25519 private key on a filesystem. Anything that can read the app's data can copy it, and an unencrypted local backup carries it straight off the device. It works perfectly well — it just means the key exists in a form that can leave.
  10. 3 · The same file, one layer further inAn emulated Linux does not add protection; it adds distance. There is no enclave to ask, no Face ID gate on signing, and no passphrase at all unless you chose one when you generated the key. Custody here is no better than the Blink key-file case.
  11. 4 · SSH starts it, then gets out of the waySSH authenticates, launches mosh-server, collects a one-time key, and exits. From then on the session is UDP and identified by that key rather than by your IP address — which is what lets it survive Wi-Fi to LTE. mosh --install-static is what put the server binary there without needing root.
  12. 4 · One TCP connection, and that is the sessionPlain SSH ties the session to this IP and this socket. ServerAliveInterval helps the client notice a dead link promptly, but nothing client-side can stop the link dying — and iSH has no Mosh client to upgrade to. This is where the chain stops being resilient.
  13. 5 · Twenty minutes later, it is still thereiOS suspended the app the moment the screen locked, and the phone probably changed network on the way home. Mosh reconnects from whatever IP it now has, and tmux new -A -s phone attaches rather than starting anything. Watch for Low Power Mode: it makes iOS far more aggressive about suspending, which is the usual answer when sessions only die on a low battery.
  14. 5 · Twenty minutes later, it is goneiSH is an emulator, so iOS kills it early under memory pressure. That produces a TCP reset, which the server logs as Connection reset by peer — useful, because it proves the fault was on the phone rather than on the network or the server. Without tmux, whatever was running died with the shell.
Only one rung on this chain depends on which key you chose — but it is the one you cannot undo later. Switch between Blink + Secure Enclave key and iSH + plain ssh and watch the last two rungs collapse: Mosh and tmux are what make the phone survive being a phone, and the enclave is what makes losing it a one-line revocation instead of a key rotation across three machines.

Harden the device first

A phone that can open a root shell on three machines deserves the same care as those machines.

  • Six-digit passcode minimum, ideally alphanumeric. Face ID is convenience on top of the passcode, not a replacement — the passcode is what actually derives the encryption keys.
  • Auto-Lock at 30 seconds or 1 minute. The window where a snatched unlocked phone is useful should be tiny.
  • Erase after 10 failed attempts if you have backups worth trusting.
  • Encrypted backups. An unencrypted local backup can carry file-based keys off the device. Secure Enclave keys never leave, which is one more argument for them.
  • Lockdown Mode if you are plausibly a target. It breaks some web features; it does not affect SSH, Tailscale or Blink.

Tailscale on iOS

Install Tailscale from the App Store and sign in with the same account as your other machines. A few iOS-specific notes that save confusion later:

  • Tailscale runs as a Network Extension. iOS shows a VPN badge — it is not routing all your traffic unless you have set an exit node; by default only tailnet addresses go through it.
  • Leave on-demand / connect on demand enabled so the tunnel is up before you open a terminal.
  • Low Power Mode makes disconnections much more aggressive. If sessions start dying only when your battery is low, that is why.
  • If you enabled device approval on the tailnet, approve the phone in the admin console before it can reach anything.
# From the phone, once Tailscale is up — Blink can ping
ping macbook
# then check the path from the Mac side
tailscale ping iphone

Blink Shell

1. Create a Secure Enclave key

Settings → Keys → +, and choose the Secure Enclave key type rather than a generated ed25519 file. Name it for the device, e.g. iphone-se.

Why this specific choice matters

A Secure Enclave key is generated inside a separate coprocessor and cannot be exported — not by Blink, not by iOS, not by malware, not by you. Signing happens inside the enclave, gated by Face ID or your passcode. There is no file to steal, nothing in a backup, and nothing to rotate on your other machines if the phone is lost. You revoke one line and move on.

2. Install the public key on each machine

Copy the public key from Blink, then add it on each host. Keep the comment meaningful — this is the label you will use when revoking:

# On each machine (Mac, VPS, Ubuntu laptop)
echo 'ssh-ed25519 AAAAC3Nza... iphone-blink-se' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
chmod 700 ~/.ssh

Optionally constrain it to the tailnet, so the key is useless from anywhere else even if the server is later exposed:

from="100.64.0.0/10" ssh-ed25519 AAAAC3Nza... iphone-blink-se

3. Verify plain SSH, and check the host key

ssh yourname@macbook

The first connection asks you to trust a host key. Compare the fingerprint against what the server reports before accepting — this is the one moment where a man-in-the-middle would be invisible:

# Run this ON the server, compare to what Blink shows
for f in /etc/ssh/ssh_host_*_key.pub; do ssh-keygen -lf "$f"; done

4. Upgrade to Mosh

mosh --install-static yourname@macbook

Blink deploys mosh-server without needing root. After that, connect with mosh rather than ssh.

5. Save a host so you type one word

Settings → Hosts → +:

FieldValue
Hostmacbook
HostNamemacbook.your-tailnet.ts.net
Portwhatever you set in chapter 05
Useryour account name
Keyiphone-se
Mosh commandtmux new -A -s phone

Use the MagicDNS name, not the 100.x address — names survive a device being removed and re-added.

6. Client-side resilience

Blink → Settings → SSH Config
Host *
  ServerAliveInterval 30
  ServerAliveCountMax 6
  TCPKeepAlive yes

These help plain SSH notice a dead link promptly. They do not prevent drops — nothing client-side can, once iOS decides to suspend the app. That is what Mosh and tmux are for.

Making a phone genuinely usable

Two settings do most of the work. In Blink, map Caps Lock to Ctrl — on a hardware keyboard this single change makes tmux and Emacs bindings reachable. Then turn on tmux's mouse mode (chapter 04), which makes panes tappable and scrollback draggable with a thumb.

The daily command
mosh macbook -- tmux new -A -s phone

Set as the host's command, this is the entire interface. Connecting is attaching; a dropped connection followed by a reconnect puts you back exactly where you were.

If sessions still drop

Work through it in this order — it is ordered by how often each is the actual cause:

  1. Are you using Mosh, or did you fall back to SSH? The command is mosh, not ssh. This is the answer most of the time.
  2. Low Power Mode? It makes iOS far more aggressive about suspending apps.
  3. Which client? iSH is an x86 emulator and is killed readily under memory pressure. That produces a TCP reset, which the server logs as Connection reset by peer — proof the fault is client-side.
  4. Is tmux running? If it is, a drop costs you nothing and the question becomes cosmetic.

Checklist

  • Passcode, short Auto-Lock, and encrypted backups configured.
  • Tailscale installed, signed in, approved in the admin console, connect-on-demand enabled.
  • Blink installed with a key created in the Secure Enclave.
  • Public key added to every machine, with an identifying comment.
  • Host key fingerprints verified on first connection.
  • mosh --install-static run against each machine.
  • A saved host per machine, with tmux new -A -s phone as the command.
  • Caps Lock remapped to Ctrl.
  • Wi-Fi → LTE mid-session tested; the session survived.

The commands in this guide change firewall and login settings, and can lock you out of a machine. Practise on something disposable first. Everything here is provided as is, with no warranty — you accept the risk of running it. Read the disclaimer.