Skip to content
Notifications
Clear all

Tailscale ACLs are making my head spin - any simple examples?

7 Posts
7 Users
0 Reactions
2 Views
(@jordanf)
Trusted Member
Joined: 1 week ago
Posts: 42
Topic starter   [#9684]

I've been tasked with implementing Tailscale across our development environments to segment access between teams (frontend, backend, data science). The concept of ACLs seems powerful, but I'm struggling to translate our policy ideas into the actual `tailscale.com` ACL syntax.

The documentation is comprehensive, but I'm missing some straightforward, real-world patterns. For instance, how do you cleanly handle a scenario where:
* A group of "admin" users can reach all hosts.
* The "backend" team can access their own tagged servers and the "database" tag, but not the frontend servers.
* The "frontend" team can only access the "backend-api" tag on port 8080.

My current attempt is a mess of auto-approvers and tag owners, and I'm not confident it's secure. I keep worrying about a misplaced wildcard.

Could anyone share a simplified, annotated example ACL file that demonstrates:
* Defining groups and tags clearly.
* Using those groups and tags in `autogroup:members` and `acls` sections.
* A clear, layered permission model without circular dependencies?

I'm particularly interested in seeing how you manage the order of rules and the principle of least privilege in practice. A working example would be immensely helpful to reverse-engineer.



   
Quote
(@katherinea)
Eminent Member
Joined: 1 week ago
Posts: 26
 

I've been there. The syntax can feel alien at first. For your specific scenario, start by mapping your requirements directly onto the four core sections: `acls`, `tagOwners`, `groups`, and `autoApprovers`.

First, define your groups in the `groups` section, like `"group:admin": ["alice@company.com", "bob@company.com"]` and `"group:backend": ["charlie@company.com", ...]`. Then, in `tagOwners`, you'd declare who can apply the server tags, e.g., `"tag:database": ["group:backend"]` and `"tag:backend-api": ["group:backend"]`. This keeps tag management with the right teams.

Your `acls` list would then have rules in order. A first rule for admins: `{"action": "accept", "src": ["group:admin"], "dst": ["*:*"]}`. Then for the backend team: `{"action": "accept", "src": ["group:backend"], "dst": ["tag:backend-servers:*", "tag:database:*"]}`. Finally, a rule for the frontend team: `{"action": "accept", "src": ["group:frontend"], "dst": ["tag:backend-api:8080"]}`. Remember, rules are processed top-down, so that first admin wildcard is safe because it's specific to that source group.

The key is letting the `tagOwners` section handle the security of who *tags* a device, while the `acls` section purely defines access between those tagged resources and your groups. That separation cleaned up most of my early confusion.


read the contract


   
ReplyQuote
(@katherineh)
Eminent Member
Joined: 1 week ago
Posts: 30
 

You're on the right track, but your example `dst` for the backend rule has a critical nuance. Using `"tag:backend-servers:*"` permits access to *all ports* on those hosts. That's often too permissive for production.

A more precise pattern would specify ports explicitly, even for the backend team. For example, `"tag:backend-servers:22,443,3000-3002"`. This mirrors real segmentation where a backend dev might need SSH, a metrics port, and the app port, but shouldn't have carte blanche to, say, a Redis admin port on the same box.

Also, I'd suggest adding an explicit `"action": "accept"` rule for `"group:admin"` to reach `"tag:tailnet:*"` *before* the wildcard `"*:*"`. It's functionally the same given rule order, but it documents intent cleanly and prevents accidental expansion later if someone reorders rules.


—KH


   
ReplyQuote
(@kerneldev)
Estimable Member
Joined: 4 months ago
Posts: 68
 

Good point about specifying ports. That granularity is what makes Tailscale's ACLs powerful vs. a simple firewall.

I'd even take it a step further: don't just rely on port lists, consider explicit service names in your tag definitions. For instance, you might have:
* `tag:backend-app` for the main service (ports 3000-3002)
* `tag:backend-monitoring` for metrics (port 9100)
* `tag:backend-admin` for SSH (port 22)

Then your ACL rules can reference `"tag:backend-app:3000-3002"`, `"tag:backend-monitoring:9100"`, etc. It's more verbose, but it maps cleaner to actual services and principle of least privilege. It also makes audit logs easier to parse later 😅

Anyone know if there's a measurable overhead to having a longer ACL list with many tags vs. fewer rules with complex port lists? The rule processing happens in userspace, right?


System calls per second matter.


   
ReplyQuote
(@cloud_watcher_99)
Reputable Member
Joined: 1 month ago
Posts: 172
 

Great approach with the service-specific tags. We use something similar for our AWS workloads - a tag for the Prometheus exporter port, another for the debug port. It makes FinOps tagging audits easier too because you can cross-reference cloud service tags with Tailscale tags.

On overhead, from what I've seen in our Grafana dashboards, rule count barely registers compared to packet encryption overhead. The userspace processing is pretty lightweight. The real cost is in human readability when you have 50+ tags, but that's a docs problem, not a performance one.

Have you tried the `tailscale debug` commands to see rule evaluation time? Might be interesting to compare a few long lists vs. one complex rule.


cost first, then scale


   
ReplyQuote
(@data_pipeline_tinker)
Estimable Member
Joined: 3 months ago
Posts: 122
 

That's a useful data point on the performance side. We haven't benchmarked rule evaluation directly, but our monitoring setup echoes your observation - the WireGuard encryption overhead dominates the latency charts, not ACL traversal.

Your mention of cross-referencing cloud service tags with Tailscale tags is a solid pattern. We enforce this through a small Terraform module that syncs AWS resource tags to a central registry, which then generates a portion of our ACL config. It prevents drift between the infra state and the network policy.

On human readability with 50+ tags, that's where a structured comment block or even a separate, generated markdown file for documentation pays off. The ACL file itself becomes machine-verified policy, and you rely on the docs to explain the intent behind tag groups like `backend-monitoring`.


Extract, transform, trust


   
ReplyQuote
(@coffeelover)
Estimable Member
Joined: 1 week ago
Posts: 111
 

> auto-approvers and tag owners

You're mixing two different things. Auto-approvers are for sharing nodes from personal devices, not for defining access control. Stick to `groups`, `tagOwners`, and `acls`. The `autoApprovers` section is a trap for people who want to treat Tailscale like a corporate VPN.

Your example screams "I'm overcomplicating dev environments." For a dev sandbox, why not just give the backend team `tag:backend-servers:*` and `tag:database:*` and call it a day? The frontend team only needs one port on one tag. You don't need a layered permission model with 50 rules for a dozen engineers. More rules = more chances to misplace a wildcard, which is exactly what you're worried about.

Here's a stripped-down version that works:

groups:
group:admin: [alice, bob]
group:backend: [charlie, dave]
group:frontend: [eve, frank]

tagOwners:
tag:backend-servers: [group:backend]
tag:database: [group:backend]
tag:backend-api: [group:backend]

acls:
action: accept
src: [group:admin]
dst: [*:*]

action: accept
src: [group:backend]
dst: [tag:backend-servers:*, tag:database:*]

action: accept
src: [group:frontend]
dst: [tag:backend-api:8080]

That's it. No `autoApprovers`, no `autogroup:members` (which is for user groups, not tags). If you need to add data science later, just create a `group:data-science` and a `tag:data-pipeline` and add a rule. Don't try to model every possible future need in one ACL file.

Also, put your admin rule last. Not because it matters for Tailscale, but because it forces you to think about least privilege. If you start with `*:*`, you'll never bother fixing the rest.


Just my two cents.


   
ReplyQuote