juanfont/headscale · error · ErrUserNotFound
%w: token %q
Error message
%w: token %q
What it means
Username.resolve searched all users by ProviderIdentifier, Email, and Name and found no match for the policy token. The username is well-formed (has '@') but does not correspond to any user in the database the policy was compiled against.
Source
Thrown at hscontrol/policy/v2/types.go:409
// At parsetime, we require all usernames to contain an "@" character, if the
// username token does not naturally do so (like email), the user have to
// add it to the end of the username. We strip it here as we do not expect the
// usernames to be stored with the "@".
uTrimmed := strings.TrimSuffix(u.String(), "@")
for _, user := range users {
if user.ProviderIdentifier.Valid && user.ProviderIdentifier.String == uTrimmed {
// Prioritize ProviderIdentifier match and exit early
return user, nil
}
if user.Email == uTrimmed || user.Name == uTrimmed {
potentialUsers = append(potentialUsers, user)
}
}
if len(potentialUsers) == 0 {
return types.User{}, fmt.Errorf("%w: token %q", ErrUserNotFound, u.String())
}
if len(potentialUsers) > 1 {
return types.User{}, fmt.Errorf("%w: token %q found: %s", ErrMultipleUsersFound, u.String(), potentialUsers.String())
}
return potentialUsers[0], nil
}
func (u *Username) Resolve(_ *Policy, users types.Users, nodes views.Slice[types.NodeView]) (ResolvedAddresses, error) {
return newResolvedAddresses(u.resolve(nil, users, nodes))
}
func (u *Username) resolve(_ *Policy, users types.Users, nodes views.Slice[types.NodeView]) (*netipx.IPSet, error) {
var (
ips netipx.IPSetBuilder
errs []error
)View on GitHub (pinned to 565fd254d0)
Solutions
- Verify the exact identifier: run 'headscale users list' and compare email/name/ProviderIdentifier.
- Update the policy token to the current email or username.
- If the user should exist, recreate it or re-run OIDC login so the user row is populated.
- Prefer groups for stable policies so individual renames do not break ACLs.
Example fix
// before
{"src": ["bob@old.example.com"], "dst": ["web:80"]}
// after
{"src": ["bob@new.example.com"], "dst": ["web:80"]} Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check the token against the user DB before compiling the policy.
if _, err := findUserByToken(users, token); err != nil {
return fmt.Errorf("policy token %q matches no user; run 'headscale users list'", token)
} Try / catch
user, err := username.Resolve(pol, users, nodes)
if err != nil {
if errors.Is(err, v2.ErrUserNotFound) {
// missing user: sync DB or update policy; do not silently continue
return fmt.Errorf("policy references unknown user %q: %w", username, err)
}
return err
} Prevention
- Keep policy files and user directory in the same repo/review stream so renames update both.
- Prefer groups over individual usernames in grants.
- Add a CI step that compiles the policy against a production DB snapshot.
When it happens
Trigger: Policy references 'bob@old.example.com' after the user's email changed; a user deleted via 'headscale users destroy'; a fresh database where tests reference users never created; ProviderIdentifier mismatch after IdP migration.
Common situations: Renaming users or changing the OIDC provider without updating policy files; policy files shared across environments (prod ACL in a test DB); typos in the domain part.
Related errors
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/ea09d278bcdc3c9a.
Report an issue: GitHub.