From 8081a0a5d8db6aafb3f026824b2836b98d1ef5cf Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Sun, 9 Aug 2026 16:12:03 +0700 Subject: [PATCH] Give the Tailscale ACL step a working policy file (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #52, which merged before this landed. Docs only — no code, no compose changes. `DEPLOY.md` §7 told the operator to "tag the two machines" and showed a bare `acls` fragment. Following it literally does not work and is actively harmful: - the fragment references `tag:bookmark-api` / `tag:bookmark-browser` without a `tagOwners` section, so the policy is rejected on save; - it never says how a tag gets onto a device (`tailscale up --advertise-tags=...`, which re-authenticates); - replacing the tailnet's default allow-all with only that one rule **removes the operator's own SSH access to the browser machine**. Replaced with a complete, saveable policy file: `tagOwners`, the CDP rule, a second rule preserving own-device access including `:22`, and a `tests` block so a later edit that widens 9222 is rejected rather than silently applied. Also records two things that were assumed rather than stated: - **Why tagging is load-bearing.** Tailscale has no `deny`, so restricting 9222 means removing the blanket accept and enumerating what remains. That is only expressible if the browser machine falls outside a selector that still covers your own devices — which is exactly what a tag does, since a tagged device has no user and stops matching `autogroup:member` / `autogroup:self`. Without that, the whole step reads as arbitrary ceremony. - **Tagging replaces a device's user identity**, so it suits a dedicated box and disrupts a daily driver. Both paths are now written down. Finally, separates two checks the old text conflated: the existing `curl` runs on the home machine and proves only the **bind**, because node-local traffic is not filtered. Proving the **ACL** needs a third device, so that check is now its own step. Verified: `tailscale.com/docs/reference/syntax/policy-file` and `/docs/features/tags` (validated Apr 2026 / Dec 2025) for `tagOwners`, `autogroup:self` semantics vs tagged devices, `--advertise-tags` re-auth and key-expiry behaviour. Markdown fences balanced. Reviewed-on: https://gitea.violetcrown.my.id/sulthan/mangaBookmark/pulls/53 Co-authored-by: Sulthan Zaki Co-committed-by: Sulthan Zaki --- DEPLOY.md | 104 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 96 insertions(+), 8 deletions(-) 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