{"record":{"id":"465c71cac0068596","repo":"juanfont/headscale","slug":"user-mismatch","errorCode":null,"errorMessage":"user mismatch","messagePattern":"user mismatch","errorType":"http","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"hscontrol/db/preauth_keys.go","lineNumber":24,"sourceCode":"\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"golang.org/x/crypto/bcrypt\"\n\t\"gorm.io/gorm\"\n\t\"tailscale.com/util/rands\"\n\t\"tailscale.com/util/set\"\n)\n\nvar (\n\t// ErrPreAuthKeyNotFound wraps gorm.ErrRecordNotFound so an unknown or\n\t// deleted key is treated as a missing record by callers, which the\n\t// registration handler maps to a 401 rather than a raw server error.\n\tErrPreAuthKeyNotFound          = fmt.Errorf(\"auth-key not found: %w\", gorm.ErrRecordNotFound)\n\tErrPreAuthKeyExpired           = errors.New(\"auth-key expired\")\n\tErrSingleUseAuthKeyHasBeenUsed = errors.New(\"auth-key has already been used\")\n\tErrUserMismatch                = errors.New(\"user mismatch\")\n\tErrPreAuthKeyACLTagInvalid     = errors.New(\"auth-key tag is invalid\")\n)\n\n// validateACLTags deduplicates, sorts, and checks that every tag carries the\n// \"tag:\" prefix. Shared by the pre-auth-key and OAuth credential paths so both\n// enforce the same tag shape.\nfunc validateACLTags(tags []string) ([]string, error) {\n\ttags = set.SetOf(tags).Slice()\n\tslices.Sort(tags)\n\n\tfor _, tag := range tags {\n\t\tif !strings.HasPrefix(tag, \"tag:\") {\n\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\"%w: '%s' did not begin with 'tag:'\",\n\t\t\t\tErrPreAuthKeyACLTagInvalid,\n\t\t\t\ttag,\n\t\t\t)\n\t\t}","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/preauth_keys.go#L6-L42","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := db.UsePreAuthKey(tx, pak, user); err != nil {\n    if errors.Is(err, db.ErrUserMismatch) {\n        return registrationFailed(401, \"auth-key belongs to a different user\")\n    }\n    return err\n}","preventionTips":["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"],"tags":["preauth-key","user","registration","authorization","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}