juanfont/headscale · warning

auth-key has already been used

Error message

auth-key has already been used

What it means

Sentinel in hscontrol/db/preauth_keys.go returned by UsePreAuthKey when a key flagged single-use (--reusable=false, the default) is presented for registration a second time. The key row records its use; any subsequent attempt is rejected regardless of expiration.

Source

Thrown at hscontrol/db/preauth_keys.go:23

	"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,
				tag,
			)

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Create a reusable key for templated provisioning: `headscale preauthkeys create --reusable`
  2. Or generate one key per node from an orchestration step (via CLI or API) before first boot
  3. For a failed first enrollment, mint a fresh key rather than retrying the consumed one

Example fix

# before
headscale preauthkeys create --user infra  # single-use default; cloned VMs fail

# after
headscale preauthkeys create --user infra --reusable
Defensive patterns

Strategy: try-catch

Try / catch

if err := db.UsePreAuthKey(tx, pak, user); err != nil {
    if errors.Is(err, db.ErrSingleUseAuthKeyHasBeenUsed) {
        return registrationFailed(401, "single-use key already consumed; request a new key")
    }
    return err
}

Prevention

When it happens

Trigger: Baking one non-reusable auth-key into a VM/PXE template or docker image and booting multiple nodes from it; re-running `tailscale up --auth-key=...` after the first registration succeeded.

Common situations: Mass-provisioning with a single key; enrollment retries after a partially successful first attempt already consumed the key.

Related errors


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