Grants explained
Grants are the rules in the tailnet policy file that control access in your Tailscale network (known as a tailnet). A policy file usually has many grants, and several of them can apply to the same device. The way Tailscale combines those grants decides who can connect to what.
This topic explains how Tailscale evaluates and combines grants. After you read it, you can predict the access that a policy file gives. You can also find the grant that causes unexpected access. This topic is for Owners, Admins, and Network admins who edit the tailnet policy file.
Overview
Grants work differently from the rules of a traditional firewall. A firewall reads rules from top to bottom, and the first rule that matches decides. In the tailnet policy file, every grant is an allow rule, and Tailscale adds together every grant that matches a connection. No grant can remove, override, or narrow access that another grant gives. For example, one grant gives group:eng access to port 5432 on tag:db. A second grant gives all tailnet members ping access to tag:db. A member of group:eng gets both. A grant that is more specific, or earlier in the file, has no priority. This model makes access predictable. It also means that one broad grant can open access that you expect another grant to restrict.
The following table compares firewall rules and grants:
| Behavior | Traditional firewall | Tailscale grants |
|---|---|---|
| Rule types | Allow and deny | Allow. Tailscale denies anything that no grant permits. |
| Evaluation | Top to bottom. The first match decides. | Tailscale checks every grant and adds the matches together. |
| Rule order | Often critical | No effect on access. Order affects IP set steps, SSH rules, and the order of app capability values. |
| Restrict a broad rule | Add a deny rule above it | Make the broad grant narrower. A second grant cannot restrict it. |
| Reply traffic | Depends on the product. Some products need a rule for each direction. | Replies to an allowed connection always get through. |
| Enforcement point | Often a device between networks | The device that receives the connection, or the subnet router in front of it |
| Invalid rule | Depends on the product | The save fails, and the previous policy stays active. |
Recommended reading
This topic builds on the following concepts:
- The tailnet policy file and the sections that define access, such as
grants,groups, andtagOwners. - Grants syntax, including the
src,dst,ip, andappfields. - Selectors, such as users, groups, tags, and autogroups, and the devices that each selector matches.
- Device posture, including how to define a posture and add it to an access rule.
- Subnet routers and exit nodes, including how they advertise routes to a tailnet.
Core concepts
These four concepts are the foundation for how Tailscale combines grants.
Grants
A grant is a rule that gives a source (src) network access, app capabilities, or both, to a destination (dst). Every grant has an implied accept action.
A policy file can contain both grants and entries in the legacy acls section. When Tailscale reads the policy file, it converts each acls entry into grants that give the same network access, then evaluates all grants together. The rules in this topic apply to acls entries in the same way.
Default deny
A tailnet denies all access that no grant permits. There are no deny rules, and no grant can block access that another grant permits. To remove access, you change or remove the grant that permits it.
To keep access blocked, add a test with a deny entry for it. If a later change to the policy file permits that access, the test fails and Tailscale rejects the save.
Network access and app capabilities
A grant can give two kinds of permission. Network access, in the ip field, lets a source open connections to destination ports. App capabilities, in the app field, are values that Tailscale sends to the destination application, which decides what they permit.
Posture conditions
A posture condition is a set of device requirements, such as an operating system or a client version. You attach it to a grant with the srcPosture field. It applies to that grant, not to the destination.
How grants work
Tailscale validates the whole policy file when you save it, then distributes the result to the devices in your tailnet. Each device that receives connections enforces network access. Applications read the app capabilities that Tailscale delivers for each source. The following sections explain the rules that connect these parts.
Rules for combining grants
Tailscale follows these rules when it combines grants:
- Grants add access: Tailscale adds together every grant that matches, so a device gets the access of each matching grant. No grant overrides or narrows another grant.
- Order does not change access: Moving a grant up or down in the file does not change who can connect. There is no "first match wins" rule and no "most specific wins" rule.
- Each grant carries its own conditions: Tailscale checks
srcPosturefor each grant separately, before it adds grants together. A condition on one grant does not affect other grants. - Network access and app capabilities are separate: The
ipfield controls which connections a source can open. Theappfield sends capabilities to the destination application, which decides what the source can do after it connects. A grant can include both fields. If a grant has only anappfield, the source needs network access to the application from a grant with anipfield. - The receiving device enforces access: The destination, or the subnet router in front of it, checks each new connection. Replies to an allowed connection always get through.
Components that apply grants
Four components take part when grants apply:
- Control plane: Validates the policy file and runs its tests when you save it. Distributes the rules and capabilities that apply to each device.
- Destination device: Checks each new connection against the grants that allow traffic to it. Receives the app capabilities of each source.
- Subnet router: Checks each connection to the networks that it advertises against the grants that it knows about.
- Destination application: Reads the app capabilities that Tailscale delivers and decides what each source can do.
Network access from multiple grants
A useful model is a keyring, a ring that holds several keys, where each key opens a different lock. A device carries every grant whose src includes it. A connection goes through if any grant on the ring permits it.
The following grants give different access to tag:db:
"grants": [
{
"src": ["group:eng"],
"dst": ["tag:db"],
"ip": ["tcp:5432"]
},
{
"src": ["group:oncall"],
"dst": ["tag:db"],
"ip": ["tcp:22"]
},
{
"src": ["autogroup:member"],
"dst": ["tag:db"],
"ip": ["icmp:*"]
}
]
The following table shows the access that each user gets:
| User | Matching grants | Access to tag:db |
|---|---|---|
Member of group:eng | First and third | Port 5432 and ping |
Member of group:eng and group:oncall | All three | Ports 5432 and 22, and ping |
| Other tailnet member | Third | Ping |
Grants can overlap completely without effect, because a second copy of the same access changes nothing.
A grant is necessary to open a new connection, but not to reply to one. The destination tracks connections and lets replies through:
- For TCP, Tailscale checks the packet that opens a connection. The rest of the connection flows in both directions.
- For UDP, a device accepts replies to traffic that it sent.
- For ICMP, a device accepts echo replies and error messages.
- A device does not filter the traffic that it sends. The receiving device decides.
- Any grant with an
ipfield permits ping to its destination, even a grant for one TCP port. A grant with anappfield and noipfield does not allow ping.
The following grant permits HTTPS from group:eng to tag:web:
"grants": [
{
"src": ["group:eng"],
"dst": ["tag:web"],
"ip": ["tcp:443"]
}
]
The following table shows which traffic this grant permits:
| Traffic | Result | Reason |
|---|---|---|
A laptop in group:eng opens an HTTPS connection to tag:web | Allowed | The grant permits port 443. |
| The web server responds to the laptop | Allowed | The response is a reply on a connection that the laptop opened. |
The laptop pings tag:web | Allowed | The laptop has network access to tag:web. |
| The web server opens a new connection to the laptop | Blocked | No grant gives tag:web access to the laptop. |
Device posture in a union
Tailscale checks posture for each grant separately, before it adds grants together. A device that fails the posture of a grant loses that grant and keeps every other grant. The device also loses any subnet routes that the grant would give it. Posture does not protect a destination. It protects one grant.
The following policy gives group:eng two grants to tag:prod. The first grant requires a posture:
"postures": {
"posture:managed": {
"assertions": [
"node:os == 'macos'",
"custom:mdm == 'enrolled'"
]
}
},
"grants": [
{
"src": ["group:eng"],
"dst": ["tag:prod"],
"ip": ["*"],
"srcPosture": ["posture:managed"]
},
{
"src": ["group:eng"],
"dst": ["tag:prod"],
"ip": ["tcp:443"]
}
]
The following table shows the access that each device gets:
Device in group:eng | Port 22 on tag:prod | Port 443 on tag:prod |
|---|---|---|
| Managed Mac | Allowed | Allowed |
| Unmanaged laptop | Blocked | Allowed |
The unmanaged laptop fails the posture, so it loses the first grant. It keeps port 443, because the second grant has no posture condition.
These rules apply to posture inside one grant:
- When a
srcPosturelist has several postures, a device must pass one of them. - A grant without
srcPostureusesdefaultSrcPosture, if you set one. This includes grants that Tailscale converts fromaclsentries. - A grant with its own
srcPostureuses its own list. The list replacesdefaultSrcPostureand does not add to it. - An empty
srcPosturelist ([]) removes the default posture from that grant.
Posture removes grants from devices that Tailscale can evaluate, which are the devices in your own tailnet. Two kinds of source keep a grant, whatever their posture:
- Devices shared into your tailnet from another tailnet. Your tailnet has no posture data for them.
- Addresses behind a subnet router, which are not Tailscale devices. A
srcof*includes every approved subnet route. If the subnet router uses source NAT, the router is the source, and the posture of the router applies.
The following grant uses posture:managed from the previous example:
"grants": [
{
"src": ["*"],
"dst": ["tag:print-server"],
"ip": ["tcp:631"],
"srcPosture": ["posture:managed"]
}
]
The following table shows which sources can reach tag:print-server:
| Source | Result | Reason |
|---|---|---|
| Managed Mac in your tailnet | Allowed | It passes the posture. |
| Windows laptop in your tailnet | Blocked | It fails the posture, so it loses the grant. |
| Windows laptop shared from another tailnet | Allowed | Tailscale cannot evaluate its posture. |
192.168.113.20 in an approved subnet route | Allowed | * includes the route, and Tailscale cannot evaluate its posture. |
App capabilities from multiple grants
Tailscale collects the app values from every matching grant. It sends them to the destination application as one list for each capability name. For how applications define capabilities, refer to app capabilities.
The following grants give three values for the same capability:
"grants": [
{
"src": ["group:eng"],
"dst": ["tag:wiki"],
"app": {
"example.com/cap/wiki": [{"role": "reader"}]
}
},
{
"src": ["autogroup:member"],
"dst": ["tag:wiki"],
"app": {
"example.com/cap/wiki": [{"role": "reader"}]
}
},
{
"src": ["group:docs"],
"dst": ["tag:wiki"],
"app": {
"example.com/cap/wiki": [{"role": "editor"}]
}
}
]
When a user in group:eng and group:docs connects, the wiki receives this list for example.com/cap/wiki:
[{"role": "reader"}, {"role": "reader"}, {"role": "editor"}]
These rules apply to the list:
- Tailscale appends values. Each capability name gets one list that holds every value from every matching grant.
- Tailscale keeps repeats.
{"role": "reader"}appears twice because two grants give it. - The values are in policy order. Tailscale does not move values from a broad grant behind values from a narrow grant.
- Different capability names stay separate. Tailscale never merges them.
Tailscale does not read the values or decide which value wins. The application decides. For example, Taildrive gives read-write access to a share when any value grants rw. The Kubernetes API server proxy combines the groups from every value.
An app grant opens no ports. Tailscale delivers the capabilities even when no grant lets the source reach the application. For an application that accepts connections on a port, the source also needs network access. A grant with an ip field that reaches that port gives it. Any such grant works. It does not need to match the app grant.
The following grant tells an identity provider that group:admins members are admins:
"grants": [
{
"src": ["group:admins"],
"dst": ["tag:idp"],
"app": {
"example.com/cap/idp": [{"role": "admin"}]
}
}
]
The following table shows the result for an admin with and without network access:
| Grants for the admin | Sign in to tag:idp | Reason |
|---|---|---|
The app grant | Blocked | No grant opens port 443, and the admin cannot ping tag:idp. |
The app grant, plus a grant from autogroup:member to tag:idp on tcp:443 | Allowed as admin | The second grant opens the port. The first grant tells the application that the user is an admin. |
Tailscale delivers capabilities to the destination, not to the source. The destination is the device that owns the destination address, or the device that hosts the destination Tailscale Service. Subnet routers do not pass capabilities along. An app grant that targets a subnet range, or that uses via, reaches no application.
Taildrop, Taildrive, and peer relays need each side to know about the other. When a grant gives one of these capabilities, Tailscale adds the matching capability in the reverse direction. You do not need a second grant.
Node attributes combine the same way. A device gets the app values from every nodeAttrs entry that targets it. Tailscale appends the values for each name, in file order, and keeps repeats. The device receives one combined result.
The following node attributes give two entries to devices with tag:server:
"nodeAttrs": [
{
"target": ["tag:server"],
"app": {
"example.com/cap/agent": [{"mode": "audit"}]
}
},
{
"target": ["*"],
"app": {
"example.com/cap/agent": [{"mode": "audit"}, {"mode": "report"}]
}
}
]
A device with tag:server receives [{"mode": "audit"}, {"mode": "audit"}, {"mode": "report"}] for example.com/cap/agent.
Where order matters
The order of grants does not change access. Order matters in three other places:
- The steps in an IP set run from first to last. Each step changes the result of the steps before it.
- Tailscale SSH rules have their own order. Refer to Tailscale SSH rules.
- App capability values arrive in policy order. If an application reads the first value, the order of grants changes what users can do.
The following IP sets have the same steps in a different order:
"ipsets": {
"ipset:office-a": ["add 192.0.2.0/24", "remove 192.0.2.5", "add 192.0.2.5"],
"ipset:office-b": ["add 192.0.2.0/24", "add 192.0.2.5", "remove 192.0.2.5"]
}
The following table shows the contents of each IP set:
| IP set | Contents | Reason |
|---|---|---|
ipset:office-a | All of 192.0.2.0/24 | The last step adds 192.0.2.5 back. |
ipset:office-b | 192.0.2.0/24 without 192.0.2.5 | The last step removes 192.0.2.5. The earlier add step has no effect. |
Policy validation and tests
Tailscale validates the whole policy file when you save it, and it runs the tests in the file. If anything is invalid or a test fails, the save fails, and the previous policy stays active. Tailscale never saves part of a file or skips an invalid grant.
Every group, tag, IP set, and host alias that a grant uses must have a definition. A defined name can be empty. It then matches nothing, and the save succeeds.
The following policy defines an empty group and a tag that no device has yet:
"tagOwners": {
"tag:staging": ["group:eng"]
},
"groups": {
"group:eng": ["alice@example.com"],
"group:contractors": []
},
"grants": [
{
"src": ["group:contractors"],
"dst": ["tag:staging"],
"ip": ["tcp:443"]
}
]
The following table shows what happens when you save a grant that uses each type of name:
| Name in a grant | Result on save |
|---|---|
A group that is not in groups | Error |
A tag that is not in tagOwners | Error |
An IP set that is not in ipsets | Error |
A host alias that is not in hosts | Error |
A defined group with no members, such as group:contractors | Saves. Matches nothing. |
A defined tag that no device has, such as tag:staging | Saves. Matches nothing until a device has the tag. |
| A user with no devices, or with no account | Saves. Matches nothing. |
Tailscale also rejects some conflicts when you save, such as two app connectors with the same name.
A test checks whether a source can reach a destination port. It uses the same combined result as real traffic, with every grant and converted acls entry added together.
The following tests use the posture and grants from Device posture in a union, with alice@example.com in group:eng:
"tests": [
{
"src": "alice@example.com",
"srcPostureAttrs": {
"node:os": "macos",
"custom:mdm": "enrolled"
},
"accept": ["tag:prod:22"]
},
{
"src": "alice@example.com",
"accept": ["tag:prod:443"],
"deny": ["tag:prod:22"]
}
]
Both tests pass. In the first test, Alice's device meets posture:managed, so the first grant opens port 22. In the second test, her device has no attributes. It loses the first grant and keeps port 443.
These rules apply to tests:
- A test uses its
srcPostureAttrsas the device attributes of the source. Without them, the source has no attributes and fails any posture that checks an attribute. - Tests check network access. They do not check
viarouting or app capabilities.
Relationships and context
Other parts of the tailnet policy file interact with grants. Tailscale checks SSH rules in order instead of adding them together, and the via field in a grant affects which subnet router a connection uses. Auto approvers and node attributes use the same additive model as grants.
Tailscale SSH rules
Tailscale SSH rules work like a queue, not a union. For each SSH connection, the destination checks rules in order. It stops at the first rule that matches both the source device and the requested login. The action of that rule decides. If a rule does not list the login, the destination moves to the next rule.
The position of a rule in the file does not set this order. Tailscale sorts SSH rules in this order:
- Recent checks. After a user passes a check, Tailscale accepts the same login from that device until the
checkPeriodends. checkrules.acceptrules.
Avoid overlapping rules of the same kind. When several check rules, or rules with different srcPosture conditions, match the same source, destination, and login, the rule that Tailscale applies might not be the one you expect. Neither the file order nor the shortest checkPeriod reliably decides it.
A Tailscale SSH connection also needs network access to port 22 from a grant.
The following SSH rules give group:eng two rules for tag:prod:
"ssh": [
{
"action": "accept",
"src": ["group:eng"],
"dst": ["tag:prod"],
"users": ["root", "autogroup:nonroot"]
},
{
"action": "check",
"src": ["group:eng"],
"dst": ["tag:prod"],
"users": ["root"],
"checkPeriod": "12h"
}
]
The following table shows the result for a user in group:eng:
Login on tag:prod | Result | Reason |
|---|---|---|
alice | Accept | The check rule comes first but does not list alice. The accept rule includes alice through autogroup:nonroot. |
root | Check | check rules come before accept rules, whatever their order in the file. |
root, again after a few minutes | Accept | The recent check comes first, until the 12-hour checkPeriod ends. |
These rules apply to SSH:
- Tailscale decides each login separately. A user can get
checkforrootandacceptfor their own account on the same device. - When an
acceptrule does not list a login, Tailscale tries the next rule. As a result, severalacceptrules combine in effect.
To require a check for a login, list that login in a check rule for the same source and destination. Moving an accept rule above it changes nothing. To let a login skip the check, remove it from every check rule that matches the same source and destination.
Subnet routers and the via field
Access to a subnet involves two separate decisions. First, Tailscale picks the subnet router that a device uses for a destination network. It picks one router for each network, for all ports. Second, the router that Tailscale picks checks every connection against the grants that it knows about, port by port.
The via field limits which routers one grant can use. A grant without via can use every router. Tailscale considers every router that any matching grant permits. Then it picks one router with its usual router selection. Tailscale does not know which grant you meant to steer. For via syntax, refer to route filtering with via.
In the following policy, router A (tag:router-a) and router B (tag:router-b) both advertise 10.0.2.0/24:
"grants": [
{
"src": ["group:eng"],
"dst": ["10.0.2.0/24"],
"ip": ["tcp:443"],
"via": ["tag:router-a"]
},
{
"src": ["group:eng"],
"dst": ["10.0.2.0/24"],
"ip": ["tcp:22"]
}
]
The second grant makes router B a valid choice. If Tailscale picks router B, connections to port 443 fail. Router B has no grant that permits port 443, because the first grant applies to router A. The first grant is valid, but traffic does not use it.
The following table shows common combinations. In each row, both routers advertise 10.0.2.0/24, and the source is the same device:
| Grants for the device | Routers that Tailscale can pick | Result |
|---|---|---|
One grant with via router A | Router A | Traffic uses router A. |
The same grant, plus a copy of it without via | Router A or router B | Traffic can use either router. Access works. |
One grant with via router A, and one grant with via router B | Router A or router B | The same result as one grant with both tags in via. |
via router A for port 443, and no via for port 22 | Router A or router B | If Tailscale picks router B, port 443 is blocked. |
For exit nodes, the user picks the exit node. The via field controls which exit nodes Tailscale offers to the user. A grant to autogroup:internet with via offers the tagged exit nodes. If you add a grant to autogroup:internet without via, Tailscale offers every exit node.
Because Tailscale checks posture for each grant, you can use posture and via together to route devices by compliance. In the following policy, managed devices can use any router, and other devices must use the office router:
"grants": [
{
"src": ["group:eng"],
"dst": ["10.0.2.0/24"],
"ip": ["*"],
"srcPosture": ["posture:managed"]
},
{
"src": ["group:eng"],
"dst": ["10.0.2.0/24"],
"ip": ["*"],
"via": ["tag:office-router"]
}
]
An unmanaged device fails the first grant, so the office router is the one router that it can use. A managed device has both grants, so every router is a valid choice. Tailscale picks one, which can be the office router or a different router.
Policy tests do not check routing. A test passes if any grant permits the traffic, including a grant with via. Tests do not model which router Tailscale picks or what that router permits. In the first example, a test for 10.0.2.15:443 passes, but real traffic can fail.
Auto approvers and node attributes
Auto approvers use the same additive model as grants. If any entry approves a subnet route, an exit node, or a Tailscale Service, Tailscale approves it. For a Tailscale Service, an entry can name the service, a tag on the service, or autogroup:tagged. Tailscale approves a device to host a service when the device is tagged and advertises the service.
Node attributes are also cumulative. A device gets every attribute from every nodeAttrs entry that targets it. For how app values in node attributes combine, refer to App capabilities from multiple grants.
Practical applications
An understanding of how grants combine changes how you design and review a policy file.
Require device posture for a resource
A posture condition applies to one grant, not to a resource. A device that fails the posture of one grant keeps access through every other grant that matches it. As a result, a resource requires posture only when every grant that gives access to it requires posture.
To require posture for a resource, find each grant and acls entry whose dst covers the resource, and add the posture to its srcPosture field. You can set defaultSrcPosture instead, but it applies to every grant in the policy file that has no srcPosture field, not only to grants for this resource. A grant with its own srcPosture list, including an empty list, does not use the default.
Tailscale cannot evaluate posture for devices shared from another tailnet or for addresses behind a subnet router, so these sources keep a grant whatever its posture. To exclude them, name users, groups, or tags in src instead of *.
Steer subnet traffic through specific routers
To steer traffic through specific routers, every grant from a source to the network must use the same via. Check grants with broad sources, such as autogroup:member or *, and grants for other ports. One grant for the same source and network without via makes every router a valid choice. A passing policy test does not prove that the steering works, so review the grants manually.
Read app capabilities in your own application
If you build an application that reads app capabilities, treat the list as a set of permissions to combine. A common approach is to use the most permissive value in the list. Expect repeats, and do not read the first value alone, because grant order then decides what users can do. Document each capability name and its parameters, because Tailscale does not validate them.
Further exploration
To build on this understanding:
- Grants syntax documents every field and selector that this topic uses.
- Grant examples show common access patterns that you can adapt for your tailnet.
- Troubleshooting grants helps you find the grant that causes unexpected access or a blocked connection.