{"record":{"id":"211902f7140082f6","repo":"juanfont/headscale","slug":"dst-q-w","errorCode":null,"errorMessage":"dst=%q: %w","messagePattern":"dst=%q: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":2402,"sourceCode":"\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)\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\terr := p.Groups.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\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, fmt.Errorf(\"dst=%q: %w\", *h, err))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Validate protocol-port compatibility\n\t\tif err := validateProtocolPortCompatibility(acl.Protocol, acl.Destinations); err != nil { //nolint:noinlineerr\n\t\t\terrs = append(errs, err)\n\t\t}\n\n\t\t// Validate ACL source/destination combinations follow Tailscale's security model\n\t\terr := validateACLSrcDstCombination(acl.Sources, acl.Destinations)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tfor _, ssh := range p.SSHs {\n\t\t// Empty action and users survive parse; surface them here.","sourceCodeStart":2384,"sourceCodeEnd":2420,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L2384-L2420","documentation":"Thrown in Policy.validate() (hscontrol/policy/v2/types.go:2402) when a tag: alias used as an ACL destination is not declared in tagOwners. The error from p.TagOwners.Contains(h) is wrapped as \"dst=%q: %w\" with the offending tag interpolated, so the message pinpoints which destination alias failed. Tag destinations must be ownable/declared even though ownership matters mostly for claiming nodes.","triggerScenarios":"An ACL entry with \"dst\": [\"tag:db:5432\"] where \"db\" is absent from tagOwners. The parallel src-side check produces the 'src=' wording instead.","commonSituations":"Adding a new service tag to dst but forgetting tagOwners; cleaning up tagOwners and missing one ACL dst reference; casing mismatch between tagOwners key and ACL dst.","solutions":["Declare the tag under tagOwners with an owner list (group or user)","Remove the stale dst tag from the ACL","Double-check exact tag spelling/case against the %q value in the message"],"exampleFix":"// before\n\"acls\": [{ \"action\": \"accept\", \"src\": [\"group:app\"], \"dst\": [\"tag:db:5432\"] }]\n\n// after\n\"tagOwners\": { \"tag:db\": [\"group:admin\"] },\n\"acls\": [{ \"action\": \"accept\", \"src\": [\"group:app\"], \"dst\": [\"tag:db:5432\"] }]","handlingStrategy":"validation","validationCode":"func aclDstTagsDeclared(p *policyv2.Policy) error {\n    for _, acl := range p.ACLs {\n        for _, d := range acl.Destinations {\n            if t, ok := d.Alias.(*policyv2.Tag); ok {\n                if err := p.TagOwners.Contains(t); err != nil { return fmt.Errorf(\"dst=%q: %w\", *t, 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(), \"dst=\") { /* register the dst tag in tagOwners */ }","preventionTips":["Run `headscale policy check` (or your validate call) in CI so tag refs are verified before apply","The dst= prefix in the message names the exact failing tag — fix that one first, errors are aggregated"],"tags":["policy","acl","tags","tagowners","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}