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
- List nodes for the machine (headscale node list) and delete stale/duplicate entries so one machine key maps to one node
- Clean up expired nodes of that machine before re-registering
- If data looks impossible (tagged + user-owned coexisting), inspect and repair the DB rows explicitly
- 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
- Periodically clean up expired and duplicate nodes per machine key
- Avoid re-registering machines repeatedly without removing old entries
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
- node marked as tagged but has no tags
- node has neither user nor tags - must be owned by user or ta
- requested tags
- foreign key constraints violated
- auth-key expired
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/ac90a135daca86f3.
Report an issue: GitHub.