juanfont/headscale · error
failed to parse ApiKey
Error message
failed to parse ApiKey
What it means
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.
Source
Thrown at hscontrol/db/api_key.go:26
"github.com/juanfont/headscale/hscontrol/types"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"tailscale.com/util/rands"
)
const (
apiKeyPrefix = "hskey-api-" //nolint:gosec // This is a prefix, not a credential
apiKeyPrefixLength = 12
apiKeyHashLength = 64
// Legacy format constants.
legacyAPIPrefixLength = 7
legacyAPIKeyLength = 32
)
var (
ErrAPIKeyFailedToParse = errors.New("failed to parse ApiKey")
ErrAPIKeyGenerationFailed = errors.New("failed to generate API key")
ErrAPIKeyExpired = errors.New("API key expired")
)
// CreateAPIKey creates a new [types.APIKey] in a user, and returns it.
func (hsdb *HSDatabase) CreateAPIKey(
expiration *time.Time,
) (string, *types.APIKey, error) {
// Generate public prefix (12 chars)
prefix := rands.HexString(apiKeyPrefixLength)
// Generate secret (64 chars)
secret := rands.HexString(apiKeyHashLength)
// Full key string (shown ONCE to user)
keyStr := apiKeyPrefix + prefix + "-" + secret
// bcrypt hash of secretView on GitHub (pinned to 565fd254d0)
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
Example fix
// before
key := string(rawFileBytes) // trailing newline included
_, ak, err := hsdb.GetAPIKey(key)
// after
key := strings.TrimSpace(string(rawFileBytes))
if !strings.HasPrefix(key, "hskey-api-") {
return fmt.Errorf("not an API key: %w", db.ErrAPIKeyFailedToParse)
}
_, ak, err := hsdb.GetAPIKey(key) Defensive patterns
Strategy: type-guard
Validate before calling
// verify shape before calling the DB layer
func looksLikeAPIKey(s string) bool {
parts := strings.Split(s, "-")
return len(parts) == 4 && parts[0] == "hskey" && parts[1] == "api" &&
len(parts[2]) == 12 && len(parts[3]) == 64 && isHex(parts[2]+parts[3])
} Type guard
func isAPIKey(s string) bool {
rest, ok := strings.CutPrefix(s, "hskey-api-")
if !ok {
return false
}
prefix, secret, found := strings.Cut(rest, "-")
return found && len(prefix) == 12 && len(secret) == 64
} Try / catch
_, ak, err := hsdb.GetAPIKey(keyStr)
if err != nil {
if errors.Is(err, db.ErrAPIKeyFailedToParse) {
// reject the credential early; do not retry — the string is malformed
return echo.NewHTTPError(http.StatusUnauthorized, "malformed API key")
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- failed to parse oauth client secret
- failed to generate API key
- failed to parse oauth access token
- failed to parse auth-key
- node not found
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/16216519227d24d4.
Report an issue: GitHub.