Skip to content

Conversation

@Thifhi
Copy link
Contributor

@Thifhi Thifhi commented Jun 5, 2025

Current Behavior

POST /machine/map requests are processed by extracting the Node from the public NodeKey provided in the request, without any additional verification. This allows an attacker who possesses the public key of any Node in the network to:

  1. Establish a Noise connection using a new machine key,
  2. Submit a MapRequest containing a NodeKey that does not belong to them, and
  3. Receive a stream of MapResponses.

As a result, the attacker can learn sensitive details about the entire Tailnet.

Problem

The current approach implicitly trusts the NodeKey in the request, even though the client has not proven ownership of its corresponding private key.

Proposed Change

Instead, we should retrieve the Node based on the machine key used in the Noise connection, as the client must possess the corresponding private key to establish the session. This provides proper authentication and matches the behavior of Tailscale servers. This is what Tailscale control plane returns in this case:
image

  • have read the CONTRIBUTING.md file
  • raised a GitHub issue or discussed it on the projects chat beforehand
  • added unit tests
  • added integration tests
  • updated documentation if needed
  • updated CHANGELOG.md

@Thifhi Thifhi requested review from juanfont and kradalby as code owners June 5, 2025 12:11
@ghost
Copy link

ghost commented Jun 5, 2025

Pull Request Revisions

RevisionDescription
r5
Node connection key matching improvedUpdated node connection logic to validate both node and machine keys simultaneously
r4
Added node validation for noiseIntroduced getAndValidateNode method to centralize node retrieval and machine key validation for Noise protocol handlers
r3
Improved node key validation logicEnhanced node key validation in noise polling handler with more descriptive error message and clearer node key matching check
r2
Node key comparison field changedModified node key comparison from ns.nodeKey to node.NodeKey in noise map request handler
r1
Node key verification and retrieval updatedModified node lookup logic to first retrieve node by machine key, then validate node key match

✅ AI review completed for r5
Help React with emojis to give feedback on AI-generated reviews:
  • 👍 means the feedback was helpful and actionable
  • 👎 means the feedback was incorrect or unhelpful
💬 Replying to feedback with a comment helps us improve the system. Your input also contributes to shaping future interactions with the AI reviewer.

We'd love to hear from you—reach out anytime at team@review.ai.

Comment on lines 223 to 226
if ns.nodeKey != mapRequest.NodeKey {
httpError(writer, NewHTTPError(http.StatusNotFound, "node does not belong to machine key", nil))
return
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In hscontrol/noise.go, the code now checks that the nodeKey from the map request matches the existing nodeKey before processing. This is a security improvement, but have you considered adding test cases to verify this validation works correctly?

Comment on lines 223 to 228
if ns.nodeKey != mapRequest.NodeKey {
httpError(writer, NewHTTPError(http.StatusNotFound, "node does not belong to machine key", nil))
return
}

ns.nodeKey = node.NodeKey
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In hscontrol/noise.go, line 228 assigns ns.nodeKey = node.NodeKey after the validation check. However, if ns.nodeKey hasn't been properly initialized before this handler is called, the validation check on line 223 might fail incorrectly. Consider verifying if ns.nodeKey is always properly set before reaching this handler.

}

if ns.nodeKey != mapRequest.NodeKey {
httpError(writer, NewHTTPError(http.StatusNotFound, "node does not belong to machine key", nil))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In hscontrol/noise.go, the error message 'node does not belong to machine key' on line 224 might be confusing. Consider changing it to something more precise like 'node key in request doesn't match the one associated with this machine key' to clarify the exact validation that's failing.

Comment on lines 223 to 226
if node.NodeKey != mapRequest.NodeKey {
httpError(writer, NewHTTPError(http.StatusNotFound, "node does not belong to machine key", nil))
return
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In hscontrol/noise.go, consider adding a comment explaining the security implications of this change. The new approach correctly verifies that the node key in the map request matches the one associated with the machine key before proceeding, which prevents potential spoofing attacks.

Copy link
Collaborator

@kradalby kradalby left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!, thank you. I think the review agent has two good points, can you address those?

Comment on lines 275 to 277
return &regReq, resp
} else {
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In hscontrol/noise.go, there's an empty else block at lines 275-277. Consider removing the empty block for cleaner code.

@Thifhi
Copy link
Contributor Author

Thifhi commented Jun 5, 2025

Good catch!, thank you. I think the review agent has two good points, can you address those?

Addressed 👍

@kradalby
Copy link
Collaborator

kradalby commented Jun 5, 2025

Thank you, i'll merge after the tests

@Thifhi
Copy link
Contributor Author

Thifhi commented Jun 5, 2025

Also I just realized that MapRequest is used to submit updates for the node as well, so just by knowing the node's public key, an attacker might be able to modify the network configuration. I'm not fully familiar with the broader codebase, so I'll leave it to you to assess the severity of this.

@kradalby
Copy link
Collaborator

kradalby commented Jun 5, 2025

I'll take a look tomorrow, also the failing tests

kradalby added 2 commits June 6, 2025 08:35
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>
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
@kradalby
Copy link
Collaborator

kradalby commented Jun 6, 2025

Also I just realized that MapRequest is used to submit updates for the node as well, so just by knowing the node's public key, an attacker might be able to modify the network configuration. I'm not fully familiar with the broader codebase, so I'll leave it to you to assess the severity of this.

This should not be a problem as it happens in the same endpoint as we just added the check. So any update mechanism will not start a stream nor post.

I have just pushed two changes to address some test failures and comments for future. Please take a look @Thifhi, thanks.

@Thifhi
Copy link
Contributor Author

Thifhi commented Jun 6, 2025

Looks good , thank you

@Thifhi
Copy link
Contributor Author

Thifhi commented Jun 6, 2025

Though still failing 😕

@kradalby
Copy link
Collaborator

kradalby commented Jun 6, 2025

Though still failing 😕

I think it is only github actions having a bad time, im gonna pull it down and run the ones locally.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
@kradalby kradalby merged commit bad7833 into juanfont:main Jun 6, 2025
140 of 150 checks passed
kradalby added a commit to kradalby/headscale that referenced this pull request Jun 6, 2025
* 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>
EdGeraghty pushed a commit to privacyint/docker-headscale that referenced this pull request Jun 6, 2025
mazlumtoprak pushed a commit to mazlumtoprak/headscale that referenced this pull request Jul 24, 2025
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants