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 permittedView on GitHub (pinned to 565fd254d0)
Solutions
- If the key is legitimately shared by multiple hosts, recreate it with --reusable
- Generate a fresh key if this one is used up or expired (`headscale preauthkeys list` shows state)
- 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
- Use --reusable for any key consumed by more than one process or restart cycle
- Never share a single-use key across container replicas
- Check `headscale preauthkeys list` for used/expired state before debugging registrations
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
- registering existing node in database: %w
- saving node to database: %w
- finding user: %w
- node not found
- failed to parse auth-key
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/8a2f5450e2c3ba40.
Report an issue: GitHub.