{"record":{"id":"f0aa2292a1bcea03","repo":"juanfont/headscale","slug":"errnoauthsession","errorCode":"ErrNoAuthSession","errorMessage":"Invalid auth_id","messagePattern":"Invalid auth_id","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"warning","filePath":"hscontrol/noise.go","lineNumber":613,"sourceCode":"\tauth, ok := ns.headscale.state.GetAuthCacheEntry(authID)\n\tif !ok {\n\t\t// The session is gone (expired, evicted, or lost on a control-plane\n\t\t// restart). A bare error dead-ends the client: it keeps polling this\n\t\t// now-defunct auth_id until the SSH connection times out. Re-delegate\n\t\t// so a still-required check can complete instead.\n\t\tif checkFound {\n\t\t\treqLog.Info().Caller().\n\t\t\t\tMsg(\"SSH check auth session missing; re-delegating\")\n\n\t\t\treturn ns.sshActionHoldAndDelegate(\n\t\t\t\treqLog, action, srcNodeID, dstNodeID,\n\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(","sourceCodeStart":595,"sourceCodeEnd":631,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/noise.go#L595-L631","documentation":"Returned when the auth_id parses but has no entry in the state auth cache (GetAuthCacheEntry miss, ErrNoAuthSession). Sessions expire or are lost on control-plane restart; if no SSH check is still required the code re-delegates, otherwise this 400 ends the flow. The comment explains the design: a missing session must not dead-end a client that is still required to complete a check.","triggerScenarios":"headscale restarted or the auth cache entry expired between the initial SSH action and the user completing the check in the browser; an auth_id replayed after expiry.","commonSituations":"User leaves the SSH check approval page open past the TTL, then approves; headscale restarts (deploy, crash) mid check-mode session; slow human approval workflows.","solutions":["Retry the SSH connection — the client re-runs the check flow and gets a fresh auth_id","If frequent, review the auth cache TTL vs. your approval workflow time","Avoid restarting headscale while check-mode approvals are pending"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"auth, ok := ns.headscale.state.GetAuthCacheEntry(authID)\nif !ok {\n    // session expired or lost to a restart: restart the flow to obtain a fresh auth_id\n    return ns.sshActionHoldAndDelegate(reqLog, action, srcNodeID, dstNodeID)\n}","preventionTips":["Complete SSH check approvals promptly relative to the auth cache TTL","Avoid restarting headscale while check-mode approvals are pending"],"tags":["ssh","auth-cache","session-expiry","retry"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}