{"record":{"id":"6fe574704c0f1fad","repo":"juanfont/headscale","slug":"invalid-localpart-format-must-be-localpart-dom","errorCode":null,"errorMessage":"invalid localpart format, must be localpart:*@<domain>","messagePattern":"invalid localpart format, must be localpart:\\*@<domain>","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":50,"sourceCode":"}\n\nconst 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","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L32-L68","documentation":"ErrInvalidLocalpart in hscontrol/policy/v2/types.go:50 is returned when an SSH rule user of the localpart form does not match the required shape localpart:*@<domain>. headscale supports exactly one wildcard localpart pattern (any local-part at a domain), and parseLocalpartUser (types.go ~3073-3092) rejects anything else: missing \"localpart:\" prefix, missing \"@\", a local part other than \"*\", or an empty domain.","triggerScenarios":"An ssh rule \"users\" entry like \"localpart:user@corp.com\" (local part not *), \"localpart:*corp.com\" (missing @), \"*@corp.com\" (missing localpart: prefix), or \"localpart:*@\" (empty domain). Each fails with a wrapped ErrInvalidLocalpart naming the exact defect.","commonSituations":"Trying to match a specific user's email (\"localpart:alice@corp.com\") instead of the whole domain; migrating email-style identities from another ACL system; typos when hand-writing the localpart: prefix.","solutions":["Use the exact form localpart:*@<domain>, e.g. localpart:*@corp.com, which matches every local-part at that domain","To target one specific user, use the plain username (e.g. \"alice\") or autogroup:username instead of localpart syntax","Fix the specific defect named in the wrapped error message (missing prefix, missing @, non-* local part, or empty domain)"],"exampleFix":"// before\n{\"users\": [\"localpart:alice@corp.com\"], ...}\n// after\n{\"users\": [\"localpart:*@corp.com\"], ...}  // or just [\"alice\"]","handlingStrategy":"validation","validationCode":"var localpartRe = regexp.MustCompile(`^localpart:\\*@[^@]+$`)\n\nfunc validLocalpartUser(u string) bool { return localpartRe.MatchString(u) }","typeGuard":"func isLocalpartUser(u string) bool {\n    return strings.HasPrefix(u, \"localpart:*\") && strings.Count(u, \"@\") == 1 && strings.HasSuffix(u, \"@\") == false && len(strings.SplitN(u, \"@\", 2)[1]) > 0\n}","tryCatchPattern":"if errors.Is(err, hpolicy.ErrInvalidLocalpart) { /* message names the exact defect: prefix, @, local part, or domain */ }","preventionTips":["Remember only localpart:*@domain is supported — specific users use plain names","Unit-test policy generation against the regex before pushing to headscale"],"tags":["policy","ssh","localpart","configuration","validation","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}