juanfont/headscale · error
invalid localpart format, must be localpart:*@<domain>
Error message
invalid localpart format, must be localpart:*@<domain>
What it means
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.
Source
Thrown at hscontrol/policy/v2/types.go:50
}
const Wildcard = Asterix(0)
var ErrAutogroupSelfRequiresPerNodeResolution = errors.New("autogroup:self requires per-node resolution and cannot be resolved in this context")
var ErrUndefinedTagReference = errors.New("references undefined tag")
// SSH validation errors.
var (
ErrSSHTagSourceToUserDest = errors.New("tags in SSH source cannot access user-owned devices")
ErrSSHUserDestRequiresSameUser = errors.New("user destination requires source to contain only that same user")
ErrSSHAutogroupSelfRequiresUserSource = errors.New("autogroup:self destination requires source to contain only users or groups, not tags or autogroup:tagged")
ErrSSHTagSourceToAutogroupMember = errors.New("tags in SSH source cannot access autogroup:member (user-owned devices)")
ErrSSHWildcardDestination = errors.New("wildcard (*) is not supported as SSH destination")
ErrSSHCheckPeriodAboveMax = errors.New("is above the max (168h)")
ErrSSHCheckPeriodNegative = errors.New("must be a positive duration")
ErrSSHCheckPeriodOnNonCheck = errors.New("checkPeriod is only valid with action \"check\"")
ErrInvalidLocalpart = errors.New("invalid localpart format, must be localpart:*@<domain>")
ErrSSHUsersMustBeSpecified = errors.New("users must be specified")
ErrSSHUserInvalid = errors.New("is not valid")
ErrSSHAcceptEnvEmpty = errors.New("acceptEnv values cannot be empty")
ErrSSHActionMustBeSpecified = errors.New("action must be specified")
ErrSSHActionInvalid = errors.New("is not a valid action")
ErrSSHDestinationHostAlias = errors.New("invalid dst")
ErrTagNameMustStartWithLetter = errors.New("tag names must start with a letter, after 'tag:'")
ErrGroupMembersCannotBeRecursive = errors.New("group members cannot be recursive")
)
// SSH check period constants per Tailscale docs:
// https://tailscale.com/docs/features/tailscale-ssh#checkperiod
// SaaS imposes no minimum (0s is accepted) so headscale matches.
const (
SSHCheckPeriodDefault = 12 * time.Hour
SSHCheckPeriodMax = 7 * 24 * time.Hour
)
View on GitHub (pinned to 565fd254d0)
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)
Example fix
// before
{"users": ["localpart:alice@corp.com"], ...}
// after
{"users": ["localpart:*@corp.com"], ...} // or just ["alice"] Defensive patterns
Strategy: validation
Validate before calling
var localpartRe = regexp.MustCompile(`^localpart:\*@[^@]+$`)
func validLocalpartUser(u string) bool { return localpartRe.MatchString(u) } Type guard
func isLocalpartUser(u string) bool {
return strings.HasPrefix(u, "localpart:*") && strings.Count(u, "@") == 1 && strings.HasSuffix(u, "@") == false && len(strings.SplitN(u, "@", 2)[1]) > 0
} Try / catch
if errors.Is(err, hpolicy.ErrInvalidLocalpart) { /* message names the exact defect: prefix, @, local part, or domain */ } Prevention
- Remember only localpart:*@domain is supported — specific users use plain names
- Unit-test policy generation against the regex before pushing to headscale
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- must be a positive duration
- checkPeriod is only valid with action "check"
- users must be specified
- is not valid
- action must be specified
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/6fe574704c0f1fad.
Report an issue: GitHub.