juanfont/headscale · warning · HTTPError
ErrNoAuthSession
ErrNoAuthSession
Error message
Invalid auth_id
What it means
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.
Source
Thrown at hscontrol/noise.go:613
auth, ok := ns.headscale.state.GetAuthCacheEntry(authID)
if !ok {
// The session is gone (expired, evicted, or lost on a control-plane
// restart). A bare error dead-ends the client: it keeps polling this
// now-defunct auth_id until the SSH connection times out. Re-delegate
// so a still-required check can complete instead.
if checkFound {
reqLog.Info().Caller().
Msg("SSH check auth session missing; re-delegating")
return ns.sshActionHoldAndDelegate(
reqLog, action, srcNodeID, dstNodeID,
)
}
return nil, NewHTTPError(
http.StatusBadRequest,
"Invalid auth_id",
fmt.Errorf("%w: %s", ErrNoAuthSession, authID),
)
}
// Verify the cached binding matches the (src, dst) pair the
// follow-up URL claims. Without this check an attacker who knew an
// auth_id could submit a follow-up for any other (src, dst) pair
// and have its verdict recorded against that pair instead.
if !auth.IsSSHCheck() {
return nil, NewHTTPError(
http.StatusBadRequest,
"auth session is not for SSH check",
fmt.Errorf("%w: %s", ErrSSHAuthSessionNotBound, authID),
)
}
binding := auth.SSHCheckBinding()
if binding.SrcNodeID != srcNodeID || binding.DstNodeID != dstNodeID {
return nil, NewHTTPError(View on GitHub (pinned to 565fd254d0)
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
Defensive patterns
Strategy: retry
Try / catch
auth, ok := ns.headscale.state.GetAuthCacheEntry(authID)
if !ok {
// session expired or lost to a restart: restart the flow to obtain a fresh auth_id
return ns.sshActionHoldAndDelegate(reqLog, action, srcNodeID, dstNodeID)
} Prevention
- Complete SSH check approvals promptly relative to the auth cache TTL
- Avoid restarting headscale while check-mode approvals are pending
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ssh action: cached auth session is not an SSH-check binding
- ssh action: cached binding does not match request src/dst
- tags in SSH source cannot access user-owned devices
- user destination requires source to contain only that same u
- autogroup:self destination requires source to contain only u
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/f0aa2292a1bcea03.
Report an issue: GitHub.