1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-08-10 13:46:46 +02:00

policy: replace old ssh tests with more granular test

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2025-05-02 15:22:30 +02:00
parent 5347a39cc1
commit c5ea2d4aae
No known key found for this signature in database
2 changed files with 384 additions and 194 deletions

View File

@ -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)
}
})
}
}
}

View File

@ -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