{"record":{"id":"07cf263aebac401c","repo":"juanfont/headscale","slug":"failed-to-parse-apikey-prefix-contains-invalid-ch","errorCode":null,"errorMessage":"failed to parse ApiKey: prefix contains invalid characters","messagePattern":"failed to parse ApiKey: prefix contains invalid characters","errorType":"validation","errorClass":"ErrAPIKeyFailedToParse","httpStatus":500,"severity":"warning","filePath":"hscontrol/db/api_key.go","lineNumber":182,"sourceCode":"\n\t// If it starts with the API key prefix, parse it\n\tif strings.HasPrefix(displayPrefix, apiKeyPrefix) {\n\t\t// Remove the \"hskey-api-\" prefix\n\t\t_, remainder, found := strings.Cut(displayPrefix, apiKeyPrefix)\n\t\tif !found {\n\t\t\treturn \"\", fmt.Errorf(\"%w: invalid display prefix format\", ErrAPIKeyFailedToParse)\n\t\t}\n\n\t\t// Extract just the first 12 characters (the actual prefix)\n\t\tif len(remainder) < apiKeyPrefixLength {\n\t\t\treturn \"\", fmt.Errorf(\"%w: prefix too short\", ErrAPIKeyFailedToParse)\n\t\t}\n\n\t\tprefix := remainder[:apiKeyPrefixLength]\n\n\t\t// Validate it's base64 URL-safe\n\t\tif !isValidBase64URLSafe(prefix) {\n\t\t\treturn \"\", fmt.Errorf(\"%w: prefix contains invalid characters\", ErrAPIKeyFailedToParse)\n\t\t}\n\n\t\treturn prefix, nil\n\t}\n\n\t// For legacy 7-character prefixes or other formats, return as-is\n\treturn displayPrefix, nil\n}\n\n// validateAPIKey validates an API key and returns the key if valid.\n// Handles both new (hskey-api-{prefix}-{secret}) and legacy (prefix.secret) formats.\nfunc validateAPIKey(db *gorm.DB, keyStr string) (*types.APIKey, error) {\n\t// Validate input is not empty\n\tif keyStr == \"\" {\n\t\treturn nil, ErrAPIKeyFailedToParse\n\t}\n\n\t// Check for new format: hskey-api-{prefix}-{secret}","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/api_key.go#L164-L200","documentation":"Returned by ParseAPIKeyPrefix when the first 12 characters after 'hskey-api-' fail the isValidBase64URLSafe check. Headscale generates prefixes from a base64 URL-safe alphabet, so invalid characters (spaces, '+', '/', control chars, shell-expanded symbols) mean the string is not a headscale-generated key.","triggerScenarios":"Calling ParseAPIKeyPrefix with 'hskey-api-' followed by characters outside [A-Za-z0-9_-]: a key corrupted by shell expansion ($, !), URL-decoded with '+' or '/' inside, or a foreign token pasted with the headscale prefix.","commonSituations":"Keys pasted through terminals that mangle special characters; double-URL-encoding/decoding; users prefixing another system's token with 'hskey-api-'.","solutions":["Re-copy the key verbatim from its source ('headscale apikeys create' output) without shell interpolation.","Quote the key in shell usage: use single quotes to prevent variable expansion.","If the key was stored URL-encoded, decode it once before passing."],"exampleFix":"// before\n_, err := db.ParseAPIKeyPrefix(os.Args[1]) // unquoted $ or ! chars got expanded\n\n// after\n// pass the key single-quoted on the command line:\n//   headscale ... --apikey 'hskey-api-XXXXXXXXXXXX-YYYY...'\n_, err := db.ParseAPIKeyPrefix(key)","handlingStrategy":"validation","validationCode":"var base64urlRe = regexp.MustCompile(`^[A-Za-z0-9_-]{12,}$`)\nif strings.HasPrefix(display, \"hskey-api-\") && !base64urlRe.MatchString(strings.TrimPrefix(display, \"hskey-api-\")) {\n    return errors.New(\"API key contains invalid characters; re-copy it verbatim\")\n}","typeGuard":"func looksLikeBase64URLSafe(s string) bool {\n    for _, r := range s {\n        if !(r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-' || r == '_') {\n            return false\n        }\n    }\n    return true\n}","tryCatchPattern":"if _, err := db.ParseAPIKeyPrefix(display); err != nil && errors.Is(err, db.ErrAPIKeyFailedToParse) {\n    // input is not a headscale-generated key; do not retry, re-collect the value\n}","preventionTips":["Single-quote API keys in shell commands to prevent $ / ! expansion.","Pass keys via environment variables or files instead of command-line arguments.","Do not URL-encode/decode headscale keys; they are already URL-safe."],"tags":["api-key","validation","base64url","user-input"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}