{"record":{"id":"d30dd59c96b3e22f","repo":"juanfont/headscale","slug":"src-w","errorCode":null,"errorMessage":"src=%w","messagePattern":"src=%w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":2371,"sourceCode":"\n\t\t\t\terr = validateAutogroupForSrc(ag)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase *Group:\n\t\t\t\tg := src\n\n\t\t\t\terr := p.Groups.Contains(g)\n\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\ttagOwner := src\n\n\t\t\t\terr := p.TagOwners.Contains(tagOwner)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\"src=%w\", err))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor _, dst := range acl.Destinations {\n\t\t\tswitch h := dst.Alias.(type) {\n\t\t\tcase *Host:\n\t\t\t\tif !p.Hosts.exist(*h) {\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\"%w: %q\", ErrHostNotDefined, *h))\n\t\t\t\t}\n\t\t\tcase *AutoGroup:\n\t\t\t\terr := validateAutogroupSupported(h)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\terr = validateAutogroupForDst(h)","sourceCodeStart":2353,"sourceCodeEnd":2389,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L2353-L2389","documentation":"Thrown in Policy.validate() (hscontrol/policy/v2/types.go:2371) when a tag: alias used as an ACL source is not defined under tagOwners. The underlying error comes from p.TagOwners.Contains(tagOwner) and is wrapped with the prefix \"src=\" so the combined message reads like 'src=tag not found: \"tag:foo\"' (or the Contains wording), distinguishing source-position failures from destination ones. This enforces that only tags with declared owners can appear in ACLs.","triggerScenarios":"An ACL entry with \"src\": [\"tag:prod\"] where \"prod\" is not a key in the policy's tagOwners map. Note \"autogroup:tagged\" is fine — this fires only for concrete tags.","commonSituations":"Deleting a tagOwners entry but leaving the tag in acls; introducing a new tag in ACLs before registering its owners; inconsistent naming (tag:Prod vs tag:prod — matching is case-sensitive).","solutions":["Add the tag to tagOwners, e.g. \"tagOwners\": { \"tag:prod\": [\"group:admin\"] }","Remove the tag from the ACL source if it is obsolete","Verify exact casing and the tag: prefix — the %q in the message shows what was looked up"],"exampleFix":"// before\n\"acls\": [{ \"action\": \"accept\", \"src\": [\"tag:prod\"], \"dst\": [\"*\"] }]\n\n// after\n\"tagOwners\": { \"tag:prod\": [\"group:admin\"] },\n\"acls\": [{ \"action\": \"accept\", \"src\": [\"tag:prod\"], \"dst\": [\"*\"] }]","handlingStrategy":"validation","validationCode":"func aclSrcTagsDeclared(p *policyv2.Policy) error {\n    for _, acl := range p.ACLs {\n        for _, src := range acl.Sources {\n            if t, ok := src.(*policyv2.Tag); ok {\n                if err := p.TagOwners.Contains(t); err != nil { return err }\n            }\n        }\n    }\n    return nil\n}","typeGuard":"func isTag(s string) bool { return strings.HasPrefix(s, \"tag:\") }","tryCatchPattern":"if err := pol.Validate(); strings.Contains(err.Error(), \"src=\") { /* check src tag spelling against tagOwners keys */ }","preventionTips":["Every tag referenced anywhere (src, dst, via) must be a tagOwners key — register new tags in the same commit","Tag matching is case-sensitive; standardize on lowercase"],"tags":["policy","acl","tags","tagowners","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}