diff --git a/hscontrol/policy/policy_test.go b/hscontrol/policy/policy_test.go index 671ed829..05129a7b 100644 --- a/hscontrol/policy/policy_test.go +++ b/hscontrol/policy/policy_test.go @@ -4,6 +4,9 @@ import ( "fmt" "net/netip" "testing" + "time" + + "github.com/juanfont/headscale/hscontrol/policy/matcher" "github.com/juanfont/headscale/hscontrol/policy/matcher" @@ -1540,3 +1543,384 @@ func TestFilterNodesByACL(t *testing.T) { }) } } + +func TestSSHPolicyRules(t *testing.T) { + users := []types.User{ + {Name: "user1", Model: gorm.Model{ID: 1}}, + {Name: "user2", Model: gorm.Model{ID: 2}}, + {Name: "user3", Model: gorm.Model{ID: 3}}, + } + + // Create standard node setups used across tests + nodeUser1 := types.Node{ + Hostname: "user1-device", + IPv4: ap("100.64.0.1"), + UserID: 1, + User: users[0], + } + nodeUser2 := types.Node{ + Hostname: "user2-device", + IPv4: ap("100.64.0.2"), + UserID: 2, + User: users[1], + } + taggedServer := types.Node{ + Hostname: "tagged-server", + IPv4: ap("100.64.0.3"), + UserID: 3, + User: users[2], + ForcedTags: []string{"tag:server"}, + } + taggedClient := types.Node{ + Hostname: "tagged-client", + IPv4: ap("100.64.0.4"), + UserID: 2, + User: users[1], + ForcedTags: []string{"tag:client"}, + } + + tests := []struct { + name string + targetNode types.Node + peers types.Nodes + policy string + wantSSH *tailcfg.SSHPolicy + expectErr bool + errorMessage string + }{ + { + name: "group-to-user", + targetNode: nodeUser1, + peers: types.Nodes{&nodeUser2}, + policy: `{ + "groups": { + "group:admins": ["user2@"] + }, + "ssh": [ + { + "action": "accept", + "src": ["group:admins"], + "dst": ["user1@"], + "users": ["autogroup:nonroot"] + } + ] + }`, + wantSSH: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{ + { + Principals: []*tailcfg.SSHPrincipal{ + {NodeIP: "100.64.0.2"}, + }, + SSHUsers: map[string]string{ + "autogroup:nonroot": "=", + }, + Action: &tailcfg.SSHAction{ + Accept: true, + AllowAgentForwarding: true, + AllowLocalPortForwarding: true, + }, + }, + }}, + }, + { + name: "group-to-tag", + targetNode: taggedServer, + peers: types.Nodes{&nodeUser1, &nodeUser2}, + policy: `{ + "groups": { + "group:users": ["user1@", "user2@"] + }, + "ssh": [ + { + "action": "accept", + "src": ["group:users"], + "dst": ["tag:server"], + "users": ["autogroup:nonroot"] + } + ] + }`, + wantSSH: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{ + { + Principals: []*tailcfg.SSHPrincipal{ + {NodeIP: "100.64.0.1"}, + {NodeIP: "100.64.0.2"}, + }, + SSHUsers: map[string]string{ + "autogroup:nonroot": "=", + }, + Action: &tailcfg.SSHAction{ + Accept: true, + AllowAgentForwarding: true, + AllowLocalPortForwarding: true, + }, + }, + }}, + }, + { + name: "tag-to-user", + targetNode: nodeUser1, + peers: types.Nodes{&taggedClient}, + policy: `{ + "ssh": [ + { + "action": "accept", + "src": ["tag:client"], + "dst": ["user1@"], + "users": ["autogroup:nonroot"] + } + ] + }`, + wantSSH: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{ + { + Principals: []*tailcfg.SSHPrincipal{ + {NodeIP: "100.64.0.4"}, + }, + SSHUsers: map[string]string{ + "autogroup:nonroot": "=", + }, + Action: &tailcfg.SSHAction{ + Accept: true, + AllowAgentForwarding: true, + AllowLocalPortForwarding: true, + }, + }, + }}, + }, + { + name: "tag-to-tag", + targetNode: taggedServer, + peers: types.Nodes{&taggedClient}, + policy: `{ + "ssh": [ + { + "action": "accept", + "src": ["tag:client"], + "dst": ["tag:server"], + "users": ["autogroup:nonroot"] + } + ] + }`, + wantSSH: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{ + { + Principals: []*tailcfg.SSHPrincipal{ + {NodeIP: "100.64.0.4"}, + }, + SSHUsers: map[string]string{ + "autogroup:nonroot": "=", + }, + Action: &tailcfg.SSHAction{ + Accept: true, + AllowAgentForwarding: true, + AllowLocalPortForwarding: true, + }, + }, + }}, + }, + { + name: "group-to-wildcard", + targetNode: nodeUser1, + peers: types.Nodes{&nodeUser2, &taggedClient}, + policy: `{ + "groups": { + "group:admins": ["user2@"] + }, + "ssh": [ + { + "action": "accept", + "src": ["group:admins"], + "dst": ["*"], + "users": ["autogroup:nonroot"] + } + ] + }`, + wantSSH: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{ + { + Principals: []*tailcfg.SSHPrincipal{ + {NodeIP: "100.64.0.2"}, + }, + SSHUsers: map[string]string{ + "autogroup:nonroot": "=", + }, + Action: &tailcfg.SSHAction{ + Accept: true, + AllowAgentForwarding: true, + AllowLocalPortForwarding: true, + }, + }, + }}, + }, + { + name: "invalid-source-user-not-allowed", + targetNode: nodeUser1, + peers: types.Nodes{&nodeUser2}, + policy: `{ + "ssh": [ + { + "action": "accept", + "src": ["user2@"], + "dst": ["user1@"], + "users": ["autogroup:nonroot"] + } + ] + }`, + expectErr: true, + errorMessage: "not supported", + }, + { + name: "check-period-specified", + targetNode: nodeUser1, + peers: types.Nodes{&taggedClient}, + policy: `{ + "ssh": [ + { + "action": "check", + "checkPeriod": "24h", + "src": ["tag:client"], + "dst": ["user1@"], + "users": ["autogroup:nonroot"] + } + ] + }`, + wantSSH: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{ + { + Principals: []*tailcfg.SSHPrincipal{ + {NodeIP: "100.64.0.4"}, + }, + SSHUsers: map[string]string{ + "autogroup:nonroot": "=", + }, + Action: &tailcfg.SSHAction{ + Accept: true, + SessionDuration: 24 * time.Hour, + AllowAgentForwarding: true, + AllowLocalPortForwarding: true, + }, + }, + }}, + }, + { + name: "no-matching-rules", + targetNode: nodeUser2, + peers: types.Nodes{&nodeUser1}, + policy: `{ + "ssh": [ + { + "action": "accept", + "src": ["tag:client"], + "dst": ["user1@"], + "users": ["autogroup:nonroot"] + } + ] + }`, + wantSSH: &tailcfg.SSHPolicy{Rules: nil}, + }, + { + name: "invalid-action", + targetNode: nodeUser1, + peers: types.Nodes{&nodeUser2}, + policy: `{ + "ssh": [ + { + "action": "invalid", + "src": ["group:admins"], + "dst": ["user1@"], + "users": ["autogroup:nonroot"] + } + ] + }`, + expectErr: true, + errorMessage: `SSH action "invalid" is not valid, must be accept or check`, + }, + { + name: "invalid-check-period", + targetNode: nodeUser1, + peers: types.Nodes{&nodeUser2}, + policy: `{ + "ssh": [ + { + "action": "check", + "checkPeriod": "invalid", + "src": ["group:admins"], + "dst": ["user1@"], + "users": ["autogroup:nonroot"] + } + ] + }`, + expectErr: true, + errorMessage: "not a valid duration string", + }, + { + name: "multiple-ssh-users-with-autogroup", + targetNode: nodeUser1, + peers: types.Nodes{&taggedClient}, + policy: `{ + "ssh": [ + { + "action": "accept", + "src": ["tag:client"], + "dst": ["user1@"], + "users": ["alice", "bob"] + } + ] + }`, + wantSSH: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{ + { + Principals: []*tailcfg.SSHPrincipal{ + {NodeIP: "100.64.0.4"}, + }, + SSHUsers: map[string]string{ + "alice": "=", + "bob": "=", + }, + Action: &tailcfg.SSHAction{ + Accept: true, + AllowAgentForwarding: true, + AllowLocalPortForwarding: true, + }, + }, + }}, + }, + { + name: "unsupported-autogroup", + targetNode: nodeUser1, + peers: types.Nodes{&taggedClient}, + policy: `{ + "ssh": [ + { + "action": "accept", + "src": ["tag:client"], + "dst": ["user1@"], + "users": ["autogroup:invalid"] + } + ] + }`, + expectErr: true, + errorMessage: "autogroup \"autogroup:invalid\" is not supported", + }, + } + + for _, tt := range tests { + for idx, pmf := range PolicyManagerFuncsForTest([]byte(tt.policy)) { + version := idx + 1 + t.Run(fmt.Sprintf("%s-v%d", tt.name, version), func(t *testing.T) { + var pm PolicyManager + var err error + pm, err = pmf(users, append(tt.peers, &tt.targetNode)) + + if tt.expectErr { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMessage) + return + } + + require.NoError(t, err) + + got, err := pm.SSHPolicy(&tt.targetNode) + require.NoError(t, err) + + if diff := cmp.Diff(tt.wantSSH, got); diff != "" { + t.Errorf("SSHPolicy() unexpected result (-want +got):\n%s", diff) + } + }) + } + } +} diff --git a/hscontrol/policy/v1/acls_test.go b/hscontrol/policy/v1/acls_test.go index 03dcd431..f2871064 100644 --- a/hscontrol/policy/v1/acls_test.go +++ b/hscontrol/policy/v1/acls_test.go @@ -2159,200 +2159,6 @@ func Test_getTags(t *testing.T) { } } -func TestSSHRules(t *testing.T) { - users := []types.User{ - { - Name: "user1", - }, - } - tests := []struct { - name string - node types.Node - peers types.Nodes - pol ACLPolicy - want *tailcfg.SSHPolicy - }{ - { - name: "peers-can-connect", - node: types.Node{ - Hostname: "testnodes", - IPv4: iap("100.64.99.42"), - UserID: 0, - User: users[0], - }, - peers: types.Nodes{ - &types.Node{ - Hostname: "testnodes2", - IPv4: iap("100.64.0.1"), - UserID: 0, - User: users[0], - }, - }, - pol: ACLPolicy{ - Groups: Groups{ - "group:test": []string{"user1"}, - }, - Hosts: Hosts{ - "client": netip.PrefixFrom(netip.MustParseAddr("100.64.99.42"), 32), - }, - ACLs: []ACL{ - { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, - }, - }, - SSHs: []SSH{ - { - Action: "accept", - Sources: []string{"group:test"}, - Destinations: []string{"client"}, - Users: []string{"autogroup:nonroot"}, - }, - { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"client"}, - Users: []string{"autogroup:nonroot"}, - }, - { - Action: "accept", - Sources: []string{"group:test"}, - Destinations: []string{"100.64.99.42"}, - Users: []string{"autogroup:nonroot"}, - }, - { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"100.64.99.42"}, - Users: []string{"autogroup:nonroot"}, - }, - }, - }, - want: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{ - { - Principals: []*tailcfg.SSHPrincipal{ - { - UserLogin: "user1", - }, - }, - SSHUsers: map[string]string{ - "autogroup:nonroot": "=", - }, - Action: &tailcfg.SSHAction{ - Accept: true, - AllowAgentForwarding: true, - AllowLocalPortForwarding: true, - }, - }, - { - SSHUsers: map[string]string{ - "autogroup:nonroot": "=", - }, - Principals: []*tailcfg.SSHPrincipal{ - { - Any: true, - }, - }, - Action: &tailcfg.SSHAction{ - Accept: true, - AllowAgentForwarding: true, - AllowLocalPortForwarding: true, - }, - }, - { - Principals: []*tailcfg.SSHPrincipal{ - { - UserLogin: "user1", - }, - }, - SSHUsers: map[string]string{ - "autogroup:nonroot": "=", - }, - Action: &tailcfg.SSHAction{ - Accept: true, - AllowAgentForwarding: true, - AllowLocalPortForwarding: true, - }, - }, - { - SSHUsers: map[string]string{ - "autogroup:nonroot": "=", - }, - Principals: []*tailcfg.SSHPrincipal{ - { - Any: true, - }, - }, - Action: &tailcfg.SSHAction{ - Accept: true, - AllowAgentForwarding: true, - AllowLocalPortForwarding: true, - }, - }, - }}, - }, - { - name: "peers-cannot-connect", - node: types.Node{ - Hostname: "testnodes", - IPv4: iap("100.64.0.1"), - UserID: 0, - User: users[0], - }, - peers: types.Nodes{ - &types.Node{ - Hostname: "testnodes2", - IPv4: iap("100.64.99.42"), - UserID: 0, - User: users[0], - }, - }, - pol: ACLPolicy{ - Groups: Groups{ - "group:test": []string{"user1"}, - }, - Hosts: Hosts{ - "client": netip.PrefixFrom(netip.MustParseAddr("100.64.99.42"), 32), - }, - ACLs: []ACL{ - { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, - }, - }, - SSHs: []SSH{ - { - Action: "accept", - Sources: []string{"group:test"}, - Destinations: []string{"100.64.99.42"}, - Users: []string{"autogroup:nonroot"}, - }, - { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"100.64.99.42"}, - Users: []string{"autogroup:nonroot"}, - }, - }, - }, - want: &tailcfg.SSHPolicy{Rules: nil}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := tt.pol.CompileSSHPolicy(&tt.node, users, tt.peers) - require.NoError(t, err) - - if diff := cmp.Diff(tt.want, got); diff != "" { - t.Errorf("TestSSHRules() unexpected result (-want +got):\n%s", diff) - } - }) - } -} - func TestParseDestination(t *testing.T) { tests := []struct { dest string