juanfont/headscale · error

using pre auth key: %w

Error message

using pre auth key: %w

What it means

Marking a non-reusable pre-auth key as used failed inside the new-node registration transaction, right after the node INSERT succeeded. hsdb.UsePreAuthKey performs an atomic compare-and-set on the key's Used flag; the whole transaction (including the just-inserted node) rolls back on failure.

Source

Thrown at hscontrol/state/state.go:2033

	// Seed GivenName from the sanitised raw hostname. [NodeStore.PutNode]
	// bumps on collision and falls back to "node" if the sanitised
	// result is empty (pure non-ASCII / punctuation input).
	if nodeToRegister.GivenName == "" {
		nodeToRegister.GivenName = dnsname.SanitizeHostname(nodeToRegister.Hostname)
	}

	// New node - database first to get ID, then [NodeStore]
	savedNode, err := hsdb.Write(s.db.DB, func(tx *gorm.DB) (*types.Node, error) {
		err := tx.Save(&nodeToRegister).Error
		if err != nil {
			return nil, fmt.Errorf("saving node: %w", err)
		}

		if params.PreAuthKey != nil && !params.PreAuthKey.Reusable {
			err := hsdb.UsePreAuthKey(tx, params.PreAuthKey)
			if err != nil {
				return nil, fmt.Errorf("using pre auth key: %w", err)
			}
		}

		return &nodeToRegister, nil
	})
	if err != nil {
		return types.NodeView{}, err
	}

	// Add to [NodeStore] after database creates the ID
	return s.nodeStore.PutNode(*savedNode), nil
}

// validateRequestTags validates that the requested tags are permitted for the node.
// This should be called BEFORE [NodeStore.UpdateNode] to ensure we don't modify [NodeStore]
// if validation fails. Returns the list of rejected tags (empty if all valid).
func (s *State) validateRequestTags(node types.NodeView, requestTags []string) []string {
	// Empty tags = clear tags, always permitted

View on GitHub (pinned to 565fd254d0)

Solutions

  1. If the key is legitimately shared by multiple hosts, recreate it with --reusable
  2. Generate a fresh key if this one is used up or expired (`headscale preauthkeys list` shows state)
  3. Serialize container startups so one-shot keys are consumed one at a time

Example fix

# before
headscale preauthkeys create --user alice --expiration 1h

# after (shared across N hosts)
headscale preauthkeys create --user alice --reusable --expiration 1h
Defensive patterns

Strategy: validation

Validate before calling

// Provisioning-time key audit before handing the key to a client:
if key.Used && !key.Reusable {
    return fmt.Errorf("key %d already consumed; create --reusable or a new key", key.ID)
}
if key.Expiration != nil && key.Expiration.Before(time.Now()) {
    return errors.New("key expired; generate a new one")
}

Try / catch

if err != nil && strings.Contains(err.Error(), "using pre auth key") {
    // Whole tx rolled back; mint a fresh key rather than retrying
    key, _ = client.CreatePreAuthKey(user, true /*reusable*/, ttl)
}

Prevention

When it happens

Trigger: Registering with a one-shot (non-reusable) key where UsePreAuthKey fails: the key was already used (concurrent registration with the same key), the key expired between validation and write, or the row was deleted mid-transaction.

Common situations: Two containers boot simultaneously sharing one single-use auth key; key expired during a long OIDC/interactive delay; key deleted by an admin while a client was mid-registration.

Related errors


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