juanfont/headscale · warning
user mismatch
Error message
user mismatch
What it means
Sentinel in hscontrol/db/preauth_keys.go returned when the user attached to the registration does not match the pre-auth key's owner. UsePreAuthKey compares the node's registering user with the key's user_id; a mismatch means the key is being used to enroll into a different user's namespace than it was issued for.
Source
Thrown at hscontrol/db/preauth_keys.go:24
"slices"
"strings"
"time"
"github.com/juanfont/headscale/hscontrol/types"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"tailscale.com/util/rands"
"tailscale.com/util/set"
)
var (
// ErrPreAuthKeyNotFound wraps gorm.ErrRecordNotFound so an unknown or
// deleted key is treated as a missing record by callers, which the
// registration handler maps to a 401 rather than a raw server error.
ErrPreAuthKeyNotFound = fmt.Errorf("auth-key not found: %w", gorm.ErrRecordNotFound)
ErrPreAuthKeyExpired = errors.New("auth-key expired")
ErrSingleUseAuthKeyHasBeenUsed = errors.New("auth-key has already been used")
ErrUserMismatch = errors.New("user mismatch")
ErrPreAuthKeyACLTagInvalid = errors.New("auth-key tag is invalid")
)
// validateACLTags deduplicates, sorts, and checks that every tag carries the
// "tag:" prefix. Shared by the pre-auth-key and OAuth credential paths so both
// enforce the same tag shape.
func validateACLTags(tags []string) ([]string, error) {
tags = set.SetOf(tags).Slice()
slices.Sort(tags)
for _, tag := range tags {
if !strings.HasPrefix(tag, "tag:") {
return nil, fmt.Errorf(
"%w: '%s' did not begin with 'tag:'",
ErrPreAuthKeyACLTagInvalid,
tag,
)
}View on GitHub (pinned to 565fd254d0)
Solutions
- Issue the pre-auth key from the same user the node is enrolling as (`headscale preauthkeys create --user <that-user>`)
- For shared/role enrollment, use tagged keys with --tags so nodes land in the tag namespace instead of a user
- Reset the node's registration state (delete the node) before re-enrolling with a different user's key
Defensive patterns
Strategy: try-catch
Try / catch
if err := db.UsePreAuthKey(tx, pak, user); err != nil {
if errors.Is(err, db.ErrUserMismatch) {
return registrationFailed(401, "auth-key belongs to a different user")
}
return err
} Prevention
- Issue keys from the user the node will enroll as; verify before handing keys out
- Use tagged pre-auth keys (--tags) for shared infrastructure so user ownership is not the axis
- Delete and re-register nodes moving between users instead of mixing credentials
When it happens
Trigger: Registration flows where the resolved user (from OIDC login, existing node owner, or registration cache) differs from pak.User: e.g. an OIDC-authenticated user pasting a pre-auth key issued to another user, or a re-registration where the node's user changed.
Common situations: Sharing keys across team members' accounts; mixing interactive login and auth-key enrollment on the same node; admin revoking/transferring a user while a key from the old user is in flight.
Related errors
- auth-key expired
- auth-key has already been used
- failed to parse auth-key
- authenticated principal is not in any allowed group
- authenticated principal has an unverified email
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/465c71cac0068596.
Report an issue: GitHub.