{"record":{"id":"d4aa5a60a423c746","repo":"juanfont/headscale","slug":"auth-key-expired","errorCode":null,"errorMessage":"auth-key expired","messagePattern":"auth-key expired","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"hscontrol/db/preauth_keys.go","lineNumber":22,"sourceCode":"\t\"errors\"\n\t\"fmt\"\n\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,","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/preauth_keys.go#L4-L40","documentation":"Sentinel in hscontrol/db/preauth_keys.go returned by UsePreAuthKey when the key's expiration time is in the past. The key exists, is not exhausted, but its set expiration (visible via `headscale preauthkeys list`) has passed, so registration is refused.","triggerScenarios":"Registering a node with `tailscale up --auth-key=...` (or `tailscale login`) using a pre-auth key created with --expiration that has since passed; also possible with server clock skew.","commonSituations":"Keys created with short expirations (default 1h) reused days later; expired key left in automation config; NTP drift on the server expiring keys early.","solutions":["Create a new key: `headscale preauthkeys create --user <user> --expiration <duration>` and use it immediately","Check `headscale preauthkeys list` to confirm the key's expiration before use","For long-lived automation, create keys with a longer --expiration, or integrate the API to mint keys on demand"],"exampleFix":"# before\nheadscale preauthkeys create --user alice --expiration 1h\n# ...days later\ntailscale up --auth-key=<old-key>  # auth-key expired\n\n# after\nheadscale preauthkeys create --user alice --expiration 30d\ntailscale up --auth-key=<new-key>","handlingStrategy":"try-catch","validationCode":"# before enrollment, check the key's expiry from list output:\nheadscale preauthkeys list --user \"$USER\" | awk -v k=\"$KEY\" '$0 ~ k {print $3, $4}'","typeGuard":null,"tryCatchPattern":"if err := db.UsePreAuthKey(tx, pak, user); err != nil {\n    if errors.Is(err, db.ErrPreAuthKeyExpired) {\n        return registrationFailed(401, \"auth-key expired; create a new one\")\n    }\n    return err\n}","preventionTips":["Create keys just-in-time for enrollment rather than stockpiling them","Mint keys with --expiration matched to the provisioning window","Automations should request fresh keys via the API on each run"],"tags":["preauth-key","registration","expiration","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}