{"record":{"id":"9bde0e7bb21b9894","repo":"juanfont/headscale","slug":"tag-owner-is-not-an-alias","errorCode":null,"errorMessage":"tag owner is not an Alias","messagePattern":"tag owner is not an Alias","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/policy.go","lineNumber":29,"sourceCode":"\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/policy/matcher\"\n\t\"github.com/juanfont/headscale/hscontrol/policy/policyutil\"\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"github.com/puzpuzpuz/xsync/v4\"\n\t\"github.com/rs/zerolog/log\"\n\t\"go4.org/netipx\"\n\t\"tailscale.com/net/tsaddr\"\n\t\"tailscale.com/tailcfg\"\n\t\"tailscale.com/types/views\"\n\t\"tailscale.com/util/deephash\"\n\t\"tailscale.com/util/multierr\"\n)\n\n// ErrInvalidTagOwner is returned when a tag owner is not an [Alias] type.\nvar ErrInvalidTagOwner = errors.New(\"tag owner is not an Alias\")\n\ntype PolicyManager struct {\n\t// RWMutex, not Mutex, so concurrent map generation does not serialise on\n\t// reads. The per-node caches are xsync.Maps so a read can fill them without\n\t// taking the write lock.\n\tmu    sync.RWMutex\n\tpol   *Policy\n\tusers []types.User\n\tnodes views.Slice[types.NodeView]\n\n\tfilterHash deephash.Sum\n\tfilter     []tailcfg.FilterRule\n\tmatchers   []matcher.Match\n\n\ttagOwnerMapHash deephash.Sum\n\ttagOwnerMap     map[Tag]*netipx.IPSet\n\n\texitSetHash        deephash.Sum","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/policy.go#L11-L47","documentation":"Returned while resolving tag owners (hscontrol/policy/v2/policy.go:1750-1752) when a flattened tag owner is neither a Username, Group, nor any other Alias type. The code comment marks it 'Should never happen' — after flattening, all owners must be Alias implementations — so hitting it indicates an internal invariant break, not a user config error.","triggerScenarios":"A tagOwners map entry resolves to a non-Alias value at compile time. In the current tree this requires a code path that inserts a non-Alias owner (e.g. a new owner type added without updating the flatten step) — no valid policy file can trigger it directly.","commonSituations":"Custom forks adding new alias/owner types to policy/v2; headscale version skew where policy structs changed; effectively unreachable on stock releases.","solutions":["If running a stock release, report it upstream with the full policy — it is a bug","In forks: ensure every owner added to tagOwners is flattened into an Alias before resolveTagOwners runs, and add the case to the switch"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if errors.Is(err, ErrInvalidTagOwner) { // internal invariant break: capture the policy and report upstream; do not attempt config-side fixes }","preventionTips":["In forks, add a unit test asserting every owner type flattens to Alias before resolveTagOwners","Run policy tests when vendoring new headscale versions"],"tags":["policy","internal","invariant","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}