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.
- 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. - 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.
- 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. - 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.
- 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.
- 2 · The name resolves inside the tunnelMagicDNS answers
macbookfrom within the tailnet, not from whatever DNS the café hands you. Save the MagicDNS name in the host, never the100.xaddress — the name survives a device being removed and re-added, and the address does not. - 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 2222on a phone keyboard at every connection. Small on its own; it is the reason people stop using the phone. - 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.
- 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.
- 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.
- 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-staticis what put the server binary there without needing root. - 4 · One TCP connection, and that is the sessionPlain SSH ties the session to this IP and this socket.
ServerAliveIntervalhelps 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. - 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 phoneattaches 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. - 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.
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.
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 → +:
| Field | Value |
|---|---|
| Host | macbook |
| HostName | macbook.your-tailnet.ts.net |
| Port | whatever you set in chapter 05 |
| User | your account name |
| Key | iphone-se |
| Mosh command | tmux 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
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.
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:
- Are you using Mosh, or did you fall back to SSH? The command is
mosh, notssh. This is the answer most of the time. - Low Power Mode? It makes iOS far more aggressive about suspending apps.
- 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. - 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-staticrun against each machine.- A saved host per machine, with
tmux new -A -s phoneas 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.