From 474ea236d0c6d393dbcf7baa98da240ad20c1b66 Mon Sep 17 00:00:00 2001 From: Mustafa Enes Batur <40495733+Thifhi@users.noreply.github.com> Date: Fri, 6 Jun 2025 12:14:11 +0200 Subject: [PATCH] Fix `/machine/map` endpoint vulnerability (#2642) * Improve map auth logic * Bugfix * Add comment, improve error message * noise: make func, get by node this commit splits the additional validation into a separate function so it can be reused if we add more endpoints in the future. It swaps the check, so we still look up by NodeKey, but before accepting the connection, we validate the known machinekey from the db against the noise connection. The reason for this is that when a node logs in or out, the node key is replaced and it will no longer be possible to look it up, breaking reauthentication. Signed-off-by: Kristoffer Dalby Co-authored-by: Kristoffer Dalby --- CHANGELOG.md | 8 +++++++- hscontrol/noise.go | 35 +++++++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bca556d..7562f130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # CHANGELOG -## Next +## 0.26.1 (2025-06-06) + +### Changes + +- Ensure nodes are matching both node key and machine key + when connecting. + [#2642](https://github.com/juanfont/headscale/pull/2642) ## 0.26.0 (2025-05-14) diff --git a/hscontrol/noise.go b/hscontrol/noise.go index 1269d032..ce83bc79 100644 --- a/hscontrol/noise.go +++ b/hscontrol/noise.go @@ -100,6 +100,10 @@ func (h *Headscale) NoiseUpgradeHandler( router.HandleFunc("/machine/register", noiseServer.NoiseRegistrationHandler). Methods(http.MethodPost) + + // Endpoints outside of the register endpoint must use getAndValidateNode to + // get the node to ensure that the MachineKey matches the Node setting up the + // connection. router.HandleFunc("/machine/map", noiseServer.NoisePollNetMapHandler) noiseServer.httpBaseConfig = &http.Server{ @@ -209,18 +213,14 @@ func (ns *noiseServer) NoisePollNetMapHandler( return } - ns.nodeKey = mapRequest.NodeKey - - node, err := ns.headscale.db.GetNodeByNodeKey(mapRequest.NodeKey) + node, err := ns.getAndValidateNode(mapRequest) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - httpError(writer, NewHTTPError(http.StatusNotFound, "node not found", nil)) - return - } httpError(writer, err) return } + ns.nodeKey = node.NodeKey + sess := ns.headscale.newMapSession(req.Context(), mapRequest, writer, node) sess.tracef("a node sending a MapRequest with Noise protocol") if !sess.isStreaming() { @@ -266,8 +266,8 @@ func (ns *noiseServer) NoiseRegistrationHandler( Error: httpErr.Msg, } return ®Req, resp - } else { } + return ®Req, regErr(err) } @@ -289,3 +289,22 @@ func (ns *noiseServer) NoiseRegistrationHandler( writer.WriteHeader(http.StatusOK) writer.Write(respBody) } + +// getAndValidateNode retrieves the node from the database using the NodeKey +// and validates that it matches the MachineKey from the Noise session. +func (ns *noiseServer) getAndValidateNode(mapRequest tailcfg.MapRequest) (*types.Node, error) { + node, err := ns.headscale.db.GetNodeByNodeKey(mapRequest.NodeKey) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, NewHTTPError(http.StatusNotFound, "node not found", nil) + } + return nil, err + } + + // Validate that the MachineKey in the Noise session matches the one associated with the NodeKey. + if ns.machineKey != node.MachineKey { + return nil, NewHTTPError(http.StatusNotFound, "node key in request does not match the one associated with this machine key", nil) + } + + return node, nil +}