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

  1. Regenerate the pre-auth key (headscale preauthkeys create) and use the fresh full key string verbatim.
  2. Verify the key was copied without truncation: new-format keys are 'hskey-<kind>-<12-char prefix>-<64-char secret>'.
  3. Confirm the key still exists and is not expired: list keys with 'headscale preauthkeys list <user>'.
  4. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/4300f5ba83db9fd5. Report an issue: GitHub.