diff --git a/DEPLOY.md b/DEPLOY.md index 892f5b5..6cc6daf 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -309,18 +309,95 @@ refuses to start rather than guess. **Narrow it to the one device that needs it.** The bind address keeps CDP off your LAN; it still leaves port 9222 open to every device on the tailnet, and -CDP has no login. Add a rule in the Tailscale admin console's access controls -so only the VPS can reach it — tag the two machines, then: +CDP has no login — a compromised phone is enough to drive this host. A new +tailnet's policy is allow-all, so this is the step that makes "Tailscale +identity is the access control" true rather than aspirational. + +Tailscale has no `deny`, so a restriction is expressed by removing the blanket +grant and enumerating what is left. That only works if the browser machine can +be *excluded* from a selector that still covers your own devices — which is +what tagging buys: a tagged device has no user, so `autogroup:member` and +`autogroup:self` stop matching it. Tagging is the mechanism, not decoration. + +In the admin console, under **Access controls**, the shipped policy grants +`{"src": ["*"], "dst": ["*"], "ip": ["*"]}`. Replace it: ```jsonc -// tailnet policy file -"acls": [ - { "action": "accept", "src": ["tag:bookmark-api"], "dst": ["tag:bookmark-browser:9222"] }, -] +{ + "tagOwners": { + // Empty list: implicitly owned by the tailnet Owner/Admins, which is you. + "tag:bookmark-api": [], + "tag:bookmark-browser": [], + }, + + "grants": [ + // The only thing on the tailnet that may drive the browser. + { + "src": ["tag:bookmark-api"], + "dst": ["tag:bookmark-browser"], + "ip": ["tcp:9222"], + }, + // Your own devices reach your own devices, and the VPS, in full. + { + "src": ["autogroup:member"], + "dst": ["autogroup:self", "tag:bookmark-api"], + "ip": ["*"], + }, + // On the browser machine you get SSH and nothing else. Widen this to `*` + // and the restriction above is void; delete it and you are locked out. + { + "src": ["autogroup:member"], + "dst": ["tag:bookmark-browser"], + "ip": ["tcp:22"], + }, + // Uncomment if you route traffic through an exit node — dropping the + // blanket grant takes exit-node access with it. + // {"src": ["autogroup:member"], "dst": ["autogroup:internet"], "ip": ["*"]}, + ], + + // Tagged devices left `autogroup:self`, so Tailscale SSH needs them named. + // Irrelevant if you reach these boxes with ordinary sshd over the tailnet — + // that is the `tcp:22` grant above. + "ssh": [ + { + "action": "check", + "src": ["autogroup:member"], + "dst": ["autogroup:self", "tag:bookmark-api", "tag:bookmark-browser"], + "users": ["autogroup:nonroot", "root"], + }, + ], + + // Run on every save, so a later edit that reopens 9222 is rejected outright. + "tests": [ + { "src": "tag:bookmark-api", "accept": ["tag:bookmark-browser:9222"] }, + { + "src": "you@example.com", + "accept": ["tag:bookmark-browser:22"], + "deny": ["tag:bookmark-browser:9222"], + }, + ], +} ``` -Without a rule the tailnet default is allow-all, so this step is what makes -"Tailscale identity is the access control" true rather than aspirational. +Then apply the tags — on the VPS and the home machine respectively: + +```bash +sudo tailscale up --advertise-tags=tag:bookmark-api +sudo tailscale up --advertise-tags=tag:bookmark-browser +``` + +Each re-authenticates in a browser and issues a new node key; the tailnet IP is +unchanged, so `BROWSER_WS_URL` and `BROWSER_BIND_ADDR` still hold. Key expiry is +disabled once a device is tagged, which is what you want for a server — an +expired key would otherwise take the poller down every few months. + +**Tagging replaces the device's user identity**, so do this only to machines +that exist to run these services. If your "home machine" is also your daily +driver, tag it anyway and reach it through the `:22` rule above, or skip the +tag and accept that any device of yours can reach CDP. + +Enforcement is by the destination's packet filter, so the check below is real, +not advisory. Prove the bind is tight, from the home machine itself: @@ -334,6 +411,17 @@ The first call is also what wakes Chrome: it is not running until something connects, and it is reaped again after five idle minutes. A cold first response takes a few seconds; that is the browser starting, not a fault. +That check proves the *bind*, not the ACL — traffic that starts on the node is +not filtered. Prove the ACL from somewhere else: on your laptop or phone the +same URL must now time out, and from the VPS it must answer. + +```bash +# on any other device of yours -> hangs until timeout +curl -s -m 5 http://:9222/json/version +# on the VPS -> JSON +curl -s -m 20 http://:9222/json/version +``` + **On the VPS:** ```bash