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

  1. Retry the SSH connection — the client re-runs the check flow and gets a fresh auth_id
  2. If frequent, review the auth cache TTL vs. your approval workflow time
  3. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/f0aa2292a1bcea03. Report an issue: GitHub.