{"record":{"id":"16216519227d24d4","repo":"juanfont/headscale","slug":"failed-to-parse-apikey","errorCode":null,"errorMessage":"failed to parse ApiKey","messagePattern":"failed to parse ApiKey","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/db/api_key.go","lineNumber":26,"sourceCode":"\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)\n\nconst (\n\tapiKeyPrefix       = \"hskey-api-\" //nolint:gosec // This is a prefix, not a credential\n\tapiKeyPrefixLength = 12\n\tapiKeyHashLength   = 64\n\n\t// Legacy format constants.\n\tlegacyAPIPrefixLength = 7\n\tlegacyAPIKeyLength    = 32\n)\n\nvar (\n\tErrAPIKeyFailedToParse    = errors.New(\"failed to parse ApiKey\")\n\tErrAPIKeyGenerationFailed = errors.New(\"failed to generate API key\")\n\tErrAPIKeyExpired          = errors.New(\"API key expired\")\n)\n\n// CreateAPIKey creates a new [types.APIKey] in a user, and returns it.\nfunc (hsdb *HSDatabase) CreateAPIKey(\n\texpiration *time.Time,\n) (string, *types.APIKey, error) {\n\t// Generate public prefix (12 chars)\n\tprefix := rands.HexString(apiKeyPrefixLength)\n\n\t// Generate secret (64 chars)\n\tsecret := rands.HexString(apiKeyHashLength)\n\n\t// Full key string (shown ONCE to user)\n\tkeyStr := apiKeyPrefix + prefix + \"-\" + secret\n\n\t// bcrypt hash of secret","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/api_key.go#L8-L44","documentation":"Sentinel from hscontrol/db/api_key.go used to reject malformed API key strings before any database lookup. Valid keys have the shape hskey-api-<prefix(12)>-<secret(64)>; a legacy 32-char format is also accepted. Wrap sites (api_key.go:170-246) add detail such as 'invalid display prefix format', 'prefix too short', 'prefix contains invalid characters', or a legacy length mismatch.","triggerScenarios":"Calling GetAPIKey / verification helpers with a string that is empty, lacks the hskey-api- prefix, has a prefix shorter than 12 chars, contains non-hex characters, or is a truncated/edited legacy key. Also triggered by passing a key of a different kind (e.g. an auth-key or oauth token) to the API-key verifier.","commonSituations":"Shell quoting mangled the key (truncated at a newline or dash-split); copy/paste lost characters; using an oauth access token (hskey-oauthtok-...) where an admin API key is expected; scripts reading the key from a file that includes a trailing newline.","solutions":["Check the key starts with hskey-api- followed by 12 hex chars, a dash, and 64 hex chars — re-copy it from `headscale apikeys create` output","Strip whitespace/newlines when loading the key in scripts (e.g. tr -d '\\n')","Confirm you are not passing a pre-auth key (authkey-...) or oauth token to the API-key code path","If the key is legacy format, verify it is exactly 32 characters after the legacy prefix"],"exampleFix":"// before\nkey := string(rawFileBytes) // trailing newline included\n_, ak, err := hsdb.GetAPIKey(key)\n\n// after\nkey := strings.TrimSpace(string(rawFileBytes))\nif !strings.HasPrefix(key, \"hskey-api-\") {\n    return fmt.Errorf(\"not an API key: %w\", db.ErrAPIKeyFailedToParse)\n}\n_, ak, err := hsdb.GetAPIKey(key)","handlingStrategy":"type-guard","validationCode":"// verify shape before calling the DB layer\nfunc looksLikeAPIKey(s string) bool {\n    parts := strings.Split(s, \"-\")\n    return len(parts) == 4 && parts[0] == \"hskey\" && parts[1] == \"api\" &&\n        len(parts[2]) == 12 && len(parts[3]) == 64 && isHex(parts[2]+parts[3])\n}","typeGuard":"func isAPIKey(s string) bool {\n    rest, ok := strings.CutPrefix(s, \"hskey-api-\")\n    if !ok {\n        return false\n    }\n    prefix, secret, found := strings.Cut(rest, \"-\")\n    return found && len(prefix) == 12 && len(secret) == 64\n}","tryCatchPattern":"_, ak, err := hsdb.GetAPIKey(keyStr)\nif err != nil {\n    if errors.Is(err, db.ErrAPIKeyFailedToParse) {\n        // reject the credential early; do not retry — the string is malformed\n        return echo.NewHTTPError(http.StatusUnauthorized, \"malformed API key\")\n    }\n    return err\n}","preventionTips":["Store keys in secret managers whole; never split/rejoin them in scripts","Trim whitespace when reading keys from files or env vars","Distinguish credential kinds by prefix (hskey-api- vs authkey vs hskey-oauthtok-) at ingress"],"tags":["api-key","authentication","validation","headscale","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}