{"record":{"id":"fe93ad3c46079e13","repo":"juanfont/headscale","slug":"w-s-use-autogroup-tagged-or-specific-tags-as","errorCode":null,"errorMessage":"%w (%s); use autogroup:tagged or specific tags as destinations instead","messagePattern":"%w \\((.+?)\\); use autogroup:tagged or specific tags as destinations instead","errorType":"validation","errorClass":"ErrSSHTagSourceToUserDest","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":2175,"sourceCode":"\t\t\tif v.Is(AutoGroupTagged) {\n\t\t\t\tsrcHasTaggedEntities = true\n\t\t\t} else if v.Is(AutoGroupMember) {\n\t\t\t\tsrcHasGroups = true // autogroup:member is like a group of users\n\t\t\t}\n\t\tcase *Group:\n\t\t\tsrcHasGroups = true\n\t\tcase *Username:\n\t\t\tsrcUsernames[string(*v)] = true\n\t\t}\n\t}\n\n\t// Check destinations against source constraints\n\tfor _, dst := range destinations {\n\t\tswitch v := dst.(type) {\n\t\tcase *Username:\n\t\t\t// Rule: Tags/autogroup:tagged CANNOT SSH to user destinations\n\t\t\tif srcHasTaggedEntities {\n\t\t\t\treturn fmt.Errorf(\"%w (%s); use autogroup:tagged or specific tags as destinations instead\",\n\t\t\t\t\tErrSSHTagSourceToUserDest, *v)\n\t\t\t}\n\t\t\t// Rule: Username destination requires source to be that same single user only\n\t\t\tif srcHasGroups || len(srcUsernames) != 1 || !srcUsernames[string(*v)] {\n\t\t\t\treturn fmt.Errorf(\"%w %q; use autogroup:self instead for same-user SSH access\",\n\t\t\t\t\tErrSSHUserDestRequiresSameUser, *v)\n\t\t\t}\n\t\tcase *AutoGroup:\n\t\t\t// Rule: autogroup:self requires source to NOT contain tags\n\t\t\tif v.Is(AutoGroupSelf) && srcHasTaggedEntities {\n\t\t\t\treturn ErrSSHAutogroupSelfRequiresUserSource\n\t\t\t}\n\t\t\t// Rule: autogroup:member (user-owned devices) cannot be accessed by tagged entities\n\t\t\tif v.Is(AutoGroupMember) && srcHasTaggedEntities {\n\t\t\t\treturn ErrSSHTagSourceToAutogroupMember\n\t\t\t}\n\t\t}\n\t}","sourceCodeStart":2157,"sourceCodeEnd":2193,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L2157-L2193","documentation":"Thrown by validateSSHSrcDstCombination (hscontrol/policy/v2/types.go:2176) during Policy.validate(). The SSH rule has at least one tagged source (a tag: alias or autogroup:tagged) while another entry in the same rule's destinations is a Username. Tailscale's security model forbids tagged devices from SSHing to user-owned devices — tags confer machine identity, not user identity, so a check-user session cannot be attributed. The message suggests autogroup:tagged or specific tags as the destination instead.","triggerScenarios":"An ssh rule like {\"src\": [\"tag:prod\", \"group:eng\"], \"dst\": [\"alice\"]} — any mix where srcHasTaggedEntities is true and dst contains a *Username. Also triggered when src includes autogroup:tagged and dst includes a username.","commonSituations":"Combining human and service sources in one SSH rule for brevity; migrating from older headscale/Tailscale versions before this combination check existed; writing a 'break-glass' rule with both tag and user sources.","solutions":["Split the rule: one SSH rule with tag/autogroup:tagged sources and tag/autogroup:tagged destinations, and a separate rule with user/group sources and the username destination","If the intent was same-user access from user-owned devices, use src=user/group + dst=autogroup:self"],"exampleFix":"// before\n\"ssh\": [{ \"action\": \"accept\", \"src\": [\"tag:prod\", \"group:eng\"], \"dst\": [\"alice\"], \"users\": [\"alice\"] }]\n\n// after\n\"ssh\": [\n  { \"action\": \"accept\", \"src\": [\"group:eng\"], \"dst\": [\"alice\"], \"users\": [\"alice\"] },\n  { \"action\": \"accept\", \"src\": [\"tag:prod\"], \"dst\": [\"tag:prod\"], \"users\": [\"root\"] }\n]","handlingStrategy":"validation","validationCode":"func sshRuleMixesTagsAndUserDst(s *policyv2.SSH) bool {\n    srcTagged, dstUser := false, false\n    for _, src := range s.Sources {\n        if _, ok := src.(*policyv2.Tag); ok { srcTagged = true }\n        if ag, ok := src.(*policyv2.AutoGroup); ok && ag.Is(policyv2.AutoGroupTagged) { srcTagged = true }\n    }\n    for _, dst := range s.Destinations { if _, ok := dst.(*policyv2.Username); ok { dstUser = true } }\n    return srcTagged && dstUser\n}","typeGuard":"func isTagAlias(s string) bool { return strings.HasPrefix(s, \"tag:\") }","tryCatchPattern":"if err := pol.Validate(); errors.Is(err, policyv2.ErrSSHTagSourceToUserDest) { /* split rule: tags→tag dst, users→user dst */ }","preventionTips":["Never mix tag sources and user destinations in one SSH rule — split it","Design rule of thumb: machine sources target machine destinations, user sources target user destinations"],"tags":["policy","ssh","tags","security-model","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}