{"record":{"id":"c9f4475f0a20d6ce","repo":"juanfont/headscale","slug":"is-not-valid","errorCode":null,"errorMessage":"is not valid","messagePattern":"is not valid","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":52,"sourceCode":"const Wildcard = Asterix(0)\n\nvar ErrAutogroupSelfRequiresPerNodeResolution = errors.New(\"autogroup:self requires per-node resolution and cannot be resolved in this context\")\n\nvar ErrUndefinedTagReference = errors.New(\"references undefined tag\")\n\n// SSH validation errors.\nvar (\n\tErrSSHTagSourceToUserDest             = errors.New(\"tags in SSH source cannot access user-owned devices\")\n\tErrSSHUserDestRequiresSameUser        = errors.New(\"user destination requires source to contain only that same user\")\n\tErrSSHAutogroupSelfRequiresUserSource = errors.New(\"autogroup:self destination requires source to contain only users or groups, not tags or autogroup:tagged\")\n\tErrSSHTagSourceToAutogroupMember      = errors.New(\"tags in SSH source cannot access autogroup:member (user-owned devices)\")\n\tErrSSHWildcardDestination             = errors.New(\"wildcard (*) is not supported as SSH destination\")\n\tErrSSHCheckPeriodAboveMax             = errors.New(\"is above the max (168h)\")\n\tErrSSHCheckPeriodNegative             = errors.New(\"must be a positive duration\")\n\tErrSSHCheckPeriodOnNonCheck           = errors.New(\"checkPeriod is only valid with action \\\"check\\\"\")\n\tErrInvalidLocalpart                   = errors.New(\"invalid localpart format, must be localpart:*@<domain>\")\n\tErrSSHUsersMustBeSpecified            = errors.New(\"users must be specified\")\n\tErrSSHUserInvalid                     = errors.New(\"is not valid\")\n\tErrSSHAcceptEnvEmpty                  = errors.New(\"acceptEnv values cannot be empty\")\n\tErrSSHActionMustBeSpecified           = errors.New(\"action must be specified\")\n\tErrSSHActionInvalid                   = errors.New(\"is not a valid action\")\n\tErrSSHDestinationHostAlias            = errors.New(\"invalid dst\")\n\tErrTagNameMustStartWithLetter         = errors.New(\"tag names must start with a letter, after 'tag:'\")\n\tErrGroupMembersCannotBeRecursive      = errors.New(\"group members cannot be recursive\")\n)\n\n// SSH check period constants per Tailscale docs:\n// https://tailscale.com/docs/features/tailscale-ssh#checkperiod\n// SaaS imposes no minimum (0s is accepted) so headscale matches.\nconst (\n\tSSHCheckPeriodDefault = 12 * time.Hour\n\tSSHCheckPeriodMax     = 7 * 24 * time.Hour\n)\n\n// ACL validation errors.\nvar (","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L34-L70","documentation":"ErrSSHUserInvalid in hscontrol/policy/v2/types.go:52 is a fragment-style sentinel (\"is not valid\") wrapped as user %q is not valid at types.go:2435. It fires when an SSH rule users entry is not one of the accepted forms: a plain username, autogroup:nonroot, autogroup:self (subject to SSH rules), or the localpart:*@<domain> pattern.","triggerScenarios":"An ssh rule users entry like \"group:admins\" (groups are not allowed in ssh users in this build), \"*\", an email with an unsupported form, or a user string failing validation at types.go:2435. The error message includes the offending user value.","commonSituations":"Porting ACL src entries (which allow groups and *) into ssh users where they are not accepted; assuming Tailscale SaaS grant syntax equals headscale ssh syntax; referencing a group defined in the policy inside an SSH rule.","solutions":["Check the offending user value quoted in the error and replace it with a valid form: plain username, autogroup:nonroot, or localpart:*@<domain>","For group-style access, enumerate the group's users in the users array or restructure the rule per docs/ ACL reference","Re-validate and re-apply the policy"],"exampleFix":"// before\n{\"users\": [\"group:admins\"], \"dst\": [\"tag:srv:admin\"], \"action\": \"accept\"}\n// after\n{\"users\": [\"alice\", \"bob\"], \"dst\": [\"tag:srv:admin\"], \"action\": \"accept\"}","handlingStrategy":"validation","validationCode":"func validSSHUser(u string) bool {\n    if u == \"autogroup:nonroot\" { return true }\n    if validLocalpartUser(u) { return true }\n    return !strings.ContainsAny(u, \":@*\") // plain username\n}","typeGuard":"func isSSHUserForm(u string) bool {\n    return u == \"autogroup:nonroot\" || strings.HasPrefix(u, \"localpart:*\") || !strings.Contains(u, \":\")\n}","tryCatchPattern":"if errors.Is(err, hpolicy.ErrSSHUserInvalid) { /* error quotes the offending user; fix that entry */ }","preventionTips":["Never put groups or * in ssh users","Keep a cheat sheet: ssh users accept username | autogroup:nonroot | localpart:*@domain"],"tags":["policy","ssh","users","configuration","validation","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}