{"record":{"id":"ac90a135daca86f3","repo":"juanfont/headscale","slug":"machine-key-maps-to-ambiguous-node-ownership","errorCode":null,"errorMessage":"machine key maps to ambiguous node ownership","messagePattern":"machine key maps to ambiguous node ownership","errorType":"http","errorClass":"ErrAmbiguousNodeOwnership","httpStatus":409,"severity":"error","filePath":"hscontrol/state/state.go","lineNumber":126,"sourceCode":"\t\"LastSeen\",\n\t\"ApprovedRoutes\",\n\t\"UpdatedAt\",\n}\n\n// ErrRegistrationExpired is returned when a registration has expired.\nvar ErrRegistrationExpired = errors.New(\"registration expired\")\n\n// ErrNodeKeyInUse is returned when a registration or re-auth claims a NodeKey\n// already bound to a different machine, enforcing the 1:1 NodeKey<->MachineKey\n// binding.\nvar ErrNodeKeyInUse = errors.New(\"node key already in use by another machine\")\n\n// ErrAmbiguousNodeOwnership is returned when a machine key maps to a set of\n// nodes from which the correct one to update or convert cannot be determined:\n// multiple user-owned candidates for a tagged conversion, or a tagged node and\n// a user-owned node coexisting (impossible per validateNodeOwnership). The\n// registration is rejected rather than mutating an arbitrarily-picked node.\nvar ErrAmbiguousNodeOwnership = errors.New(\"machine key maps to ambiguous node ownership\")\n\n// sshCheckPair identifies a (source, destination) node pair for\n// SSH check auth tracking.\ntype sshCheckPair struct {\n\tSrc types.NodeID\n\tDst types.NodeID\n}\n\n// State manages Headscale's core state, coordinating between database, policy management,\n// IP allocation, and DERP routing. All methods are thread-safe.\n//\n// See [policy.PolicyManager] for policy evaluation and [NodeStore] for the\n// in-memory node cache.\ntype State struct {\n\t// cfg holds the current Headscale configuration\n\tcfg *types.Config\n\n\t// nodeStore provides an in-memory cache for nodes.","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/state/state.go#L108-L144","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"null","handlingStrategy":"try-catch","validationCode":"// Before tag conversion, ensure the machine key maps to exactly one node\nnodes := nodesForMachineKey(mk)\nif len(nodes) != 1 { cleanupDuplicates(nodes) }","typeGuard":"null","tryCatchPattern":"if errors.Is(err, state.ErrAmbiguousNodeOwnership) {\n    // list and prune duplicate nodes for this machine key, then re-register\n}","preventionTips":["Periodically clean up expired and duplicate nodes per machine key","Avoid re-registering machines repeatedly without removing old entries"],"tags":["headscale","registration","ownership","tags","data-integrity"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}