Skip to content

Commit c7df15d

Browse files
committed
policy,state: authorize reauth tags against the authenticating user
Re-authenticating an already-tagged node with --advertise-tags was always rejected: tag authorization asked whether the NODE may hold the tag, but a tag-owned node has no user and its IP is in no owner set, so every tag was refused and --force-reauth left the node logged out. Authorize each requested tag against the authenticating user as well, via a new PolicyManager.UserCanHaveTag. Both reauth check sites are fixed - the pre-check in applyAuthNodeUpdate and the apply-time re-check in processReauthTags whose rejection was discarded - so a permitted tag is actually applied, not silently dropped. Tags the user does not own are still rejected. Also clear the auth-key reference when a reauth untags a node: a node created by a tagged ephemeral key would otherwise stay ephemeral after converting to user-owned and be garbage-collected on disconnect. The cleared reference is persisted (writing NULL is FK-safe). Fixes #3374
1 parent e10844d commit c7df15d

5 files changed

Lines changed: 546 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ keys remain all-access.
4646
- Improve systemd service file hardening [#3341](https://github.com/juanfont/headscale/pull/3341)
4747
- A tagged node can re-authenticate after `tailscale logout` again: logout no longer stamps a key expiry on a tagged node, and a stale expiry left by an older version is cleared on re-registration [#3371](https://github.com/juanfont/headscale/issues/3371)
4848
- Re-registering a tagged node with a different pre-auth key now applies the new key's tags instead of silently keeping the old ones, and persists the new key reference so a node's ephemeral status no longer reverts on restart [#3370](https://github.com/juanfont/headscale/issues/3370)
49+
- An already-tagged node can re-authenticate with `--advertise-tags` again: requested tags are now authorized against the authenticating user, not the tag-owned node's absent user [#3374](https://github.com/juanfont/headscale/issues/3374)
4950

5051
## 0.29.2 (2026-07-01)
5152

hscontrol/policy/pm.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ type PolicyManager interface {
3030
// NodeCanHaveTag reports whether the given node can have the given tag.
3131
NodeCanHaveTag(node types.NodeView, tag string) bool
3232

33+
// UserCanHaveTag reports whether the given user owns the given tag, i.e.
34+
// is listed (directly or via a group) in the tag's tagOwners. This is the
35+
// user half of NodeCanHaveTag, used to authorise re-auth tag changes
36+
// against the authenticating user rather than the node's stale ownership.
37+
UserCanHaveTag(user types.UserView, tag string) bool
38+
3339
// TagExists reports whether the given tag is defined in the policy.
3440
TagExists(tag string) bool
3541

hscontrol/policy/v2/policy.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,37 @@ func (pm *PolicyManager) NodeCanHaveTag(node types.NodeView, tag string) bool {
972972
return false
973973
}
974974

975+
// UserCanHaveTag reports whether the given user is one of the tag's owners
976+
// (directly or via a group). It is the user half of [PolicyManager.NodeCanHaveTag]:
977+
// re-authentication authorises requested tags against the authenticating user,
978+
// because a tag-owned node carries no user and its IP is not in any owner set,
979+
// so only the user presenting the credential can prove ownership.
980+
func (pm *PolicyManager) UserCanHaveTag(user types.UserView, tag string) bool {
981+
if pm == nil || !user.Valid() {
982+
return false
983+
}
984+
985+
pm.mu.RLock()
986+
defer pm.mu.RUnlock()
987+
988+
if pm.pol == nil {
989+
return false
990+
}
991+
992+
owners, exists := pm.pol.TagOwners[Tag(tag)]
993+
if !exists {
994+
return false
995+
}
996+
997+
for _, owner := range owners {
998+
if pm.userMatchesOwner(user, owner) {
999+
return true
1000+
}
1001+
}
1002+
1003+
return false
1004+
}
1005+
9751006
// TagOwnedByTags reports whether a credential holding ownerTags is authorised to
9761007
// apply tag. It is true when tag is one of ownerTags, or when tag's tagOwners
9771008
// chain (tag-to-tag ownership) transitively includes one of ownerTags. This is

0 commit comments

Comments
 (0)