juanfont/headscale · warning · HTTPError
ErrSSHDstNodeNotFound
ErrSSHDstNodeNotFound
Error message
dst node not found
What it means
Returned by the SSH action handler when the dst_node_id in the URL does not match any node in the state store (GetNodeByID miss). The dst node is the tailscaled that would accept the incoming SSH connection; if it does not exist, no SSH verdict can be produced.
Source
Thrown at hscontrol/noise.go:402
err,
))
return
}
// Authenticate the Noise session: the destination node is the
// tailscaled instance asking us whether to permit an incoming SSH
// connection, so its Noise session must belong to dst. Without this
// check any unauthenticated client could open a Noise tunnel with a
// throwaway machine key and pollute lastSSHAuth for arbitrary
// (src, dst) pairs, defeating SSH check-mode's stolen-key
// protections.
dstNode, ok := ns.headscale.state.GetNodeByID(dstNodeID)
if !ok {
httpError(writer, NewHTTPError(
http.StatusNotFound,
"dst node not found",
fmt.Errorf("%w: %d", ErrSSHDstNodeNotFound, dstNodeID),
))
return
}
if dstNode.MachineKey() != ns.machineKey {
httpError(writer, NewHTTPError(
http.StatusUnauthorized,
"machine key does not match dst node",
fmt.Errorf(
"%w: machine key %s, dst node %d",
ErrSSHMachineKeyMismatch, ns.machineKey.ShortString(), dstNodeID,
),
))
return
}
View on GitHub (pinned to 565fd254d0)
Solutions
- Verify the dst node still exists: headscale nodes list
- If it was deleted, let the client re-run 'tailscale ssh' to start a fresh session
- Check for ID confusion between src and dst when reading logs
Defensive patterns
Strategy: validation
Validate before calling
if _, ok := h.state.GetNodeByID(dstNodeID); !ok {
return fmt.Errorf("dst node %d does not exist; it may have been deleted — retry the SSH session", dstNodeID)
} Prevention
- Treat node deletion as invalidating in-flight SSH check sessions
- In clients, retry the SSH connection on 404 dst-not-found instead of reusing the old action URL
When it happens
Trigger: An SSH check-mode request naming a dst node that was deleted, expired, or never existed; a stale SSHAction URL from a client cached across node removal.
Common situations: Node removed with 'headscale nodes delete' while an SSH check session was pending; node IDs reused/shifted after database changes; crafted requests probing the endpoint.
Related errors
- node not found
- 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
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/4b46a8ebd2c44294.
Report an issue: GitHub.