{"record":{"id":"0980a109ca712730","repo":"juanfont/headscale","slug":"ssh-action-cached-binding-does-not-match-request","errorCode":null,"errorMessage":"ssh action: cached binding does not match request src/dst","messagePattern":"ssh action: cached binding does not match request src/dst","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/noise.go","lineNumber":55,"sourceCode":"// ErrSSHDstNodeNotFound is returned when the dst node id on a Noise SSH\n// action request does not match any registered node.\nvar ErrSSHDstNodeNotFound = errors.New(\"ssh action: unknown dst node id\")\n\n// ErrSSHMachineKeyMismatch is returned when the Noise session's machine\n// key does not match the dst node referenced in the SSH action URL.\nvar ErrSSHMachineKeyMismatch = errors.New(\n\t\"ssh action: noise session machine key does not match dst node\",\n)\n\n// ErrSSHAuthSessionNotBound is returned when an SSH action follow-up\n// references an auth session that is not bound to an SSH check pair.\nvar ErrSSHAuthSessionNotBound = errors.New(\n\t\"ssh action: cached auth session is not an SSH-check binding\",\n)\n\n// ErrSSHBindingMismatch is returned when an SSH action follow-up's\n// (src, dst) pair does not match the cached binding for its auth_id.\nvar ErrSSHBindingMismatch = errors.New(\n\t\"ssh action: cached binding does not match request src/dst\",\n)\n\nconst (\n\t// ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade.\n\tts2021UpgradePath = \"/ts2021\"\n\n\t// The first 9 bytes from the server to client over Noise are either an HTTP/2\n\t// settings frame (a normal HTTP/2 setup) or, as Tailscale added later, an \"early payload\"\n\t// header that's also 9 bytes long: 5 bytes ([earlyPayloadMagic]) followed by 4 bytes\n\t// of length. Then that many bytes of JSON-encoded [tailcfg.EarlyNoise].\n\t// The early payload is optional. Some servers may not send it... But we do!\n\tearlyPayloadMagic = \"\\xff\\xff\\xffTS\"\n\n\t// noiseBodyLimit is the maximum allowed request body size for Noise protocol\n\t// handlers. This prevents unauthenticated OOM attacks via unbounded [io.ReadAll].\n\t// No legitimate Noise request ([tailcfg.MapRequest], [tailcfg.RegisterRequest], etc.) comes close\n\t// to this limit; typical payloads are a few KB.","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/noise.go#L37-L73","documentation":"Returned by noiseServer.sshActionFollowUp (hscontrol/noise.go:630-641) when an SSH action follow-up's (src, dst) node pair does not match the binding cached for its auth_id. This is a security check: without it, anyone who learned an auth_id could submit a verdict for a different node pair. The HTTP response is 401 'src/dst pair does not match auth session'.","triggerScenarios":"A follow-up request whose URL srcNodeID/dstNodeID differ from auth.SSHCheckBinding() cached at check-creation time. The error message includes both pairs ('cached A->B, request C->D') so mismatches are easy to confirm.","commonSituations":"Tampered or proxied follow-up URLs (attack/probing); client bug mixing up src and dst ordering after a reconnect; NAT rebinding causing the client to resume an SSH session with swapped roles.","solutions":["Compare the pairs in the error message (cached X->Y vs request A->B) and use the cached pair's URL exactly","Re-initiate the SSH connection so a fresh check binding is minted for the current node pair","Audit for tampering if the request did not originate from your own client — this error indicates a genuine authorization mismatch"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if errors.Is(err, ErrSSHBindingMismatch) { // 401: security mismatch; do not retry, re-initiate the SSH session so a fresh binding is created }","preventionTips":["Treat this error as a possible probing signal and alert on it","Clients should always derive (src,dst) from the current SSH session, not from cached URLs","Never share or persist SSH action follow-up URLs"],"tags":["ssh","security","auth","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}