juanfont/headscale · error · ErrInvalidUsername

%w, got: %q

Error message

%w, got: %q

What it means

Username.Validate rejected a policy username because it does not contain '@' (the isUser check failed). In headscale's v2 policy, a username alias is always an email-style token; this error fires during policy unmarshalling/validation, before any user lookup happens.

Source

Thrown at hscontrol/policy/v2/types.go:346

			if !seen.Contains(s) {
				seen.Add(s)
				routes = append(routes, s)
			}
		}
	}

	return routes
}

// Username is a string that represents a username, it must contain an @.
type Username string

func (u *Username) Validate() error {
	if isUser(string(*u)) {
		return nil
	}

	return fmt.Errorf("%w, got: %q", ErrInvalidUsername, *u)
}

func (u *Username) String() string {
	return string(*u)
}

// MarshalJSON marshals the Username to JSON.
func (u *Username) MarshalJSON() ([]byte, error) {
	return json.Marshal(string(*u))
}

// MarshalJSON marshals the Prefix to JSON.
func (p *Prefix) MarshalJSON() ([]byte, error) {
	return json.Marshal(p.String())
}

func (u *Username) UnmarshalJSON(b []byte) error {
	*u = Username(strings.Trim(string(b), `"`))

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Use the full email, e.g. 'alice@example.com'.
  2. If you intended a group, use 'group:engineering'; for a device label use 'tag:name'.
  3. If your IdP users genuinely lack emails, map them to groups or tags instead of username aliases.

Example fix

// before
{"action": "accept", "src": ["alice"], "dst": ["web:80"]}

// after
{"action": "accept", "src": ["alice@example.com"], "dst": ["web:80"]}
Defensive patterns

Strategy: validation

Validate before calling

// Cheap pre-flight: a username alias must contain '@'.
func isValidUsernameAlias(s string) bool { return strings.Contains(s, "@") }

Try / catch

if err := policy.Unmarshal(data); err != nil {
    if errors.Is(err, v2.ErrInvalidUsername) {
        // point the user at the offending token in the message ('got: ...')
    }
    return err
}

Prevention

When it happens

Trigger: A grants/tests entry uses "alice", "alice@", or a bare LDAP uid without a domain as src/dst/user. Username.UnmarshalJSON -> Validate fails via isUser(string) == false.

Common situations: Migrating from ACL systems that allow bare usernames; using local-part-only names from an OIDC provider that does not supply emails; typos deleting the domain.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/a67bf3fbe519815e. Report an issue: GitHub.