juanfont/headscale · error · parseErr (ErrPreAuthKeyFailedToParse|ErrOAuthClientFailedToParse|ErrAccessTokenFailedToParse|ErrAPIKeyFailedToParse)

%w: key too short, expected at least %d chars after prefix,

Error message

%w: key too short, expected at least %d chars after prefix, got %d

What it means

parsePrefixedKey rejected a credential because the string after the 'hskey-*-' scheme prefix is shorter than the minimum of prefixLen + 1 separator + secretLen characters (77 chars for today's 12/64 keys). The library-specific parseErr sentinel is wrapped so callers can classify it. Fixed-length parsing is used deliberately because base64 URL-safe secrets may contain dashes.

Source

Thrown at hscontrol/db/preauth_keys.go:246

	}

	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 {
		return "", "", fmt.Errorf(
			"%w: key too short, expected at least %d chars after prefix, got %d",
			parseErr,
			expectedMinLength,
			len(prefixAndSecret),
		)
	}

	prefix := prefixAndSecret[:prefixLen]

	// Validate separator at expected position
	if prefixAndSecret[prefixLen] != '-' {
		return "", "", fmt.Errorf(
			"%w: expected separator '-' at position %d, got '%c'",
			parseErr,
			prefixLen,
			prefixAndSecret[prefixLen],
		)
	}

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Regenerate the key and copy it as a single unbroken line.
  2. Validate length before submitting: after 'hskey-<kind>-', expect exactly 12 chars + '-' + 64 chars.
  3. If using a legacy key, create a new key with the current headscale version.

Example fix

// before
key := strings.TrimSpace(cfg.AuthKey)
node, err :=client.Up(ctx, key) // fails if truncated

// after
parts := strings.SplitN(cfg.AuthKey, "-", 3) // hskey, kind, rest
if len(parts) < 3 || len(parts[2]) != 77 {
    return fmt.Errorf("auth key truncated: expected 77 chars after scheme prefix")
}
Defensive patterns

Strategy: validation

Validate before calling

const (
    prefixLen  = 12
    secretLen  = 64
)
func checkKeyShape(rest string) error {
    if len(rest) < prefixLen+1+secretLen {
        return fmt.Errorf("key truncated: need %d chars after scheme prefix, got %d", prefixLen+1+secretLen, len(rest))
    }
    return nil
}

Prevention

When it happens

Trigger: Passing a truncated key to any API that parses new-format credentials (pre-auth key verification): a key cut off during copy/paste, a shell variable that swallowed part of the string, or an old/legacy key format with a shorter secret.

Common situations: Line wrapping in terminals, chat apps, or YAML files truncating the key; scripts building the key from parts getting the secret length wrong; using an old key generated before the current 64-char secret scheme.

Related errors


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