The whole picture
Reach your own machines from anywhere
The goal is narrow and worth stating precisely: open a phone or a laptop anywhere in the world, reach your own machines in under two seconds, and lose nothing when the train enters a tunnel. Everything in this guide exists to serve that sentence.
Most remote-access setups fail in one of two directions. They are either wide open — a password-authenticated SSH port exposed to the internet, quietly absorbing thousands of login attempts a day — or they are so locked down that you stop using them, which is its own kind of failure. This guide aims at a third thing: a network where your machines are invisible to everyone but you, and a session layer that treats disconnection as normal rather than exceptional.
Those are two separable layers, and it is worth knowing which one you actually need. The network layer — Tailscale and SSH, plus the hardening chapter for each machine — gives you private reachability, and it is useful whatever you point at it: a database that should never touch the public internet, a home server, a NAS, a web app you are not ready to publish. The session layer — Mosh and tmux, chapters 03 and 04 — is what turns that connection into somewhere you can work for hours from a phone on a moving train. Build the first on its own if that is all you want; the second earns its keep only if you live in a terminal.
The four machines
Four devices, one private network. The phone is a client only. The other three are machines you work on, and each is hardened in its own chapter.
- Four machines, no shared networkA phone, two laptops and a server. Three of them sit behind NAT with no reachable public port, and nothing here can address anything else. This is the honest starting point.
- 1 · Each device registersEvery machine sends its public key and a list of candidate endpoints to the coordination server, and gets back the keys and ACL policy for the others. Private keys are generated on-device and never move.
- 2 · The tailnet existsEach device now holds a stable
100.xaddress that follows it between Wi-Fi, LTE and ethernet. Rules are written against these identities, not against whatever public IP the café hands you today. - 3 · Traffic flows between devicesPackets go device to device, sealed with WireGuard keys the coordination server has never held. It handed out public keys and policy and then got out of the way — it is not on this path.
- 4 · Direct, and fastBoth ends punched through their NATs, so packets take the shortest route the internet will give them. The relay sits idle. This is the ordinary case, and it is why a shell from your phone feels local.
- 4 · Relayed, and still privateOne NAT refused to cooperate, so that pair falls back to a DERP relay. The relay forwards sealed packets it has no key to open: you pay latency, not confidentiality. Never disable it to "force security" — you will just lose connectivity.
- 4 · The coordinator goes awayYour traffic does not care. Devices already hold each other's keys, so existing paths keep carrying packets. What you lose is the control plane: no new device can join, and ACL changes will not propagate until it is back.
The three planes
It helps to stop thinking of this as "a VPN plus SSH" and start thinking in planes. Each answers a different question, and each fails differently.
Identity — who are you?
Tailscale ties every device to an account and a signed node key. Access rules are written against identities, not IP addresses. Revoking a stolen phone is one click, not a key hunt.
Network — can you even reach it?
WireGuard builds encrypted point-to-point tunnels. Your SSH port binds to the tailnet address only, so from the public internet and even your own café Wi-Fi, the port does not exist.
Session — does your work survive?
Mosh survives roaming and sleep. tmux survives the client dying entirely. Herdr keeps coding agents alive and tells you which ones are blocked on you.
How the session layers stack
This is the part people usually get wrong, because the layers look redundant. They are not. Each one survives a failure the layer below it cannot.
- Your IP changesThe train leaves the station and the phone hands off from Wi-Fi to LTE. Your public address changes mid-sentence. Nothing you did caused it and nothing warns you first — this is the most common failure of the four by a wide margin.
- Twenty minutes in a pocketThe screen goes off, the phone suspends the app, and the radio goes quiet. From the server's point of view your client simply stopped talking, which is indistinguishable from your client having died.
- You close the terminalNot a network problem at all. The client process is gone, and so is everything it was holding in memory. Anything that lived only on the phone is now unrecoverable.
- The host rebootsA kernel update, a power cut, or you typing
sudo reboot. This one is different from the other three: the failure is on the far side, where all of your actual work lives. - 1 · WireGuard / Tailscale holdsThe tailnet address is tied to the device, not to whatever network it is standing on. It survives the handoff, comes back on wake, and returns at boot. This layer is about reachability and identity — it has no opinion at all about your session.
- 2 · SSH dies — in every one of the fourSSH is a TCP connection, and TCP is pinned to a pair of addresses for its whole life. Change one, stay quiet too long, kill either end, and it is over:
client_loop: send disconnect: Broken pipe. SSH is not a persistence layer and was never trying to be. - 3 · Mosh does not careThere is no connection to break. Mosh syncs screen state over UDP, so a new IP is just a new return address. You keep typing — with local echo, the keystrokes were never waiting on the network anyway.
- 3 · Mosh picks up where it stoppedOn wake, the client sends its last known state and the server sends the difference. A second of catch-up, no reconnect, no lost scrollback. This is the layer that makes a phone a usable terminal.
- 3 · Mosh goes with the clientThe session state lived in
mosh-client. Closing the app took it with you, and a fresh client cannot adopt the old session. Mosh survives bad networks, not a dead client — which is exactly what the layer above it is for. - 3 · Mosh is a process on that host
mosh-serverdied with everything else when the machine went down. No transport can survive the disappearance of the thing it was talking to. - 4 · tmux never noticedYour processes were never attached to the phone. They are attached to a tmux server on the far machine, which spent the whole incident doing nothing unusual. Nothing here needed rescuing — but the two failures below did not need this layer either.
- 4 · tmux is what saves youThis is the failure the layer exists for. Your shells, builds and agents keep running with no terminal attached at all, because they never depended on one. The phone was a window, not a container.
- 4 · tmux is the honest limittmux is a process too. When the host restarts, the tmux server and every process inside it go with it. Nothing in this stack survives a reboot of the machine your work runs on — which is why long jobs belong in something that writes to disk.
- 5 · Herdr carried straight throughEvery agent pane kept running and kept its status. Five layers, one failure, and the only one that had to do any work was Mosh. Each layer sits idle until the specific failure it owns shows up. That idleness is the design, not waste.
- 5 · Herdr tells you what changedYou come back to a page that already says which agents finished while you were away and which are blocked on you. Twenty minutes of not looking cost you nothing, and you do not have to open five panes to find out.
- 5 · Reattach, unchanged
mosh macbook -- tmux new -A -s phoneputs you back in front of the same panes with the same agents. Closing the app was never destructive, which is what makes the phone usable as a real client. - 5 · Herdr rebuilds the room, not the workWith
resume_agents_on_restoreset, Herdr brings the agent conversations back after its server restarts. Your layout and context return; the scrollback and any half-finished build do not. This is the failure worth planning for — the other three plan for themselves.
client_loop: send disconnect: Broken pipe. Mosh removes the TCP dependency.
tmux removes the client dependency entirely. Herdr adds awareness of what your agents are
doing. Switch the failure above and watch how few layers do any work in each one — that
idleness is the design, not waste.
Tailscale decides who can reach the port. SSH decides who can log in. Mosh decides whether a bad network ends your session. tmux decides whether closing your phone ends your work. Confusing these four is the root of most bad setups.
What you will actually type
The end state is short enough to memorise. From Blink Shell on the phone:
mosh macbook -- tmux new -A -s phone
That single line rides an encrypted WireGuard tunnel to a host that is invisible to the rest of the internet, authenticates with a key that cannot be extracted from your phone, survives you walking into a lift, and reattaches to a session that has been running for a week. Inside it, Herdr holds your coding agents.
Chapters
The first four explain the technologies well enough that the build chapters are obvious rather than magical. If you only want it working, jump to chapter 05 and refer back.
And one part of this repository that is not a chapter:
lab/ is the same topology as real containers —
four machines on four isolated segments, each behind its own NAT, running real
tailscaled on real TUN devices, with a control page where every switch is a
command that runs inside a container. docker compose up and a browser is the
whole setup; Docker is the only prerequisite. It is the counterpart to
chapter 14 — the sandbox can only show you what
somebody modelled, and the lab has no model, so it can surprise you. It already has, and
every surprise is written up with what we measured and which of the two we corrected.
Checklists throughout remember what you have ticked, per chapter, in this browser. The theme toggle sits in the bottom-right. Every command block has a copy button. Nothing here phones home, and the whole guide works offline from the filesystem.
Disclaimer
This guide is published under CC BY-SA 4.0, and the code that drives it under GPL-3.0-or-later. Both come with no warranty of any kind. Read that as it is meant: nobody who wrote, reviewed or contributed to this is responsible for what happens on your machines.
Take that seriously rather than as boilerplate, because of what the commands here actually
do. They change firewall rules, disable password login, rewrite sshd_config,
and deliberately remove the only route into a machine you may be a long way from. Done in
the wrong order, on a box you cannot walk over to, several of them will lock you out — and
the guide says so, loudly, at every point where that is a real risk. Those warnings are not
decoration.
- Your environment is not this one. Versions drift, distributions differ, your provider's networking is its own. A command that is correct here can be wrong for you. Understand what a line does before you run it — the guide explains the why for exactly this reason.
-
Practise somewhere disposable.
Chapter 14 is a simulation and costs nothing;
chapter 13 builds throwaway VMs;
lab/builds throwaway containers in one command. None of them is your production box, and a green result in any of them is not a promise about your real one. - Keep a second way in. A second SSH session, your provider's web console, physical access — proven working before you change anything, not assumed.
- The decision is yours, and so is the outcome. You accept the risk of running any of this. If something breaks, that is not a fault of this repository, its owner, or any contributor.
None of that is a reason not to do the work. It is a reason to do it in the order the guide gives you, on something you can afford to break first.
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.