{"record":{"id":"870b8cee1b71e50d","repo":"juanfont/headscale","slug":"errsshauthsessionnotbound","errorCode":"ErrSSHAuthSessionNotBound","errorMessage":"auth session is not for SSH check","messagePattern":"auth session is not for SSH check","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"warning","filePath":"hscontrol/noise.go","lineNumber":625,"sourceCode":"\t\t\t)\n\t\t}\n\n\t\treturn nil, NewHTTPError(\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"Invalid auth_id\",\n\t\t\tfmt.Errorf(\"%w: %s\", ErrNoAuthSession, authID),\n\t\t)\n\t}\n\n\t// Verify the cached binding matches the (src, dst) pair the\n\t// follow-up URL claims. Without this check an attacker who knew an\n\t// auth_id could submit a follow-up for any other (src, dst) pair\n\t// and have its verdict recorded against that pair instead.\n\tif !auth.IsSSHCheck() {\n\t\treturn nil, NewHTTPError(\n\t\t\thttp.StatusBadRequest,\n\t\t\t\"auth session is not for SSH check\",\n\t\t\tfmt.Errorf(\"%w: %s\", ErrSSHAuthSessionNotBound, authID),\n\t\t)\n\t}\n\n\tbinding := auth.SSHCheckBinding()\n\tif binding.SrcNodeID != srcNodeID || binding.DstNodeID != dstNodeID {\n\t\treturn nil, NewHTTPError(\n\t\t\thttp.StatusUnauthorized,\n\t\t\t\"src/dst pair does not match auth session\",\n\t\t\tfmt.Errorf(\n\t\t\t\t\"%w: cached %d->%d, request %d->%d\",\n\t\t\t\tErrSSHBindingMismatch,\n\t\t\t\tbinding.SrcNodeID, binding.DstNodeID,\n\t\t\t\tsrcNodeID, dstNodeID,\n\t\t\t),\n\t\t)\n\t}\n\n\treqLog.Trace().Caller().Msg(\"SSH action follow-up\")","sourceCodeStart":607,"sourceCodeEnd":643,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/noise.go#L607-L643","documentation":"Returned when the cached auth entry referenced by auth_id exists but is not an SSH check session (IsSSHCheck() false). The auth cache is shared by registration and SSH check flows; this guard ensures a follow-up URL only consumes sessions of the right kind, preventing cross-flow confusion.","triggerScenarios":"Using an auth_id issued for a different purpose (e.g. node registration) against the /machine/ssh/action follow-up endpoint.","commonSituations":"Client bugs mixing up auth IDs between flows; manual experimentation with URLs; extremely unlikely organically since IDs are random and single-purpose.","solutions":["Confirm the auth_id in the URL is the one issued for this SSH check, not a registration auth_id","Restart the SSH session to obtain a fresh, correctly-bound auth_id","If writing clients, keep auth IDs strictly scoped to the flow that created them"],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":null,"typeGuard":"func isSSHCheckSession(a types.AuthRequest) bool {\n    return a.IsSSHCheck()\n}","tryCatchPattern":null,"preventionTips":["Scope every auth_id to the single flow that issued it","On this error, always restart the SSH session rather than reusing the auth_id"],"tags":["ssh","auth","session-binding","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}