{"record":{"id":"28350e541f11da96","repo":"juanfont/headscale","slug":"errsshmachinekeymismatch","errorCode":"ErrSSHMachineKeyMismatch","errorMessage":"machine key does not match dst node","messagePattern":"machine key does not match dst node","errorType":"http","errorClass":"HTTPError","httpStatus":401,"severity":"error","filePath":"hscontrol/noise.go","lineNumber":412,"sourceCode":"\t// throwaway machine key and pollute lastSSHAuth for arbitrary\n\t// (src, dst) pairs, defeating SSH check-mode's stolen-key\n\t// protections.\n\tdstNode, ok := ns.headscale.state.GetNodeByID(dstNodeID)\n\tif !ok {\n\t\thttpError(writer, NewHTTPError(\n\t\t\thttp.StatusNotFound,\n\t\t\t\"dst node not found\",\n\t\t\tfmt.Errorf(\"%w: %d\", ErrSSHDstNodeNotFound, dstNodeID),\n\t\t))\n\n\t\treturn\n\t}\n\n\tif dstNode.MachineKey() != ns.machineKey {\n\t\thttpError(writer, NewHTTPError(\n\t\t\thttp.StatusUnauthorized,\n\t\t\t\"machine key does not match dst node\",\n\t\t\tfmt.Errorf(\n\t\t\t\t\"%w: machine key %s, dst node %d\",\n\t\t\t\tErrSSHMachineKeyMismatch, ns.machineKey.ShortString(), dstNodeID,\n\t\t\t),\n\t\t))\n\n\t\treturn\n\t}\n\n\treqLog := log.With().\n\t\tUint64(\"src_node_id\", srcNodeID.Uint64()).\n\t\tUint64(\"dst_node_id\", dstNodeID.Uint64()).\n\t\tStr(\"local_user\", req.URL.Query().Get(\"local_user\")).\n\t\tLogger()\n\n\treqLog.Trace().Caller().Msg(\"SSH action request\")\n\n\taction, err := ns.sshAction(\n\t\treq.Context(),","sourceCodeStart":394,"sourceCodeEnd":430,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/noise.go#L394-L430","documentation":"Returned by the SSH action handler when the Noise session's machine key does not equal the machine key of the dst node. This authenticates that the asking tailscaled actually IS the destination node; otherwise a client with a throwaway machine key could pollute lastSSHAuth for arbitrary (src, dst) pairs and defeat check-mode's stolen-key protection.","triggerScenarios":"A node opens a Noise tunnel and then submits an SSH action request naming a different node as dst; node keys rotated (machine key changed) while the node record still holds the old key.","commonSituations":"Re-registered node whose machine key changed without the DB record being updated; malicious or buggy client attempting to forge SSH verdicts for another node; test harnesses reusing one noise client for many node IDs.","solutions":["Confirm the request's Noise identity matches the dst node — check machine keys via 'headscale nodes list' -o json","If the node legitimately rotated keys, delete and re-register the node so the stored machine key matches","For test harnesses, use one Noise session per node identity"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"dstNode, ok := h.state.GetNodeByID(dstNodeID)\nif !ok || dstNode.MachineKey() != ns.machineKey {\n    return errors.New(\"noise session does not own dst node; re-register or use the correct session\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use exactly one Noise session per node identity in clients and test harnesses","Re-register a node after any machine key rotation so the stored key matches"],"tags":["ssh","security","authentication","machine-key"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}