Control access
Test policy before trusting it
Add positive and negative policy tests so a future edit cannot silently broaden or remove important access paths.
A valid policy can still grant the wrong access. The syntax checker catches a missing comma. It cannot catch a rule that is perfectly formed and lets your intern into the production database.
Policy tests fix that. They record examples of connections that must keep working and connections that must keep failing. Tests encode intent, and intent is exactly what a syntax check cannot see.
Write the tests
Tailscale policy files support a tests section. Each test names a source, then lists what must be accepted and what must be denied:
{
"tests": [
{
"src": "[email protected]",
"accept": ["tag:server:22"],
"deny": ["tag:server:5432"]
},
{
"src": "[email protected]",
"deny": ["tag:server:22"]
}
]
}
The first test says the operator can reach SSH on servers but not port 5432. The second says an ordinary member cannot reach SSH at all.
The tests run every time you save the policy. A failing test blocks the save, so the bad policy never goes live. That is the property you want: the check happens before the damage, not after.
Negative tests carry the weight
The deny entries are the valuable ones. Here is why.
An accept test fails loudly on its own the moment someone needs that access, because their connection stops working and they tell you. A missing deny fails silently. The unwanted access exists, nobody notices, and it sits there until someone finds it. Maybe an auditor. Maybe an attacker.
So every time you grant something narrow, add a deny test for the broader thing you deliberately did not grant. Granted port 22? Add a deny for another port. Granted one group? Add a deny for a user outside it.
Break it on purpose
Add at least one accept case and one deny case for the lab server. Then break a rule on purpose and confirm validation catches it.
Change tcp:22 to tcp:* in your grant and try to save. Your deny test for port 5432 should fail and refuse the save. That refusal is the whole point. A future edit, made by a tired version of you six months from now, cannot silently widen access past what the tests pin down.
Keep the tests next to the policy, in the same file. Review the effective change before saving, especially when you touch tags or broad selectors like autogroup:member. Those are the edits that widen access by accident.
Lesson completed