juanfont/headscale · error · HTTPError
ErrSSHMachineKeyMismatch
ErrSSHMachineKeyMismatch
Error message
machine key does not match dst node
What it means
Returned by the SSH action handler when the Noise session's machine key does not equal the machine key of the dst node. This authenticates that the asking tailscaled actually IS the destination node; otherwise a client with a throwaway machine key could pollute lastSSHAuth for arbitrary (src, dst) pairs and defeat check-mode's stolen-key protection.
Source
Thrown at hscontrol/noise.go:412
// 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
}
reqLog := log.With().
Uint64("src_node_id", srcNodeID.Uint64()).
Uint64("dst_node_id", dstNodeID.Uint64()).
Str("local_user", req.URL.Query().Get("local_user")).
Logger()
reqLog.Trace().Caller().Msg("SSH action request")
action, err := ns.sshAction(
req.Context(),View on GitHub (pinned to 565fd254d0)
Solutions
- Confirm the request's Noise identity matches the dst node — check machine keys via 'headscale nodes list' -o json
- If the node legitimately rotated keys, delete and re-register the node so the stored machine key matches
- For test harnesses, use one Noise session per node identity
Defensive patterns
Strategy: validation
Validate before calling
dstNode, ok := h.state.GetNodeByID(dstNodeID)
if !ok || dstNode.MachineKey() != ns.machineKey {
return errors.New("noise session does not own dst node; re-register or use the correct session")
} Prevention
- Use exactly one Noise session per node identity in clients and test harnesses
- Re-register a node after any machine key rotation so the stored key matches
When it happens
Trigger: A node opens a Noise tunnel and then submits an SSH action request naming a different node as dst; node keys rotated (machine key changed) while the node record still holds the old key.
Common situations: Re-registered node whose machine key changed without the DB record being updated; malicious or buggy client attempting to forge SSH verdicts for another node; test harnesses reusing one noise client for many node IDs.
Related errors
- ssh action: cached binding does not match request src/dst
- ErrSSHBindingMismatch
- failed to parse ApiKey
- failed to parse oauth client secret
- failed to parse oauth access token
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/28350e541f11da96.
Report an issue: GitHub.