juanfont/headscale · error · gorm.ErrRecordNotFound

ErrPreAuthKeyNotFound

ErrPreAuthKeyNotFound

Error message

auth-key not found: %w

What it means

ErrPreAuthKeyNotFound wraps gorm.ErrRecordNotFound: a pre-auth key lookup (by key string) matched no row — unknown, deleted, or malformed so it derives a nonexistent key. The code comment notes the registration handler maps this to a 401 rather than a server error, so callers must test errors.Is rather than string matching.

Source

Thrown at hscontrol/db/preauth_keys.go:21

import (
	"errors"
	"fmt"
	"slices"
	"strings"
	"time"

	"github.com/juanfont/headscale/hscontrol/types"
	"golang.org/x/crypto/bcrypt"
	"gorm.io/gorm"
	"tailscale.com/util/rands"
	"tailscale.com/util/set"
)

var (
	// ErrPreAuthKeyNotFound wraps gorm.ErrRecordNotFound so an unknown or
	// deleted key is treated as a missing record by callers, which the
	// registration handler maps to a 401 rather than a raw server error.
	ErrPreAuthKeyNotFound          = fmt.Errorf("auth-key not found: %w", gorm.ErrRecordNotFound)
	ErrPreAuthKeyExpired           = errors.New("auth-key expired")
	ErrSingleUseAuthKeyHasBeenUsed = errors.New("auth-key has already been used")
	ErrUserMismatch                = errors.New("user mismatch")
	ErrPreAuthKeyACLTagInvalid     = errors.New("auth-key tag is invalid")
)

// validateACLTags deduplicates, sorts, and checks that every tag carries the
// "tag:" prefix. Shared by the pre-auth-key and OAuth credential paths so both
// enforce the same tag shape.
func validateACLTags(tags []string) ([]string, error) {
	tags = set.SetOf(tags).Slice()
	slices.Sort(tags)

	for _, tag := range tags {
		if !strings.HasPrefix(tag, "tag:") {
			return nil, fmt.Errorf(
				"%w: '%s' did not begin with 'tag:'",
				ErrPreAuthKeyACLTagInvalid,

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Check errors.Is(err, db.ErrPreAuthKeyNotFound) and return 401 to the client
  2. Generate a fresh pre-auth key and re-register the node
  3. Verify the key string is copied whole (no line-wrap truncation in terminals)

Example fix

// before
if err != nil {
	return err
}

// after
if errors.Is(err, db.ErrPreAuthKeyNotFound) {
	return ErrUnauthorized
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Optional shape check before sending registration
if !strings.HasPrefix(userKey, "nodekey-") {
	return fmt.Errorf("key does not look like a pre-auth key")
}

Type guard

func isPreAuthKeyNotFound(err error) bool {
	return errors.Is(err, db.ErrPreAuthKeyNotFound) ||
		errors.Is(err, gorm.ErrRecordNotFound)
}

Try / catch

if err := db.UsePreAuthKey(key, node); err != nil {
	if errors.Is(err, db.ErrPreAuthKeyNotFound) {
		return ErrUnauthorized // 401, matching the registration handler
	}
	return err
}

Prevention

When it happens

Trigger: Registering a node with an expired-and-deleted key, a typo'd key, or one from a different headscale instance; key revoked/expired and cleaned up.

Common situations: Users pasting keys with missing characters or extra whitespace; stale keys after server rebuild; environments pointing at the wrong control server.

Related errors


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