{"record":{"id":"2ba51bd92b850006","repo":"juanfont/headscale","slug":"tag-q-not-found","errorCode":null,"errorMessage":"tag %q not found","messagePattern":"tag %q not found","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":2654,"sourceCode":"\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\tcase *Tag:\n\t\t\t\terr := p.TagOwners.Contains(h)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Validate via tags. Wording matches Tailscale SaaS\n\t\t// (\"tag %q not found\"), which differs from the ACL-src\n\t\t// wording (\"src=tag not found: %q\").\n\t\tfor _, viaTag := range grant.Via {\n\t\t\terr := p.TagOwners.Contains(&viaTag)\n\t\t\tif err != nil {\n\t\t\t\t//nolint:err113 // SaaS-aligned dynamic phrasing; no caller does errors.Is.\n\t\t\t\terrs = append(errs, fmt.Errorf(\"tag %q not found\", viaTag))\n\t\t\t}\n\t\t}\n\n\t\t// Validate grant-specific source/destination combinations.\n\t\t// Grants are stricter than ACLs: wildcard (*) src with autogroup:self\n\t\t// dst is rejected because * includes tags, and tags cannot use\n\t\t// autogroup:self.\n\t\terr := validateGrantSrcDstCombination(grant.Sources, grant.Destinations)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tfor _, na := range p.NodeAttrs {\n\t\t// SaaS accepts entries with neither attr nor ipPool (they\n\t\t// compile to a no-op); headscale follows suit so policies\n\t\t// captured against SaaS round-trip cleanly.\n\t\tfor _, target := range na.Targets {","sourceCodeStart":2636,"sourceCodeEnd":2672,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L2636-L2672","documentation":"Thrown in Policy.validate()'s grants loop (hscontrol/policy/v2/types.go:2654) when a via tag (\"via\" array of a grant, used for subnet-route through-tag matching) is not declared in tagOwners. The wording 'tag %q not found' deliberately matches Tailscale SaaS (per the comment at :2650) and differs from the ACL wording; it is a dynamic message with no sentinel error, so callers cannot use errors.Is for it — a comment marks this intentional (nolint:err113).","triggerScenarios":"A grants entry with \"via\": [\"egress\"] where \"egress\" is not a tagOwners key. Via tags steer traffic through nodes bearing the tag (subnet routers), so an undeclared tag can never match a node.","commonSituations":"Configuring 4via6/subnet-router grants and forgetting to register the router tag; renaming router tags in tagOwners without updating via references; assuming via does lookup in hosts or DNS rather than tagOwners.","solutions":["Register the tag in tagOwners, e.g. \"tag:egress\": [\"group:netops\"], and ensure the subnet-router node actually carries that tag","Remove the via entry if the tag is retired","Because there is no sentinel, match this failure by the 'tag %q not found' message prefix or simply read the joined validation output"],"exampleFix":"// before\n\"grants\": [{ \"src\": [\"group:eng\"], \"via\": [\"egress\"], \"dst\": [\"10.0.0.0/8\"], \"ip\": [\"tcp/80\"] }]\n\n// after\n\"tagOwners\": { \"tag:egress\": [\"group:netops\"] },\n\"grants\": [{ \"src\": [\"group:eng\"], \"via\": [\"egress\"], \"dst\": [\"10.0.0.0/8\"], \"ip\": [\"tcp/80\"] }]","handlingStrategy":"validation","validationCode":"func grantViaTagsDeclared(p *policyv2.Policy) []string {\n    var missing []string\n    for _, g := range p.Grants {\n        for _, via := range g.Via {\n            if err := p.TagOwners.Contains(&via); err != nil { missing = append(missing, string(via)) }\n        }\n    }\n    return missing\n}","typeGuard":"func isViaTag(s string) bool { return !strings.ContainsAny(s, \":/\") && net.ParseIP(s) == nil } // via entries are bare tag names","tryCatchPattern":"// No sentinel exists (SaaS-aligned dynamic message); match textually.\nif err := pol.Validate(); strings.Contains(err.Error(), \"tag \\\"\" /* + name */ + \"\\\" not found\") { /* register tag in tagOwners */ }","preventionTips":["Via tags are bare names (no tag: prefix) but must still exist in tagOwners","A via tag must also be applied to a real subnet-router node or no traffic matches"],"tags":["policy","grants","via","tags","tagowners","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}