From edad4ff324f27b87ff387ab6749b2f2824aa4052 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 31 Oct 2026 14:06:30 +0000 Subject: [PATCH] types: do not stamp is-admin on tagged nodes A tagged node has no user to hold the role. SaaS leaves it out unless policy grants it. Stop stripping the cap, so captures now compare it. --- hscontrol/policy/v2/tailnet_state_caps.go | 17 ++++------------- hscontrol/types/node.go | 7 ++++++- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/hscontrol/policy/v2/tailnet_state_caps.go b/hscontrol/policy/v2/tailnet_state_caps.go index 97cf18441..bf5406780 100644 --- a/hscontrol/policy/v2/tailnet_state_caps.go +++ b/hscontrol/policy/v2/tailnet_state_caps.go @@ -66,19 +66,10 @@ func PeerCapMap(peer types.NodeView, peerSelfCaps tailcfg.NodeCapMap) tailcfg.No var unmodelledTailnetStateCaps = []nodecap.Cap{ // --- 1. User-role gated --- - // [tailcfg.CapabilityAdmin]: the hosted control plane stamps this - // on nodes whose owning user has the admin role; tagged nodes - // inherit from a tagOwner with the role. Headscale has no - // user-role model — [types.Node.TailNode] emits it as part of - // the always-on baseline. Stripping on both sides keeps the diff - // from failing on every user-owned non-admin node in a capture. - // Long-term fix is autogroup:admin support. - nodecap.Admin, - - // [tailcfg.CapabilityOwner]: same shape as is-admin, conditional - // on the "owner" role rather than admin. Headscale does not emit - // this cap at all. autogroup:owner support is tracked under - // NO_USER_ROLES — see the compat skip list. + // [tailcfg.CapabilityOwner]: conditional on the "owner" role, which + // headscale does not model, so it does not emit this cap at all. + // autogroup:owner support is tracked under NO_USER_ROLES — see the + // compat skip list. nodecap.Owner, // --- 2. Feature not implemented --- diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index b42ea6573..84ba4dbc5 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -1429,11 +1429,16 @@ func (nv NodeView) TailNode( // cfg.DNSConfig.Certificates.Enabled gates HTTPS, without which // `tailscale serve` refuses to serve HTTPS. capMap := tailcfg.NodeCapMap{ - nodecap.Admin: []tailcfg.RawMessage{}, nodecap.SSH: []tailcfg.RawMessage{}, nodecap.SSHEnvironmentVariables: []tailcfg.RawMessage{}, } + // Headscale has no user roles, so every user counts as an admin. A tagged + // node has no user to hold the role, and SaaS does not stamp it either. + if !nv.IsTagged() { + capMap[nodecap.Admin] = []tailcfg.RawMessage{} + } + if cfg.Taildrop.Enabled { capMap[nodecap.FileSharing] = []tailcfg.RawMessage{} }