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

  1. Confirm the request's Noise identity matches the dst node — check machine keys via 'headscale nodes list' -o json
  2. If the node legitimately rotated keys, delete and re-register the node so the stored machine key matches
  3. 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

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


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