{"record":{"id":"32400de3a514e685","repo":"juanfont/headscale","slug":"w-q-use-autogroup-self-instead-for-same-user-ss","errorCode":null,"errorMessage":"%w %q; use autogroup:self instead for same-user SSH access","messagePattern":"%w %q; use autogroup:self instead for same-user SSH access","errorType":"validation","errorClass":"ErrSSHUserDestRequiresSameUser","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":2180,"sourceCode":"\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}\n\n\treturn nil\n}\n\n// validateACLSrcDstCombination validates that [ACL] source/destination combinations","sourceCodeStart":2162,"sourceCodeEnd":2198,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L2162-L2198","documentation":"Thrown by validateSSHSrcDstCombination (hscontrol/policy/v2/types.go:2181) during Policy.validate(). A Username SSH destination is only valid when the source list contains exactly one username, it is that same user, and there are no groups (or tags — those hit the sibling error first). This mirrors Tailscale's rule that user destinations mean 'this exact user's own devices', expressible more idiomatically as dst=autogroup:self.","triggerScenarios":"An ssh rule with dst containing \"alice\" but src being [\"bob\"], [\"group:eng\"], or [\"alice\", \"bob\"] (multiple usernames), or any src mixing a group with usernames. srcHasGroups or len(srcUsernames) != 1 or the username not present triggers it.","commonSituations":"Trying to grant admin SSH to another user's devices (not allowed by the model); listing several users in src for convenience; translating 'user X can SSH to user X's machines' literally instead of using autogroup:self.","solutions":["If the intent is same-user SSH (user reaches their own devices), use src [\"username\"] or a group and dst [\"autogroup:self\"]","If the intent is cross-user access, it is not supported for Username destinations — target the user's tagged devices instead, or have the user share access via tags","Ensure src contains exactly one username equal to the dst username with no groups mixed in"],"exampleFix":"// before\n\"ssh\": [{ \"action\": \"accept\", \"src\": [\"group:eng\"], \"dst\": [\"alice\"], \"users\": [\"alice\"] }]\n\n// after\n\"ssh\": [{ \"action\": \"accept\", \"src\": [\"group:eng\"], \"dst\": [\"autogroup:self\"], \"users\": [\"autogroup:nonroot\", \"alice\"] }]","handlingStrategy":"validation","validationCode":"func userDstHasSameSingleUser(s *policyv2.SSH) bool {\n    var users []string; hasGroup := false\n    for _, src := range s.Sources {\n        switch v := src.(type) {\n        case *policyv2.Username: users = append(users, string(*v))\n        case *policyv2.Group: hasGroup = true\n        }\n    }\n    if hasGroup || len(users) != 1 { return false }\n    for _, dst := range s.Destinations {\n        if u, ok := dst.(*policyv2.Username); ok && string(*u) != users[0] { return false }\n    }\n    return true\n}","typeGuard":null,"tryCatchPattern":"if err := pol.Validate(); errors.Is(err, policyv2.ErrSSHUserDestRequiresSameUser) { /* replace username dst with autogroup:self */ }","preventionTips":["Default to autogroup:self for 'user reaches own devices'; username dst is almost always wrong","Cross-user access must go through tags, not username destinations"],"tags":["policy","ssh","autogroup-self","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}