juanfont/headscale · error
invalid auth key: %w
Error message
invalid auth key: %w
What it means
The pre-auth key prefix matched a row in the database, but bcrypt comparison of the stored hash against the supplied secret failed. Headscale stores only a bcrypt hash of the key secret; a mismatch means the secret portion of the key is wrong for that prefix. This is an authentication failure for the presented credential, and the raw bcrypt error is wrapped for context.
Source
Thrown at hscontrol/db/preauth_keys.go:227
prefixAndHash,
authKeyPrefixLength,
authKeyLength,
ErrPreAuthKeyFailedToParse,
)
if err != nil {
return nil, err
}
// Look up key by prefix
err = tx.Preload("User").First(&pak, "prefix = ?", prefix).Error
if err != nil {
return nil, ErrPreAuthKeyNotFound
}
// Verify hash matches
err = bcrypt.CompareHashAndPassword(pak.Hash, []byte(hash))
if err != nil {
return nil, fmt.Errorf("invalid auth key: %w", err)
}
return &pak, nil
}
// parsePrefixedKey splits the prefix-and-secret portion of a new-format key
// (the part after the "hskey-*-" prefix) into its fixed-length prefix and
// secret components, validating the length, separator position, and that both
// components are base64 URL-safe. Fixed-length parsing is used instead of
// separator-based to handle dashes in base64 URL-safe characters.
func parsePrefixedKey(
prefixAndSecret string,
//nolint:unparam // kept explicit though every credential kind uses a 12-char prefix and 64-char secret today
prefixLen, secretLen int,
parseErr error,
) (string, string, error) {
expectedMinLength := prefixLen + 1 + secretLen
if len(prefixAndSecret) < expectedMinLength {View on GitHub (pinned to 565fd254d0)
Solutions
- Regenerate the pre-auth key (headscale preauthkeys create) and use the fresh full key string verbatim.
- Verify the key was copied without truncation: new-format keys are 'hskey-<kind>-<12-char prefix>-<64-char secret>'.
- Confirm the key still exists and is not expired: list keys with 'headscale preauthkeys list <user>'.
- Check that no whitespace, quotes, or shell expansion ($ characters historically) corrupted the key in scripts.
Defensive patterns
Strategy: validation
Validate before calling
// Before submitting, parse and structurally validate the key:
if _, _, err := db.ParsePreAuthKeyFormat(rawKey); err != nil {
return err // malformed before we even hit the DB
}
// Structural check only; secret match can only be verified server-side. Try / catch
pak, err := h.cfg.DB.GetPreAuthKey(userStr, rawKey)
if err != nil {
if strings.Contains(err.Error(), "invalid auth key") {
// treat as bad credential: do not retry, prompt for a new key
}
return err
} Prevention
- Copy keys whole from 'headscale preauthkeys create' output; never retype them.
- Store keys in secret managers or single-line env vars, not multi-line files.
- Fail fast on the first mismatch — repeated guesses gain nothing since bcrypt hash comparison is deterministic.
When it happens
Trigger: A node or CLI submits an authkey whose 'hskey-<kind>-<prefix>-<secret>' secret does not match the stored hash: typo/truncation when copying the key, key regenerated with the same prefix (extremely unlikely), or a forged key that guessed an existing 12-char prefix. Produced by GetPreAuthKey during registration (noise auth flow).
Common situations: Users copy the key with a missing/extra character, line-wrapping breaks the key in terminals or chat clients, or the key was rotated/deleted and an old copy is reused. Also happens when the 'hskey-' prefix scheme is stripped and the remainder is misassembled.
Related errors
- handling register with auth key: %w
- ErrPreAuthKeyNotFound
- failed to parse ApiKey
- failed to parse oauth client secret
- failed to parse oauth access token
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/4300f5ba83db9fd5.
Report an issue: GitHub.