juanfont/headscale · error · ErrAmbiguousNodeOwnership

machine key maps to ambiguous node ownership

Error message

machine key maps to ambiguous node ownership

What it means

ErrAmbiguousNodeOwnership is a sentinel error in hscontrol/state/state.go:126 returned when a MachineKey maps to multiple nodes and headscale cannot tell which one to update or convert: multiple user-owned candidates for a tagged conversion, or a tagged node coexisting with a user-owned node (normally impossible per validateNodeOwnership). Returned at state.go:2268 and 2450; the registration is rejected rather than mutating an arbitrary node (auth_tagged_expiry_test.go:377).

Source

Thrown at hscontrol/state/state.go:126

	"LastSeen",
	"ApprovedRoutes",
	"UpdatedAt",
}

// ErrRegistrationExpired is returned when a registration has expired.
var ErrRegistrationExpired = errors.New("registration expired")

// ErrNodeKeyInUse is returned when a registration or re-auth claims a NodeKey
// already bound to a different machine, enforcing the 1:1 NodeKey<->MachineKey
// binding.
var ErrNodeKeyInUse = errors.New("node key already in use by another machine")

// ErrAmbiguousNodeOwnership is returned when a machine key maps to a set of
// nodes from which the correct one to update or convert cannot be determined:
// multiple user-owned candidates for a tagged conversion, or a tagged node and
// a user-owned node coexisting (impossible per validateNodeOwnership). The
// registration is rejected rather than mutating an arbitrarily-picked node.
var ErrAmbiguousNodeOwnership = errors.New("machine key maps to ambiguous node ownership")

// sshCheckPair identifies a (source, destination) node pair for
// SSH check auth tracking.
type sshCheckPair struct {
	Src types.NodeID
	Dst types.NodeID
}

// State manages Headscale's core state, coordinating between database, policy management,
// IP allocation, and DERP routing. All methods are thread-safe.
//
// See [policy.PolicyManager] for policy evaluation and [NodeStore] for the
// in-memory node cache.
type State struct {
	// cfg holds the current Headscale configuration
	cfg *types.Config

	// nodeStore provides an in-memory cache for nodes.

View on GitHub (pinned to 565fd254d0)

Solutions

  1. List nodes for the machine (headscale node list) and delete stale/duplicate entries so one machine key maps to one node
  2. Clean up expired nodes of that machine before re-registering
  3. If data looks impossible (tagged + user-owned coexisting), inspect and repair the DB rows explicitly
  4. Re-run registration after the duplicates are removed

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

// Before tag conversion, ensure the machine key maps to exactly one node
nodes := nodesForMachineKey(mk)
if len(nodes) != 1 { cleanupDuplicates(nodes) }

Type guard

null

Try / catch

if errors.Is(err, state.ErrAmbiguousNodeOwnership) {
    // list and prune duplicate nodes for this machine key, then re-register
}

Prevention

When it happens

Trigger: A machine key with several historical user-owned nodes re-registers with tags, so no single conversion target exists; database contains legacy data where one machine key owns both a tagged and a user-owned node; registration and SetTags racing on the same machine key.

Common situations: Machines registered repeatedly over time (expired nodes never cleaned up) accumulating multiple nodes under one machine key; databases migrated from older headscale versions with looser ownership rules; attempting tag conversion on such machines.

Related errors


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