1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-06-19 01:18:37 +02:00

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 <kristoffer@tailscale.com>
Co-authored-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Mustafa Enes Batur 2025-06-06 12:14:11 +02:00 committed by Kristoffer Dalby
parent 2dc2f3b3f0
commit 474ea236d0
No known key found for this signature in database
2 changed files with 34 additions and 9 deletions

View File

@ -1,6 +1,12 @@
# CHANGELOG # 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) ## 0.26.0 (2025-05-14)

View File

@ -100,6 +100,10 @@ func (h *Headscale) NoiseUpgradeHandler(
router.HandleFunc("/machine/register", noiseServer.NoiseRegistrationHandler). router.HandleFunc("/machine/register", noiseServer.NoiseRegistrationHandler).
Methods(http.MethodPost) 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) router.HandleFunc("/machine/map", noiseServer.NoisePollNetMapHandler)
noiseServer.httpBaseConfig = &http.Server{ noiseServer.httpBaseConfig = &http.Server{
@ -209,18 +213,14 @@ func (ns *noiseServer) NoisePollNetMapHandler(
return return
} }
ns.nodeKey = mapRequest.NodeKey node, err := ns.getAndValidateNode(mapRequest)
node, err := ns.headscale.db.GetNodeByNodeKey(mapRequest.NodeKey)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
httpError(writer, NewHTTPError(http.StatusNotFound, "node not found", nil))
return
}
httpError(writer, err) httpError(writer, err)
return return
} }
ns.nodeKey = node.NodeKey
sess := ns.headscale.newMapSession(req.Context(), mapRequest, writer, node) sess := ns.headscale.newMapSession(req.Context(), mapRequest, writer, node)
sess.tracef("a node sending a MapRequest with Noise protocol") sess.tracef("a node sending a MapRequest with Noise protocol")
if !sess.isStreaming() { if !sess.isStreaming() {
@ -266,8 +266,8 @@ func (ns *noiseServer) NoiseRegistrationHandler(
Error: httpErr.Msg, Error: httpErr.Msg,
} }
return &regReq, resp return &regReq, resp
} else {
} }
return &regReq, regErr(err) return &regReq, regErr(err)
} }
@ -289,3 +289,22 @@ func (ns *noiseServer) NoiseRegistrationHandler(
writer.WriteHeader(http.StatusOK) writer.WriteHeader(http.StatusOK)
writer.Write(respBody) 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
}