{"record":{"id":"883b17bda3034ffd","repo":"juanfont/headscale","slug":"user-q-w","errorCode":null,"errorMessage":"user %q %w","messagePattern":"user %q %w","errorType":"validation","errorClass":"ErrSSHUserInvalid","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":2435,"sourceCode":"\t}\n\n\tfor _, ssh := range p.SSHs {\n\t\t// Empty action and users survive parse; surface them here.\n\t\tif ssh.Action == \"\" {\n\t\t\terrs = append(errs, ErrSSHActionMustBeSpecified)\n\t\t}\n\n\t\tif len(ssh.Users) == 0 {\n\t\t\terrs = append(errs, ErrSSHUsersMustBeSpecified)\n\t\t}\n\n\t\t// \"\" and \"*\" are not valid login users; any other string\n\t\t// (including autogroup, group, tag, malformed localpart) is\n\t\t// treated as a literal user name.\n\t\tfor _, user := range ssh.Users {\n\t\t\tswitch user {\n\t\t\tcase \"\", \"*\":\n\t\t\t\terrs = append(errs, fmt.Errorf(\"user %q %w\", user, ErrSSHUserInvalid))\n\t\t\t}\n\t\t}\n\n\t\t// acceptEnv entries cannot be empty; \"*\" and \"**\" are valid.\n\t\tfor _, env := range ssh.AcceptEnv {\n\t\t\tif env == \"\" {\n\t\t\t\terrs = append(errs, ErrSSHAcceptEnvEmpty)\n\t\t\t}\n\t\t}\n\n\t\tfor _, src := range ssh.Sources {\n\t\t\tswitch src := src.(type) {\n\t\t\tcase *AutoGroup:\n\t\t\t\tag := src\n\n\t\t\t\terr := validateAutogroupSupported(ag)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, err)","sourceCodeStart":2417,"sourceCodeEnd":2453,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L2417-L2453","documentation":"Thrown in Policy.validate()'s SSH loop (hscontrol/policy/v2/types.go:2435) when a value in an ssh rule's users array is the empty string \"\" or \"*\". The sentinel ErrSSHUserInvalid reads \"is not valid\", producing e.g. `user \"*\" is not valid`. Empty means no login user; \"*\" as a check-user wildcard is not supported — any other string (including group:, autogroup:, or a malformed localpart) is treated as a literal login name and passes this check.","triggerScenarios":"An ssh rule with \"users\": [\"\"] (usually a trailing comma or an empty template slot in HuJSON) or \"users\": [\"*\"] intending 'any login user'.","commonSituations":"Generating policies with templates that leave an empty users entry; assuming \"*\" means all users as in some firewall CLIs; hand-editing HuJSON and leaving a dangling comma inside the users array.","solutions":["Replace \"*\" with the concrete login usernames to allow (e.g. \"root\", \"alice\", \"autogroup:nonroot\")","Remove empty-string entries — usually a stray comma or unfilled template variable","If the goal is non-root access for the connecting user, use \"autogroup:nonroot\" which is a valid literal here"],"exampleFix":"// before\n\"ssh\": [{ \"action\": \"accept\", \"src\": [\"group:eng\"], \"dst\": [\"autogroup:self\"], \"users\": [\"*\"] }]\n\n// after\n\"ssh\": [{ \"action\": \"accept\", \"src\": [\"group:eng\"], \"dst\": [\"autogroup:self\"], \"users\": [\"autogroup:nonroot\", \"alice\"] }]","handlingStrategy":"validation","validationCode":"func sshUsersValid(p *policyv2.Policy) error {\n    for _, s := range p.SSH {\n        for _, u := range s.Users {\n            if u == \"\" || u == \"*\" { return fmt.Errorf(\"ssh user %q invalid\", u) }\n        }\n    }\n    return nil\n}","typeGuard":"func isValidSSHUser(u string) bool { return u != \"\" && u != \"*\" }","tryCatchPattern":"if err := pol.Validate(); errors.Is(err, policyv2.ErrSSHUserInvalid) { /* list explicit login names or autogroup:nonroot */ }","preventionTips":["No wildcard for ssh users — enumerate names or use autogroup:nonroot","Guard policy templates against empty users slots (trailing commas in HuJSON)"],"tags":["policy","ssh","users","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}