From 240edfb6778039e5a7b5a6ac47a813f6d9ba8654 Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Sun, 9 Aug 2026 16:05:13 +0700 Subject: [PATCH] Write the Tailscale policy in grants, not acls (#46) The console ships a grants-based default policy, so an acls example forces the operator to translate before they can use it - and the two differ in exactly the place that matters: grants carry ports in `ip` (`tcp:9222`) rather than suffixed onto `dst`. Adds the three things dropping the blanket grant silently takes away: an ssh block, since tagged devices leave `autogroup:self` and Tailscale SSH stops resolving them; a commented `autogroup:internet` grant for exit-node users; and `tcp:22` to the browser machine, without which tagging locks you out. --- DEPLOY.md | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/DEPLOY.md b/DEPLOY.md index 2fbe9ab..6cc6daf 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -314,12 +314,13 @@ 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 -`accept` 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 +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**, replace the default rule: +In the admin console, under **Access controls**, the shipped policy grants +`{"src": ["*"], "dst": ["*"], "ip": ["*"]}`. Replace it: ```jsonc { @@ -329,24 +330,44 @@ In the admin console, under **Access controls**, replace the default rule: "tag:bookmark-browser": [], }, - "acls": [ + "grants": [ // The only thing on the tailnet that may drive the browser. { - "action": "accept", - "src": ["tag:bookmark-api"], - "proto": "tcp", - "dst": ["tag:bookmark-browser:9222"], + "src": ["tag:bookmark-api"], + "dst": ["tag:bookmark-browser"], + "ip": ["tcp:9222"], }, - // Your own devices: everything of yours, both servers — but not CDP. - // Drop the `:22` and you have locked yourself out of the browser machine. + // Your own devices reach your own devices, and the VPS, in full. { - "action": "accept", + "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:22"], + "dst": ["autogroup:self", "tag:bookmark-api", "tag:bookmark-browser"], + "users": ["autogroup:nonroot", "root"], }, ], - // Saved policies are rejected if these fail, so the rule cannot rot silently. + // Run on every save, so a later edit that reopens 9222 is rejected outright. "tests": [ { "src": "tag:bookmark-api", "accept": ["tag:bookmark-browser:9222"] }, {