Skip to content

Commit

Permalink
resolve user identifier to stable ID
Browse files Browse the repository at this point in the history
currently, the policy approach node to user matching
with a quite naive approach looking at the username
provided in the policy and matched it with the username
on the nodes. This worked ok as long as usernames were
unique and did not change.

As usernames are no longer guarenteed to be unique in
an OIDC environment we cant rely on this.

This changes the mechanism that matches the user string
(now user token) with nodes:

- first find all potential users by looking up:
  - database ID
  - provider ID (OIDC)
  - username/email

If more than one user is matching, then the query is
rejected, and zero matching nodes are returned.

When a single user is found, the node is matched against
the User database ID, which are also present on the actual
node.

This means that from this commit, users can use the following
to identify users in the policy:
- provider identity (iss + sub)
- username
- email
- database id

There are more changes coming to this, so it is not recommended
to start using any of these new abilities, with the exception
of email, which will not change since it includes an @.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
  • Loading branch information
kradalby committed Oct 21, 2024
1 parent 028d9aa commit f6d3c48
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 129 deletions.
8 changes: 6 additions & 2 deletions hscontrol/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1026,14 +1026,18 @@ func (h *Headscale) loadACLPolicy() error {
if err != nil {
return fmt.Errorf("loading nodes from database to validate policy: %w", err)
}
users, err := h.db.ListUsers()
if err != nil {
return fmt.Errorf("loading users from database to validate policy: %w", err)
}

_, err = pol.CompileFilterRules(nodes)
_, err = pol.CompileFilterRules(users, nodes)
if err != nil {
return fmt.Errorf("verifying policy rules: %w", err)
}

if len(nodes) > 0 {
_, err = pol.CompileSSHPolicy(nodes[0], nodes)
_, err = pol.CompileSSHPolicy(nodes[0], users, nodes)
if err != nil {
return fmt.Errorf("verifying SSH rules: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions hscontrol/db/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,10 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
c.Assert(err, check.IsNil)
c.Assert(len(testPeers), check.Equals, 9)

adminRules, _, err := policy.GenerateFilterAndSSHRulesForTests(aclPolicy, adminNode, adminPeers)
adminRules, _, err := policy.GenerateFilterAndSSHRulesForTests(aclPolicy, adminNode, adminPeers, []types.User{*stor[0].user, *stor[1].user})
c.Assert(err, check.IsNil)

testRules, _, err := policy.GenerateFilterAndSSHRulesForTests(aclPolicy, testNode, testPeers)
testRules, _, err := policy.GenerateFilterAndSSHRulesForTests(aclPolicy, testNode, testPeers, []types.User{*stor[0].user, *stor[1].user})
c.Assert(err, check.IsNil)

peersOfAdminNode := policy.FilterNodesByACL(adminNode, adminPeers, adminRules)
Expand Down
7 changes: 6 additions & 1 deletion hscontrol/db/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -648,8 +648,13 @@ func EnableAutoApprovedRoutes(
if approvedAlias == node.User.Username() {
approvedRoutes = append(approvedRoutes, advertisedRoute)
} else {
users, err := ListUsers(tx)
if err != nil {
return fmt.Errorf("looking up users to expand route alias: %w", err)
}

// TODO(kradalby): figure out how to get this to depend on less stuff
approvedIps, err := aclPolicy.ExpandAlias(types.Nodes{node}, approvedAlias)
approvedIps, err := aclPolicy.ExpandAlias(types.Nodes{node}, users, approvedAlias)
if err != nil {
return fmt.Errorf("expanding alias %q for autoApprovers: %w", approvedAlias, err)
}
Expand Down
8 changes: 6 additions & 2 deletions hscontrol/grpcv1.go
Original file line number Diff line number Diff line change
Expand Up @@ -737,14 +737,18 @@ func (api headscaleV1APIServer) SetPolicy(
if err != nil {
return nil, fmt.Errorf("loading nodes from database to validate policy: %w", err)
}
users, err := api.h.db.ListUsers()
if err != nil {
return nil, fmt.Errorf("loading users from database to validate policy: %w", err)
}

_, err = pol.CompileFilterRules(nodes)
_, err = pol.CompileFilterRules(users, nodes)
if err != nil {
return nil, fmt.Errorf("verifying policy rules: %w", err)
}

if len(nodes) > 0 {
_, err = pol.CompileSSHPolicy(nodes[0], nodes)
_, err = pol.CompileSSHPolicy(nodes[0], users, nodes)
if err != nil {
return nil, fmt.Errorf("verifying SSH rules: %w", err)
}
Expand Down
19 changes: 16 additions & 3 deletions hscontrol/mapper/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ func addNextDNSMetadata(resolvers []*dnstype.Resolver, node *types.Node) {
func (m *Mapper) fullMapResponse(
node *types.Node,
peers types.Nodes,
users []types.User,
pol *policy.ACLPolicy,
capVer tailcfg.CapabilityVersion,
) (*tailcfg.MapResponse, error) {
Expand All @@ -169,6 +170,7 @@ func (m *Mapper) fullMapResponse(
pol,
node,
capVer,
users,
peers,
peers,
m.cfg,
Expand All @@ -191,8 +193,12 @@ func (m *Mapper) FullMapResponse(
if err != nil {
return nil, err
}
users, err := m.db.ListUsers()
if err != nil {
return nil, err
}

resp, err := m.fullMapResponse(node, peers, pol, mapRequest.Version)
resp, err := m.fullMapResponse(node, peers, users, pol, mapRequest.Version)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -255,6 +261,11 @@ func (m *Mapper) PeerChangedResponse(
return nil, err
}

users, err := m.db.ListUsers()
if err != nil {
return nil, fmt.Errorf("listing users for map response: %w", err)
}

var removedIDs []tailcfg.NodeID
var changedIDs []types.NodeID
for nodeID, nodeChanged := range changed {
Expand All @@ -278,6 +289,7 @@ func (m *Mapper) PeerChangedResponse(
pol,
node,
mapRequest.Version,
users,
peers,
changedNodes,
m.cfg,
Expand Down Expand Up @@ -510,16 +522,17 @@ func appendPeerChanges(
pol *policy.ACLPolicy,
node *types.Node,
capVer tailcfg.CapabilityVersion,
users []types.User,
peers types.Nodes,
changed types.Nodes,
cfg *types.Config,
) error {
packetFilter, err := pol.CompileFilterRules(append(peers, node))
packetFilter, err := pol.CompileFilterRules(users, append(peers, node))
if err != nil {
return err
}

sshPolicy, err := pol.CompileSSHPolicy(node, peers)
sshPolicy, err := pol.CompileSSHPolicy(node, users, peers)
if err != nil {
return err
}
Expand Down
16 changes: 10 additions & 6 deletions hscontrol/mapper/mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ func Test_fullMapResponse(t *testing.T) {
lastSeen := time.Date(2009, time.November, 10, 23, 9, 0, 0, time.UTC)
expire := time.Date(2500, time.November, 11, 23, 0, 0, 0, time.UTC)

user1 := types.User{Model: gorm.Model{ID: 0}, Name: "mini"}
user2 := types.User{Model: gorm.Model{ID: 1}, Name: "peer2"}

mini := &types.Node{
ID: 0,
MachineKey: mustMK(
Expand All @@ -185,8 +188,8 @@ func Test_fullMapResponse(t *testing.T) {
IPv4: iap("100.64.0.1"),
Hostname: "mini",
GivenName: "mini",
UserID: 0,
User: types.User{Name: "mini"},
UserID: user1.ID,
User: user1,
ForcedTags: []string{},
AuthKey: &types.PreAuthKey{},
LastSeen: &lastSeen,
Expand Down Expand Up @@ -265,8 +268,8 @@ func Test_fullMapResponse(t *testing.T) {
IPv4: iap("100.64.0.2"),
Hostname: "peer1",
GivenName: "peer1",
UserID: 0,
User: types.User{Name: "mini"},
UserID: user1.ID,
User: user1,
ForcedTags: []string{},
LastSeen: &lastSeen,
Expiry: &expire,
Expand Down Expand Up @@ -320,8 +323,8 @@ func Test_fullMapResponse(t *testing.T) {
IPv4: iap("100.64.0.3"),
Hostname: "peer2",
GivenName: "peer2",
UserID: 1,
User: types.User{Name: "peer2"},
UserID: user2.ID,
User: user2,
ForcedTags: []string{},
LastSeen: &lastSeen,
Expiry: &expire,
Expand Down Expand Up @@ -480,6 +483,7 @@ func Test_fullMapResponse(t *testing.T) {
got, err := mappy.fullMapResponse(
tt.node,
tt.peers,
[]types.User{user1, user2},
tt.pol,
0,
)
Expand Down
Loading

0 comments on commit f6d3c48

Please sign in to comment.