Give the Tailscale ACL step a working policy file (#46)

The DEPLOY §7 snippet was a bare `acls` fragment: it named tags without
declaring tagOwners, without saying how tags get applied, and without the rule
that keeps the operator's own SSH access once the blanket accept is gone.
Following it literally locks you out of the browser machine.

Also records why tagging is load-bearing rather than stylistic - Tailscale has
no deny, so excluding CDP from a selector that still covers your own devices
requires the destination to fall outside autogroup:member/self, which is
exactly what a tag does - and separates the bind proof from the ACL proof,
which the old text conflated.
This commit is contained in:
2026-08-09 15:59:42 +07:00
parent 15382eb603
commit a5c8124b9b
+75 -8
View File
@@ -309,18 +309,74 @@ 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
`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
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:
```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": [],
},
"acls": [
// The only thing on the tailnet that may drive the browser.
{
"action": "accept",
"src": ["tag:bookmark-api"],
"proto": "tcp",
"dst": ["tag:bookmark-browser: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.
{
"action": "accept",
"src": ["autogroup:member"],
"dst": ["autogroup:self:*", "tag:bookmark-api:*", "tag:bookmark-browser:22"],
},
],
// Saved policies are rejected if these fail, so the rule cannot rot silently.
"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 +390,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://<home machine tailnet IP>:9222/json/version
# on the VPS -> JSON
curl -s -m 20 http://<home machine tailnet IP>:9222/json/version
```
**On the VPS:**
```bash