Chapter 10 · Operate

Threat model & key custody

Security advice given without a threat model is just superstition with a command line. This chapter states plainly who you are defending against, which controls actually stop them, and — the part usually left out — what this design does not protect you from.

Who is actually attacking you

AdversaryLikelihoodWhat stops them
Automated internet scanners
botnets sweeping for open SSH
Constant. Minutes after a VPS gets an IP. Fully solved. Tailnet-only binding means there is no port to find. This is the single highest-value control in the guide.
Hostile local network
café, hotel, conference Wi-Fi
Every time you travel. Solved. WireGuard encrypts everything; default-deny inbound means your laptop offers nothing to the segment it joined.
Lost or stolen device Plausible. Phones and laptops go missing. Strongly mitigated. Full-disk encryption at rest, Secure Enclave keys that cannot be extracted, and one-click revocation in the tailnet.
Malware on a device you own Possible. A bad dependency, a malicious VS Code extension. Partially. Secure Enclave keys cannot be stolen — but malware can still use the key while you are unlocked. Assume a compromised endpoint means compromised access.
Compromise of Tailscale itself Low, high impact. Solvable. Tailnet lock removes the coordination server from your trust model — it can no longer insert a node into your network.
Targeted attacker with resources Only you know. Not solved, and no consumer setup solves it. Raise the cost; do not pretend to eliminate the risk.

Layers, and what each one actually stops

The value of defence in depth is that each layer fails differently. A misconfigured sshd_config does not matter if the packet never arrives; a tailnet ACL mistake does not matter if the key is not in authorized_keys.

Put an attacker on the wire and see which layer answers

The sandbox has a hostile machine you can add to the topology, and each attack it runs tells you which layer stopped it rather than merely that something did. A stolen node key dies at tailnet lock; a legitimately-joined machine dies at the ACL; a port scan dies at the firewall. Those are three different defences, and knowing which one is carrying you is the whole point of this chapter.

Who is knocking
DEFENCE RING WHAT THIS ATTACKER MEETS THERE 1 · Tailnet ACL stops: the whole internet, and untrusted devices 2 · Host firewall & bind stops: the café LAN, a service bound too widely 3 · sshd policy stops: root login, password guessing 4 · Key possession stops: anyone without the key 5 · PAM / SACL stops: accounts off the list your shell an attacker must defeat every ring, in order — and each ring fails differently before any ring has been tested a botnet sweeping the whole IPv4 space it has never heard of you — it is trying everyone it arrives minutes after any VPS gets an address a laptop on the same hotel Wi-Fi it can reach your machine's local address directly no tailnet membership is required for that you widened one ACL rule at 11pm a device you never meant to allow now matches ring 1 is the ring that eventually breaks your phone, in someone else's pocket still a tailnet member until you revoke it and it is locked you, on your own laptop the one case every ring is built to let through watch all five of them say yes stop · there is no port to find sshd never binds a public address, so the scan finds nothing bypassed · this ring never sees the packet they reach your Wi-Fi address directly, not the tailnet passed · and it should not have a broad grant let through a device you never meant to allow pass · on the tailnet, port granted the ACL allows this device to reach port 22 stop · nothing is listening there sshd binds the 100.x address only — that segment gets nothing pass · it reached the right interface the packet arrived on the tailnet interface, which is allowed not run ring 1 already ended it — nothing inside was ever tested stop · the daemon says no no passwords, no root, and this account is not in AllowUsers pass · policy permits this login key auth only, not root, and the account is on AllowUsers not run the packet never got this far stop · the enclave will not sign the key needs Face ID or the passcode, and the phone is locked pass · your key signs the challenge the private key never leaves the enclave; only signatures do not run no key was ever offered here pass · this account is on the SSH list PAM checks com.apple.access_ssh and lets you through not run an outer ring already stopped this attempt
  1. An internet scannerConstant, indiscriminate, and arriving within minutes of any VPS getting an address. It has never heard of you; it is trying everyone. This is the most common adversary by several orders of magnitude, and the one the outermost ring exists for.
  2. A neighbour on the café Wi-FiYou joined a hotel network and so did they. They can reach your machine's local address directly — no tailnet, no coordination server, nothing for ring 1 to have an opinion about. This is why ring 1 alone is not enough.
  3. A bad ACL editYou widened one rule late at night and a device you never meant to allow now matches it. Nothing is broken yet and nothing warns you. Ring 1 is the ring that eventually breaks — the question is what happens next.
  4. Your phone, in someone else's pocketIt is still a member of the tailnet, because you have not revoked it yet. It holds an SSH key that every one of your machines trusts. And it is locked. Watch how far it gets.
  5. You, with your keyThe one case every ring is built to let through. Worth stepping once, because it is the only run where you see all five rings answer yes — and it shows the cost of the design is a single ssh.
  6. 1 · Stopped at the tailnet ACLThere is no port to find. sshd is bound to the tailnet address and nothing else, so the scan hits a filtered port and moves on. This is the single highest-value control in the guide — it removes an entire adversary class instead of hardening against it.
  7. 1 · Bypassed, not defeatedThe rings are not literally concentric for every attacker. This packet is addressed to your Wi-Fi interface, so the tailnet ACL is never consulted at all. A control that is not on the path cannot help you — which is the whole argument for the ring below.
  8. 1 · Passed — and it should not haveThe grant was too broad, so a device you never intended to allow now reaches port 22. Ring 1 has failed silently, exactly the way misconfiguration always does. If it were your only layer, this would already be an incident.
  9. 1 · On the tailnet, port grantedThe device is a member and the ACL permits it to reach port 22, so the packet is delivered. Correct in both cases — the phone is yours, and you have not revoked it yet.
  10. 2 · Stopped at the bind addresssshd listens on the 100.x address only, so on the hotel segment there is simply nothing to answer. Ring 1 never applied here; ring 2 did the work alone. Default-deny inbound means your laptop offers that network nothing.
  11. 2 · Reached the right interfaceThe packet arrived over the tailnet interface, which is precisely the one sshd is bound to. Nothing here looks suspicious yet — a device that slipped past ring 1 is indistinguishable from a legitimate one at this layer.
  12. 2 · Not runRing 1 already ended it. Nothing inside was ever tested, which is the point: a control that stops traffic before it arrives makes every control behind it irrelevant to that attacker.
  13. 3 · The daemon says no — and this is the payoffPassword authentication is off, root login is off, and this account is not in AllowUsers. Ring 1 failed, and the result was an inconvenience rather than an incident. This is why you harden sshd even behind a private network.
  14. 3 · Policy permits this loginKey authentication only, no root, and the account is on the allow list. The daemon has no objection to this attempt — it is a legitimate user account presenting itself the approved way.
  15. 3 · Not runThe packet never got this far, so sshd_config played no part in the outcome. Worth remembering when you are tempted to debug a connection problem by editing it.
  16. 4 · The enclave will not signThe key is hardware-bound and every use is gated by Face ID or the passcode. A thief has the device, not the key — and cannot copy it out to try elsewhere. This is what makes losing a phone a chore rather than a crisis.
  17. 4 · Your key signs the challengeThe private key never leaves the enclave; the enclave returns a signature over the session ID. Note the honest limit: if malware were running on your unlocked machine, it could ask for that same signature. Possession of the device is the boundary.
  18. 4 · Not runNo key was ever offered here, because an outer ring answered first. Key custody is a strong layer, but it only gets a turn if the packet reaches it.
  19. 5 · On the SSH access list, and into the shellPAM checks com.apple.access_ssh and lets you through. Five rings, five yeses, one shell. Each of them fails differently, which is the entire value of the arrangement — the failure of any one is survivable.
  20. 5 · Not runAn outer ring already stopped this attempt, so the shell was never in reach. Switch the attacker above and watch where the run ends — no two of them stop at the same ring.
An attacker must defeat every ring, in order. Most real-world compromises come from a single layer being the only layer. The reason to keep sshd hardened even behind a private network is that layer 1 will eventually be misconfigured, and you want that to be an inconvenience rather than an incident — switch to A bad ACL edit above and watch ring 3 catch what ring 1 let through.

Where every key lives

Knowing this table cold is what makes an incident boring instead of frightening.

KeyLivesExtractable?If the device is lost
iPhone SSH keySecure EnclaveNo — hardware-bound Remove one authorized_keys line per host; revoke the node. Nothing to rotate elsewhere.
Mac / Ubuntu SSH key~/.ssh/id_ed25519, passphrase-encryptedYes, with the passphrase Assume compromised. Rotate it everywhere, promptly.
Tailscale node keyPer-device, generated locallyNo Revoke the device in the admin console. Immediate and global.
SSH host keys/etc/ssh/ssh_host_* on each serverRoot can read them Server compromise means regenerating them, and every client will warn on next connect.
FileVault / LUKS keyDerived from your passphraseNo This is what makes a stolen disk useless. Store the recovery key off the machine.
What goes wrong
File-based key ~/.ssh/id_ed25519 key bytes on disk the file is read whatever opened it Secure Enclave key separate coprocessor, no export path private key sealed inside a signature only ssh client never sees it the laptop left with the file on it FileVault protects it while it is powered off and only while it is powered off the phone left with the key sealed the enclave goes with it, intact and inert locked behind Face ID or the passcode a process reads the file one open() is the whole attack no prompt, no Face ID, no trace left behind it asks the enclave to sign it cannot read the key, but it can use it for as long as the device stays unlocked the backup copied the file the key is now inside the archive wherever that archive ended up the backup found nothing to copy no file on the filesystem holds this key it has never existed as bytes you can move the disk holds the key shut down it is safe; asleep it is less so assume the file becomes readable eventually the enclave holds the key every use is gated by Face ID or a passcode and it cannot be moved off the chip at all they hold the key itself usable from any machine, at any time and you cannot tell that it happened they hold no key, only access signatures, on this device, while unlocked revoke the node and it ends immediately the key is in the archive protected only by its passphrase which brute-forces offline, patiently the archive holds nothing useful no bytes were ever exported to copy there is nothing in there to crack later rotate everywhere, promptly every host and service that trusts this key under time pressure, on a bad day remove one line per host revoke the node in the admin console first nothing else to rotate, anywhere rotate everywhere, now every host and service that trusts this key then audit what was done with it meanwhile revoke the node, then audit the key never left — the endpoint did nothing to rotate on your other machines rotate everywhere you do not know who read that archive or how many copies of it still exist nothing to do there was never a copy to leak the backup was always safe to store the enclave is a signing oracle, not a container — there is no operation that hands the key back
  1. Day to day, they are indistinguishableBoth keys log you in. The difference only shows up on the worst day of the year, which is exactly why it is worth understanding before then. The file key hands its bytes to whatever opens the file; the enclave hands back a signature and keeps the key.
  2. 1 · The device is goneThe laptop leaves with the key file on its disk. FileVault protects that file while the machine is powered off — and only then; a sleeping machine has already derived the volume key. The phone leaves with its key too, but sealed in a coprocessor that has no read operation.
  3. 1 · Something is running that should not beA bad dependency or a malicious extension. On the file side, one open() is the entire attack — no prompt, no trace. On the enclave side it cannot read the key, but here is the honest limit: it can ask the enclave to sign while you are unlocked. Hardware-bound is not the same as safe from a compromised endpoint.
  4. 1 · A backup ran, unencryptedTime Machine to an unencrypted disk, a tar of your home directory, a sync tool with generous defaults. The file key is now inside that archive. The enclave key was never a file, so there was nothing for the backup to pick up.
  5. 2 · What they end up holdingThe disk holds the key file, so you have to assume it becomes readable eventually — a stolen laptop is not a one-hour problem. The enclave holds the phone's key, and every use of it is gated by Face ID or the passcode. It cannot be copied off the chip at any price.
  6. 2 · What they end up holdingOn the file side, the key itself: usable from any machine, at any time, and you cannot tell it happened. On the enclave side, no key at all — only access, bounded to that one device and to the time it stays unlocked. That bound is the difference between an incident and a permanent one.
  7. 2 · What they end up holdingThe key file is in the archive, protected only by its passphrase — and a passphrase in someone else's hands brute-forces offline, patiently, without rate limits. The enclave key is simply not in there. Nothing to crack now, and nothing to crack in five years.
  8. 3 · What it costs youFile key: rotate everywhere, promptly, on every host and service that trusts it — under time pressure, on a bad day. Enclave key: revoke the node in the admin console, then delete one authorized_keys line per host. Nothing else to rotate, because the key existed nowhere else.
  9. 3 · What it costs youFile key: rotate everywhere, then audit what was done with it in the meantime. Enclave key: revoke the node and audit the same window — but you do not rotate keys on your other machines, because this one never left the phone. A compromised endpoint is still a compromised endpoint; the blast radius is what changes.
  10. 3 · What it costs youFile key: rotate everywhere, because you do not know who read that archive or how many copies of it exist. Enclave key: nothing at all. The backup was always safe to store, and that is a property you get for free rather than by remembering to be careful.
The enclave is a signing oracle, not a container. You never retrieve the key; you ask for signatures. That is why losing the phone does not mean rotating keys on your other machines — the key existed nowhere else and could not have leaked ahead of time. Switch to Malware on the machine for the honest limit: the key stays put, but it will still sign for whoever is running as you.

Blast radius

For each thing that could go wrong, what does the attacker actually get?

CompromisedThey getThey do not get
iPhone, lockedNothing. The enclave requires Face ID or passcode.Any access at all.
iPhone, unlockedShell on every machine, until you revoke.The key itself — so revocation is complete and final.
Mac, powered offNothing. FileVault.Keys, sessions, history.
Mac, unlocked and malware-infectedEverything that machine can reach, plus its SSH key.Your phone's key, your VPS disk at rest.
VPS rootThe VPS, its data, its host keys, its tailnet membership.Your laptops — unless you forwarded an agent, which is why forwarding is off.
Tailscale accountAbility to add a device — unless tailnet lock is on.Traffic, which is end-to-end encrypted.
The agent-forwarding row is the one to internalise

If you SSH into a compromised server with -A, root on that box can use your forwarded agent to authenticate as you, anywhere your key is trusted, silently, for as long as you stay connected. That converts "one server was compromised" into "everything was." Use ProxyJump instead — the keys stay on your machine.

What this design does not protect against

Stated plainly, because a threat model that claims to cover everything is not a threat model:

  • A compromised endpoint you are actively using. Malware on your unlocked Mac acts as you. No network design fixes this.
  • Malicious code you run yourself. A backdoored dependency executes with your privileges. This is the most probable serious compromise for most developers, and nothing in this guide addresses it.
  • Coercion. Encryption yields to a court order or a threat.
  • Your cloud provider. They control the hypervisor. Disk encryption on a VPS protects against decommissioned-disk recovery, not against the host.
  • Metadata. Your ISP cannot read WireGuard traffic but sees that you connect and how much.

Incident response

Phone lost or stolen

  1. Tailscale admin console → remove the device. It loses network access immediately; this is the fastest and broadest action, so do it first.
  2. Find My → Mark as Lost, and Erase if you are confident it is gone.
  3. Remove its line from ~/.ssh/authorized_keys on every machine.
  4. No further rotation needed — the Secure Enclave key never existed anywhere else.

A machine's SSH key may have leaked

  1. Remove the public key from every authorized_keys immediately. Revocation first, replacement second.
  2. Generate a fresh key on that device and distribute the new public half.
  3. Check ~/.ssh/authorized_keys on every host for entries you do not recognise — adding a key is the classic persistence move.
  4. Review authentication logs for the fingerprints that actually logged in: grep 'Accepted publickey' /var/log/auth.log. This is what LogLevel VERBOSE was for.

A server may be compromised

  1. Isolate it: sudo tailscale down, and remove the node from the tailnet.
  2. Do not trust anything it tells you. A rootkit edits the logs and the tools that read them.
  3. Snapshot the disk for later analysis if it matters.
  4. Rebuild rather than clean. Restore data, not binaries. Regenerate host keys; clients will warn, and that warning is correct.
  5. Rotate every credential that machine held.

Review periodically

  • Every entry in every authorized_keys is one you recognise and still want.
  • Tailnet device list contains no machines you have retired.
  • Key expiry is on where appropriate; you know where you disabled it and why.
  • Tailnet lock is enabled and recovery keys are reachable from another device.
  • FileVault and LUKS recovery keys are stored off the machines they unlock.
  • ACL policy still reflects reality after any new device joined.
  • Auth logs reviewed for unfamiliar key fingerprints.

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.