[Feature] "grants" feature support #822

Open
opened 2025-12-29 02:24:26 +01:00 by adam · 9 comments
Owner

Originally created by @maxpain on GitHub (Oct 8, 2024).

Hello. Does Headscale support the new "grants" field in the ACL file?

https://tailscale.com/blog/acl-grants
https://tailscale.com/kb/1324/grants

Originally created by @maxpain on GitHub (Oct 8, 2024). Hello. Does Headscale support the new "grants" field in the ACL file? https://tailscale.com/blog/acl-grants https://tailscale.com/kb/1324/grants
adam added the enhancementno-stale-botpolicy 📝grants labels 2025-12-29 02:24:26 +01:00
Author
Owner

@akelge commented on GitHub (Nov 1, 2024):

AFAIK no, but that could be a great feature to add. In my use case, I would like to let groups of users use only a subset of all the exit nodes

@akelge commented on GitHub (Nov 1, 2024): AFAIK no, but that could be a great feature to add. In my use case, I would like to let groups of users use only a subset of all the exit nodes
Author
Owner

@kradalby commented on GitHub (Nov 12, 2024):

We do not, I imagine we will eventually work on it, but fixing the current acls (and other things) will be a priority so I would not expect this for a while.

@kradalby commented on GitHub (Nov 12, 2024): We do not, I imagine we will eventually work on it, but fixing the current acls (and other things) will be a priority so I would not expect this for a while.
Author
Owner

@ArcticLampyrid commented on GitHub (Oct 30, 2025):

Thank you for your reply, but I’d like to know whether all the efforts to fix ACLs will also be applicable to the future implementation of grants.
If I understand correctly, Tailscale treats grants as a replacement for the existing (legacy) ACLs.
In that case, would it be reasonable to skip implementing some of the legacy logic?

@ArcticLampyrid commented on GitHub (Oct 30, 2025): Thank you for your reply, but I’d like to know whether all the efforts to fix ACLs will also be applicable to the future implementation of grants. If I understand correctly, Tailscale treats grants as a replacement for the existing (legacy) ACLs. In that case, would it be reasonable to skip implementing some of the legacy logic?
Author
Owner

@cchance27 commented on GitHub (Oct 31, 2025):

That’s what I was coming to ask if grants replace legacy acl and grants are required to support all the new features shouldn’t that be pivoted to? I don’t know the internal ramifications tho

@cchance27 commented on GitHub (Oct 31, 2025): That’s what I was coming to ask if grants replace legacy acl and grants are required to support all the new features shouldn’t that be pivoted to? I don’t know the internal ramifications tho
Author
Owner

@Victrid commented on GitHub (Nov 4, 2025):

Hello, I'm currently looking into “grant” implementation, primarily aiming to support the new Peer-relay feature, which requires apps field under grant (#2841 ).

I've discovered that "ip" section is merely the same with ACL and can use the same logic. The FilterRules in tailcfg also include a CapGrant field, which appears to be directly usable.

I'm currently working on this section: the fork branch is located at https://github.com/Victrid/headscale/tree/grant.

My goal is to initially implement the capability for the app and ip fields. I'm not yet clear on how the Tailscale program handles via and postures fields, so I'm not planning to implement related operations at this stage. The current implementation can already accomplish Peer-Relay (https://github.com/juanfont/headscale/issues/2841, https://tailscale.com/kb/1591/peer-relays) (Unlike the official server, it requires manually adding the relay-target cap on the reverse side).

Details

Set the relay port on lighthouse servers

tailscale set --relay-server-port=40000

acl.json

{
...
    "tagOwners": {
        "tag:lighthouse": ["gix@"],
    },
    "grants": [ // My default grants
    {
      "src": ["*"],
      "dst": ["*"],
      "ip": ["*"]
    },
    { // The "Peer-Relay" option
      "src": ["*"],
      "dst": ["tag:lighthouse"],
      "app": {
        "tailscale.com/cap/relay": []
      }
    },
    {
      "src": ["tag:lighthouse"],
      "dst": ["*"],
      "app": {
        "tailscale.com/cap/relay-target": [] // Require this to be set manually
      }
    },
    ],
...
}

Using commit:

eab0396a15

I have a built docker image from Dockerfile.integration at victrid/headscale:git-eab0396a

Results

Image

At first glance, this part doesn't introduce any new external dependencies, should be incremental to current ACL, and doesn't appear to involve significant logical inconsistencies (at least according to the grant syntax migration guide).

If the maintainers are interested in this potential PR, I should be able to provide bugfix-level maintenance for this feature at least by the end of 2026.

@Victrid commented on GitHub (Nov 4, 2025): Hello, I'm currently looking into “grant” implementation, primarily aiming to support the new Peer-relay feature, which requires apps field under grant (#2841 ). I've discovered that "ip" section is merely the same with ACL and can use the same logic. The `FilterRules` in tailcfg also include a `CapGrant` field, which appears to be directly usable. I'm currently working on this section: the fork branch is located at https://github.com/Victrid/headscale/tree/grant. My goal is to initially implement the capability for the app and ip fields. I'm not yet clear on how the Tailscale program handles via and postures fields, so I'm not planning to implement related operations at this stage. The current implementation can already accomplish Peer-Relay (https://github.com/juanfont/headscale/issues/2841, https://tailscale.com/kb/1591/peer-relays) (Unlike the official server, it requires manually adding the `relay-target` cap on the reverse side). <details> <summary>Details</summary> ### Set the relay port on lighthouse servers ```bash tailscale set --relay-server-port=40000 ``` ### acl.json ```json { ... "tagOwners": { "tag:lighthouse": ["gix@"], }, "grants": [ // My default grants { "src": ["*"], "dst": ["*"], "ip": ["*"] }, { // The "Peer-Relay" option "src": ["*"], "dst": ["tag:lighthouse"], "app": { "tailscale.com/cap/relay": [] } }, { "src": ["tag:lighthouse"], "dst": ["*"], "app": { "tailscale.com/cap/relay-target": [] // Require this to be set manually } }, ], ... } ``` ### Using commit: https://github.com/Victrid/headscale/commit/eab0396a158a8696c38c8eafa58cd6dfda871896 I have a built docker image from `Dockerfile.integration` at `victrid/headscale:git-eab0396a` ### Results <img width="1504" height="254" alt="Image" src="https://github.com/user-attachments/assets/cd2535f7-17a7-4651-baf7-36c9a7a7fec1" /> </details> At first glance, this part doesn't introduce any new external dependencies, should be incremental to current ACL, and doesn't appear to involve significant logical inconsistencies (at least according to the grant syntax migration guide). If the maintainers are interested in this potential PR, I should be able to provide bugfix-level maintenance for this feature at least by the end of 2026.
Author
Owner

@cchance27 commented on GitHub (Nov 4, 2025):

Wow nicely done, looking over the code i'm a little shocked it was that easy to turn up lol

@cchance27 commented on GitHub (Nov 4, 2025): Wow nicely done, looking over the code i'm a little shocked it was that easy to turn up lol
Author
Owner

@Victrid commented on GitHub (Nov 7, 2025):

It seems that "via" (#2409 ) and "postures" (#2458 ) are likely should be implemented within the control node, which performs as a filter to current ACL rules.

It seems that the implementations of these two is similar: The posture implementation is quite straightforward (if don't need to support the 3rd party postures) when distributing NetMap, simply apply the posture rules to the source during filterForNode - ReduceFilterRules. Implementing via requires this along with corresponding filtering of routes broadcast by Peers when distributing Peer's info.

Implementing this with the current filtering method might introduce extra computing overhead. My plan is to use aliases (along with corresponding postures/vias) as indices to configure a 2-way reverse lookup set for each node and route, and rules. By this way, each group/tag only needs to be enumerated once.

Regarding the via rules, as described in #2865, my understanding is that we first need a rule allowing A to see B. Then, using autogroup:internet, A should then be able to obtain B's routes. This differs from the current implementation where, once A can connect to B, A immediately sees all of B's routes (#2852 , #2788?). In other words:

grant: {
  src = A,
  dst = B, C,
  ip = *
}

With this, A can see B as a peer, but B does not expose its routes:

A: 
Peers: [{ 
     {  B:
        “AllowedIPs”: [
                “100.64.0.2/32”
            ],}
     {  C:
        “AllowedIPs”: [
                “100.64.0.3/32”
			],}]

Then configure autogroup:internet to distribute exit node routes to A:

grant: [{
  src = A,
  dst = B, C,
  ip = *
},
{
  src = A,
  dst = “autogroup:internet”,
  ip = *
}]
A: 
Peers: [ 
     {  B:
        “AllowedIPs”: [
        “0.0.0.0/0”,
                “100.64.0.2/32”
            ],}
     {  C:
        “AllowedIPs”: [
        “0.0.0.0/0”,
                “100.64.0.3/32”
            ],}
            ]

Configuring via is to restrict the scope of routes distributed by exit nodes:

grant: [{
  src = A,
  dst = B, C,
  ip = *
},
{
  src = A,
  dst = “autogroup:internet”,
  via = B
  ip = *
}]

This configuration results in:

A: 
Peers: [
     {  B:
        “AllowedIPs”: [
        “0.0.0.0/0”,
                “100.64.0.2/32”
            ],}
     {  C:
        “AllowedIPs”: [ // No exit nodes!
                “100.64.0.3/32”
            ],}
            ]

If this filtering part is fixed, implementing via will also be very straightforward.

@Victrid commented on GitHub (Nov 7, 2025): It seems that "via" (#2409 ) and "postures" (#2458 ) are likely should be implemented within the control node, which performs as a filter to current ACL rules. It seems that the implementations of these two is similar: The `posture` implementation is quite straightforward (if don't need to support the 3rd party postures) when distributing NetMap, simply apply the posture rules to the source during `filterForNode` - `ReduceFilterRules`. Implementing `via` requires this along with corresponding filtering of routes broadcast by Peers when distributing Peer's info. Implementing this with the current filtering method might introduce extra computing overhead. My plan is to use aliases (along with corresponding postures/vias) as indices to configure a 2-way reverse lookup set for each node and route, and rules. By this way, each group/tag only needs to be enumerated once. Regarding the via rules, as described in #2865, my understanding is that we first need a rule allowing A to see B. Then, using `autogroup:internet`, A should then be able to obtain B's routes. This differs from the current implementation where, once A can connect to B, A immediately sees all of B's routes (#2852 , #2788?). In other words: ``` grant: { src = A, dst = B, C, ip = * } ``` With this, A can see B as a peer, but B does not expose its routes: ``` A: Peers: [{ { B: “AllowedIPs”: [ “100.64.0.2/32” ],} { C: “AllowedIPs”: [ “100.64.0.3/32” ],}] ``` Then configure `autogroup:internet` to distribute exit node routes to A: ``` grant: [{ src = A, dst = B, C, ip = * }, { src = A, dst = “autogroup:internet”, ip = * }] ``` ``` A: Peers: [ { B: “AllowedIPs”: [ “0.0.0.0/0”, “100.64.0.2/32” ],} { C: “AllowedIPs”: [ “0.0.0.0/0”, “100.64.0.3/32” ],} ] ``` Configuring `via` is to restrict the scope of routes distributed by exit nodes: ``` grant: [{ src = A, dst = B, C, ip = * }, { src = A, dst = “autogroup:internet”, via = B ip = * }] ``` This configuration results in: ``` A: Peers: [ { B: “AllowedIPs”: [ “0.0.0.0/0”, “100.64.0.2/32” ],} { C: “AllowedIPs”: [ // No exit nodes! “100.64.0.3/32” ],} ] ``` If this filtering part is fixed, implementing `via` will also be very straightforward.
Author
Owner

@ArcticLampyrid commented on GitHub (Nov 10, 2025):

If this filtering part is fixed, implementing via will also be very straightforward.

Just a note, via should also be available to sub routes.

// Example copied from official website
// The following example demonstrates a scenario in which the engineering team group can access a 192.0.2.0/24 using any available router if they comply with the latestMac posture (which ensures they are running the latest stable version of the Tailscale client for macOS). Anyone else (autogroup:member) can access 192.0.2.0/24 using the designated office router (tag:office-router).

"postures": {
    "posture:latestMac": [
        "node:os == 'macos'",
        "node:osVersion == '13.4.0'",
        "node:tsReleaseTrack == 'stable'",
    ]
},
"grants": [
    {
        "src": ["group:eng"],
        "srcPosture": ["posture:latestMac"],
        "dst": ["192.0.2.0/24"],
        "ip": ["*"],
    },
    {
        "src": ["autogroup:member"],
        "dst": ["192.0.2.0/24"],
        "via": ["tag:office-router"],
        "ip": ["*"],
    },
]

Combined with high availability features, some complex routing health checks and switching logic may be required.

@ArcticLampyrid commented on GitHub (Nov 10, 2025): > If this filtering part is fixed, implementing `via` will also be very straightforward. Just a note, `via` should also be available to sub routes. ```jsonc // Example copied from official website // The following example demonstrates a scenario in which the engineering team group can access a 192.0.2.0/24 using any available router if they comply with the latestMac posture (which ensures they are running the latest stable version of the Tailscale client for macOS). Anyone else (autogroup:member) can access 192.0.2.0/24 using the designated office router (tag:office-router). "postures": { "posture:latestMac": [ "node:os == 'macos'", "node:osVersion == '13.4.0'", "node:tsReleaseTrack == 'stable'", ] }, "grants": [ { "src": ["group:eng"], "srcPosture": ["posture:latestMac"], "dst": ["192.0.2.0/24"], "ip": ["*"], }, { "src": ["autogroup:member"], "dst": ["192.0.2.0/24"], "via": ["tag:office-router"], "ip": ["*"], }, ] ``` Combined with high availability features, some complex routing health checks and switching logic may be required.
Author
Owner

@kradalby commented on GitHub (Nov 11, 2025):

I just want to chime in with a few thoughts, some not researched yet, but mostly to manage expectations.

I think it is important to separate what grant support means, I would say it is two things:

  1. A new, more modern syntax
  2. Unblock the implementation of new features:

of the top of my head.

As it seems to have been discovered here, implementing 1. should be fairly straight forward. It is more or less a way different way to represent the current acls with a slightly different syntax.

However, I suspect that each feature under 2. vary quite a lot in the complexity and while they are all doable over time, I do not think that "grants support" should include all these features.

I think we need to ask ourselves, "what is the minimum value we can get from grants over the acls" and I think the answer to that is app capabilities. So I would argue that we should focus on implementing the new grants syntax and app capabilities and then work from there.

As for a little implementation detail. We have a lot (but not enough) policy tests that are currently running against the ACLs. I think that the most valuable part to implementing grants would be to implement a converter for our current acls to grants so we can have the tests run fully as both ACLs and as Grants. That should allow us to keep acls around, and have great test coverage.

I imagine something like:

  1. Implement grant types (and reuse what can be reused) in policy v2.
  2. Implement a converter of ACL rules to Grants
  3. Remove all specific ACL types, fully relying on Grants
  4. Add app capability support

I was discussing with @nblock some weeks ago that we probably would want to rework the roadmap a bit, and likely swap the features in 0.29.0 and 0.31.0, moving CLI/API changes back two releases and making grants a priority for 0.29.0.

@kradalby commented on GitHub (Nov 11, 2025): I just want to chime in with a few thoughts, some not researched yet, but mostly to manage expectations. I think it is important to separate what grant support means, I would say it is two things: 1. A new, more modern syntax 2. Unblock the implementation of new features: - [`app` or capabilities](https://tailscale.com/kb/1537/grants-app-capabilities) - `via` https://tailscale.com/kb/1378/via - [`srcPosture`](https://tailscale.com/kb/1288/device-posture) (but this could already have been done in `acls` - Maybe even `ipsets` https://tailscale.com/kb/1387/ipsets - IP Pool (https://github.com/juanfont/headscale/issues/2912) of the top of my head. As it seems to have been discovered here, implementing 1. should be fairly straight forward. It is _more or less_ a way different way to represent the current `acls` with a slightly different syntax. However, I suspect that each feature under 2. vary quite a lot in the complexity and while they are all doable over time, I do not think that "grants support" should include all these features. I think we need to ask ourselves, "what is the minimum value we can get from `grants` over the `acls`" and I think the answer to that is app capabilities. So I would argue that we should focus on implementing the new grants syntax _and_ app capabilities and then work from there. As for a little implementation detail. We have _a lot_ (but not enough) policy tests that are currently running against the ACLs. I think that the most valuable part to implementing grants would be to implement a converter for our current `acls` to `grants` so we can have the tests run fully as both ACLs and as Grants. That should allow us to keep acls around, and have great test coverage. I imagine something like: 1. Implement grant types (and reuse what can be reused) in policy v2. 2. Implement a converter of ACL rules to Grants 3. Remove all specific ACL types, fully relying on Grants 4. Add app capability support I was discussing with @nblock some weeks ago that we probably would want to rework the roadmap a bit, and likely swap the features in 0.29.0 and 0.31.0, moving CLI/API changes back two releases and making grants a priority for 0.29.0.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/headscale#822