mirror of
https://github.com/juanfont/headscale.git
synced 2026-01-23 02:24:10 +00:00
policy: update tests for SSH validation rules
Update unit tests to use valid SSH patterns that conform to Tailscale's security model: - Change group->user destinations to group->tag - Change tag->user destinations to tag->tag - Update expected error messages for new validation format - Add proper tagged/untagged node setup in filter tests Updates #3009 Updates #3010
This commit is contained in:
parent
5688c201e9
commit
d40203e153
3 changed files with 407 additions and 77 deletions
|
|
@ -1092,6 +1092,15 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
Tags: []string{"tag:client"},
|
Tags: []string{"tag:client"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a tagged server node for valid SSH patterns
|
||||||
|
nodeTaggedServer := types.Node{
|
||||||
|
Hostname: "tagged-server",
|
||||||
|
IPv4: ap("100.64.0.5"),
|
||||||
|
UserID: ptr.To(uint(1)),
|
||||||
|
User: ptr.To(users[0]),
|
||||||
|
Tags: []string{"tag:server"},
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
targetNode types.Node
|
targetNode types.Node
|
||||||
|
|
@ -1102,10 +1111,13 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
errorMessage string
|
errorMessage string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "group-to-user",
|
name: "group-to-tag",
|
||||||
targetNode: nodeUser1,
|
targetNode: nodeTaggedServer,
|
||||||
peers: types.Nodes{&nodeUser2},
|
peers: types.Nodes{&nodeUser2},
|
||||||
policy: `{
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:server": ["user1@"]
|
||||||
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"group:admins": ["user2@"]
|
"group:admins": ["user2@"]
|
||||||
},
|
},
|
||||||
|
|
@ -1113,7 +1125,7 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"src": ["group:admins"],
|
"src": ["group:admins"],
|
||||||
"dst": ["user1@"],
|
"dst": ["tag:server"],
|
||||||
"users": ["autogroup:nonroot"]
|
"users": ["autogroup:nonroot"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -1138,18 +1150,21 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "check-period-specified",
|
name: "check-period-specified",
|
||||||
targetNode: nodeUser1,
|
targetNode: taggedClient,
|
||||||
peers: types.Nodes{&taggedClient},
|
peers: types.Nodes{&nodeUser2},
|
||||||
policy: `{
|
policy: `{
|
||||||
"tagOwners": {
|
"tagOwners": {
|
||||||
"tag:client": ["user1@"],
|
"tag:client": ["user1@"]
|
||||||
|
},
|
||||||
|
"groups": {
|
||||||
|
"group:admins": ["user2@"]
|
||||||
},
|
},
|
||||||
"ssh": [
|
"ssh": [
|
||||||
{
|
{
|
||||||
"action": "check",
|
"action": "check",
|
||||||
"checkPeriod": "24h",
|
"checkPeriod": "24h",
|
||||||
"src": ["tag:client"],
|
"src": ["group:admins"],
|
||||||
"dst": ["user1@"],
|
"dst": ["tag:client"],
|
||||||
"users": ["autogroup:nonroot"]
|
"users": ["autogroup:nonroot"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -1157,7 +1172,7 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
wantSSH: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{
|
wantSSH: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{
|
||||||
{
|
{
|
||||||
Principals: []*tailcfg.SSHPrincipal{
|
Principals: []*tailcfg.SSHPrincipal{
|
||||||
{NodeIP: "100.64.0.4"},
|
{NodeIP: "100.64.0.2"},
|
||||||
},
|
},
|
||||||
SSHUsers: map[string]string{
|
SSHUsers: map[string]string{
|
||||||
"*": "=",
|
"*": "=",
|
||||||
|
|
@ -1176,16 +1191,19 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "no-matching-rules",
|
name: "no-matching-rules",
|
||||||
targetNode: nodeUser2,
|
targetNode: nodeUser2,
|
||||||
peers: types.Nodes{&nodeUser1},
|
peers: types.Nodes{&nodeUser1, &nodeTaggedServer},
|
||||||
policy: `{
|
policy: `{
|
||||||
"tagOwners": {
|
"tagOwners": {
|
||||||
"tag:client": ["user1@"],
|
"tag:server": ["user1@"]
|
||||||
},
|
},
|
||||||
|
"groups": {
|
||||||
|
"group:admins": ["user1@"]
|
||||||
|
},
|
||||||
"ssh": [
|
"ssh": [
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"src": ["tag:client"],
|
"src": ["group:admins"],
|
||||||
"dst": ["user1@"],
|
"dst": ["tag:server"],
|
||||||
"users": ["autogroup:nonroot"]
|
"users": ["autogroup:nonroot"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -1194,14 +1212,20 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid-action",
|
name: "invalid-action",
|
||||||
targetNode: nodeUser1,
|
targetNode: nodeTaggedServer,
|
||||||
peers: types.Nodes{&nodeUser2},
|
peers: types.Nodes{&nodeUser2},
|
||||||
policy: `{
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:server": ["user1@"]
|
||||||
|
},
|
||||||
|
"groups": {
|
||||||
|
"group:admins": ["user2@"]
|
||||||
|
},
|
||||||
"ssh": [
|
"ssh": [
|
||||||
{
|
{
|
||||||
"action": "invalid",
|
"action": "invalid",
|
||||||
"src": ["group:admins"],
|
"src": ["group:admins"],
|
||||||
"dst": ["user1@"],
|
"dst": ["tag:server"],
|
||||||
"users": ["autogroup:nonroot"]
|
"users": ["autogroup:nonroot"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -1211,15 +1235,21 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid-check-period",
|
name: "invalid-check-period",
|
||||||
targetNode: nodeUser1,
|
targetNode: nodeTaggedServer,
|
||||||
peers: types.Nodes{&nodeUser2},
|
peers: types.Nodes{&nodeUser2},
|
||||||
policy: `{
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:server": ["user1@"]
|
||||||
|
},
|
||||||
|
"groups": {
|
||||||
|
"group:admins": ["user2@"]
|
||||||
|
},
|
||||||
"ssh": [
|
"ssh": [
|
||||||
{
|
{
|
||||||
"action": "check",
|
"action": "check",
|
||||||
"checkPeriod": "invalid",
|
"checkPeriod": "invalid",
|
||||||
"src": ["group:admins"],
|
"src": ["group:admins"],
|
||||||
"dst": ["user1@"],
|
"dst": ["tag:server"],
|
||||||
"users": ["autogroup:nonroot"]
|
"users": ["autogroup:nonroot"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -1229,26 +1259,12 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unsupported-autogroup",
|
name: "unsupported-autogroup",
|
||||||
targetNode: nodeUser1,
|
targetNode: taggedClient,
|
||||||
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",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "autogroup-nonroot-should-use-wildcard-with-root-excluded",
|
|
||||||
targetNode: nodeUser1,
|
|
||||||
peers: types.Nodes{&nodeUser2},
|
peers: types.Nodes{&nodeUser2},
|
||||||
policy: `{
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:client": ["user1@"]
|
||||||
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"group:admins": ["user2@"]
|
"group:admins": ["user2@"]
|
||||||
},
|
},
|
||||||
|
|
@ -1256,7 +1272,30 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"src": ["group:admins"],
|
"src": ["group:admins"],
|
||||||
"dst": ["user1@"],
|
"dst": ["tag:client"],
|
||||||
|
"users": ["autogroup:invalid"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
expectErr: true,
|
||||||
|
errorMessage: "autogroup \"autogroup:invalid\" is not supported",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "autogroup-nonroot-should-use-wildcard-with-root-excluded",
|
||||||
|
targetNode: nodeTaggedServer,
|
||||||
|
peers: types.Nodes{&nodeUser2},
|
||||||
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:server": ["user1@"]
|
||||||
|
},
|
||||||
|
"groups": {
|
||||||
|
"group:admins": ["user2@"]
|
||||||
|
},
|
||||||
|
"ssh": [
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["group:admins"],
|
||||||
|
"dst": ["tag:server"],
|
||||||
"users": ["autogroup:nonroot"]
|
"users": ["autogroup:nonroot"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -1282,9 +1321,12 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "autogroup-nonroot-plus-root-should-use-wildcard-with-root-mapped",
|
name: "autogroup-nonroot-plus-root-should-use-wildcard-with-root-mapped",
|
||||||
targetNode: nodeUser1,
|
targetNode: nodeTaggedServer,
|
||||||
peers: types.Nodes{&nodeUser2},
|
peers: types.Nodes{&nodeUser2},
|
||||||
policy: `{
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:server": ["user1@"]
|
||||||
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"group:admins": ["user2@"]
|
"group:admins": ["user2@"]
|
||||||
},
|
},
|
||||||
|
|
@ -1292,7 +1334,7 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"src": ["group:admins"],
|
"src": ["group:admins"],
|
||||||
"dst": ["user1@"],
|
"dst": ["tag:server"],
|
||||||
"users": ["autogroup:nonroot", "root"]
|
"users": ["autogroup:nonroot", "root"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -1318,9 +1360,12 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific-users-should-map-to-themselves-not-equals",
|
name: "specific-users-should-map-to-themselves-not-equals",
|
||||||
targetNode: nodeUser1,
|
targetNode: nodeTaggedServer,
|
||||||
peers: types.Nodes{&nodeUser2},
|
peers: types.Nodes{&nodeUser2},
|
||||||
policy: `{
|
policy: `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:server": ["user1@"]
|
||||||
|
},
|
||||||
"groups": {
|
"groups": {
|
||||||
"group:admins": ["user2@"]
|
"group:admins": ["user2@"]
|
||||||
},
|
},
|
||||||
|
|
@ -1328,7 +1373,7 @@ func TestSSHPolicyRules(t *testing.T) {
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"src": ["group:admins"],
|
"src": ["group:admins"],
|
||||||
"dst": ["user1@"],
|
"dst": ["tag:server"],
|
||||||
"users": ["ubuntu", "root"]
|
"users": ["ubuntu", "root"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -406,21 +406,33 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
{Name: "user2", Model: gorm.Model{ID: 2}},
|
{Name: "user2", Model: gorm.Model{ID: 2}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create test nodes
|
// Create test nodes - use tagged nodes as SSH destinations
|
||||||
nodeUser1 := types.Node{
|
// and untagged nodes as SSH sources (since group->username destinations
|
||||||
Hostname: "user1-device",
|
// are not allowed per Tailscale security model, but groups can SSH to tags)
|
||||||
|
nodeTaggedServer := types.Node{
|
||||||
|
Hostname: "tagged-server",
|
||||||
IPv4: createAddr("100.64.0.1"),
|
IPv4: createAddr("100.64.0.1"),
|
||||||
UserID: ptr.To(users[0].ID),
|
UserID: ptr.To(users[0].ID),
|
||||||
User: ptr.To(users[0]),
|
User: ptr.To(users[0]),
|
||||||
|
Tags: []string{"tag:server"},
|
||||||
}
|
}
|
||||||
nodeUser2 := types.Node{
|
nodeTaggedDB := types.Node{
|
||||||
Hostname: "user2-device",
|
Hostname: "tagged-db",
|
||||||
IPv4: createAddr("100.64.0.2"),
|
IPv4: createAddr("100.64.0.2"),
|
||||||
UserID: ptr.To(users[1].ID),
|
UserID: ptr.To(users[1].ID),
|
||||||
User: ptr.To(users[1]),
|
User: ptr.To(users[1]),
|
||||||
|
Tags: []string{"tag:database"},
|
||||||
|
}
|
||||||
|
// Add untagged node for user2 - this will be the SSH source
|
||||||
|
// (group:admins contains user2, so user2's untagged node provides the source IPs)
|
||||||
|
nodeUser2Untagged := types.Node{
|
||||||
|
Hostname: "user2-device",
|
||||||
|
IPv4: createAddr("100.64.0.3"),
|
||||||
|
UserID: ptr.To(users[1].ID),
|
||||||
|
User: ptr.To(users[1]),
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes := types.Nodes{&nodeUser1, &nodeUser2}
|
nodes := types.Nodes{&nodeTaggedServer, &nodeTaggedDB, &nodeUser2Untagged}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -431,8 +443,11 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "specific user mapping",
|
name: "specific user mapping",
|
||||||
targetNode: nodeUser1,
|
targetNode: nodeTaggedServer,
|
||||||
policy: &Policy{
|
policy: &Policy{
|
||||||
|
TagOwners: TagOwners{
|
||||||
|
Tag("tag:server"): Owners{up("user1@")},
|
||||||
|
},
|
||||||
Groups: Groups{
|
Groups: Groups{
|
||||||
Group("group:admins"): []Username{Username("user2@")},
|
Group("group:admins"): []Username{Username("user2@")},
|
||||||
},
|
},
|
||||||
|
|
@ -440,7 +455,7 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
Sources: SSHSrcAliases{gp("group:admins")},
|
Sources: SSHSrcAliases{gp("group:admins")},
|
||||||
Destinations: SSHDstAliases{up("user1@")},
|
Destinations: SSHDstAliases{tp("tag:server")},
|
||||||
Users: []SSHUser{"ssh-it-user"},
|
Users: []SSHUser{"ssh-it-user"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -451,8 +466,11 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple specific users",
|
name: "multiple specific users",
|
||||||
targetNode: nodeUser1,
|
targetNode: nodeTaggedServer,
|
||||||
policy: &Policy{
|
policy: &Policy{
|
||||||
|
TagOwners: TagOwners{
|
||||||
|
Tag("tag:server"): Owners{up("user1@")},
|
||||||
|
},
|
||||||
Groups: Groups{
|
Groups: Groups{
|
||||||
Group("group:admins"): []Username{Username("user2@")},
|
Group("group:admins"): []Username{Username("user2@")},
|
||||||
},
|
},
|
||||||
|
|
@ -460,7 +478,7 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
Sources: SSHSrcAliases{gp("group:admins")},
|
Sources: SSHSrcAliases{gp("group:admins")},
|
||||||
Destinations: SSHDstAliases{up("user1@")},
|
Destinations: SSHDstAliases{tp("tag:server")},
|
||||||
Users: []SSHUser{"ubuntu", "admin", "deploy"},
|
Users: []SSHUser{"ubuntu", "admin", "deploy"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -473,8 +491,11 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "autogroup:nonroot only",
|
name: "autogroup:nonroot only",
|
||||||
targetNode: nodeUser1,
|
targetNode: nodeTaggedServer,
|
||||||
policy: &Policy{
|
policy: &Policy{
|
||||||
|
TagOwners: TagOwners{
|
||||||
|
Tag("tag:server"): Owners{up("user1@")},
|
||||||
|
},
|
||||||
Groups: Groups{
|
Groups: Groups{
|
||||||
Group("group:admins"): []Username{Username("user2@")},
|
Group("group:admins"): []Username{Username("user2@")},
|
||||||
},
|
},
|
||||||
|
|
@ -482,7 +503,7 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
Sources: SSHSrcAliases{gp("group:admins")},
|
Sources: SSHSrcAliases{gp("group:admins")},
|
||||||
Destinations: SSHDstAliases{up("user1@")},
|
Destinations: SSHDstAliases{tp("tag:server")},
|
||||||
Users: []SSHUser{SSHUser(AutoGroupNonRoot)},
|
Users: []SSHUser{SSHUser(AutoGroupNonRoot)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -494,8 +515,11 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "root only",
|
name: "root only",
|
||||||
targetNode: nodeUser1,
|
targetNode: nodeTaggedServer,
|
||||||
policy: &Policy{
|
policy: &Policy{
|
||||||
|
TagOwners: TagOwners{
|
||||||
|
Tag("tag:server"): Owners{up("user1@")},
|
||||||
|
},
|
||||||
Groups: Groups{
|
Groups: Groups{
|
||||||
Group("group:admins"): []Username{Username("user2@")},
|
Group("group:admins"): []Username{Username("user2@")},
|
||||||
},
|
},
|
||||||
|
|
@ -503,7 +527,7 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
Sources: SSHSrcAliases{gp("group:admins")},
|
Sources: SSHSrcAliases{gp("group:admins")},
|
||||||
Destinations: SSHDstAliases{up("user1@")},
|
Destinations: SSHDstAliases{tp("tag:server")},
|
||||||
Users: []SSHUser{"root"},
|
Users: []SSHUser{"root"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -514,8 +538,11 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "autogroup:nonroot plus root",
|
name: "autogroup:nonroot plus root",
|
||||||
targetNode: nodeUser1,
|
targetNode: nodeTaggedServer,
|
||||||
policy: &Policy{
|
policy: &Policy{
|
||||||
|
TagOwners: TagOwners{
|
||||||
|
Tag("tag:server"): Owners{up("user1@")},
|
||||||
|
},
|
||||||
Groups: Groups{
|
Groups: Groups{
|
||||||
Group("group:admins"): []Username{Username("user2@")},
|
Group("group:admins"): []Username{Username("user2@")},
|
||||||
},
|
},
|
||||||
|
|
@ -523,7 +550,7 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
Sources: SSHSrcAliases{gp("group:admins")},
|
Sources: SSHSrcAliases{gp("group:admins")},
|
||||||
Destinations: SSHDstAliases{up("user1@")},
|
Destinations: SSHDstAliases{tp("tag:server")},
|
||||||
Users: []SSHUser{SSHUser(AutoGroupNonRoot), "root"},
|
Users: []SSHUser{SSHUser(AutoGroupNonRoot), "root"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -535,8 +562,11 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mixed specific users and autogroups",
|
name: "mixed specific users and autogroups",
|
||||||
targetNode: nodeUser1,
|
targetNode: nodeTaggedServer,
|
||||||
policy: &Policy{
|
policy: &Policy{
|
||||||
|
TagOwners: TagOwners{
|
||||||
|
Tag("tag:server"): Owners{up("user1@")},
|
||||||
|
},
|
||||||
Groups: Groups{
|
Groups: Groups{
|
||||||
Group("group:admins"): []Username{Username("user2@")},
|
Group("group:admins"): []Username{Username("user2@")},
|
||||||
},
|
},
|
||||||
|
|
@ -544,7 +574,7 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
Sources: SSHSrcAliases{gp("group:admins")},
|
Sources: SSHSrcAliases{gp("group:admins")},
|
||||||
Destinations: SSHDstAliases{up("user1@")},
|
Destinations: SSHDstAliases{tp("tag:server")},
|
||||||
Users: []SSHUser{SSHUser(AutoGroupNonRoot), "root", "ubuntu", "admin"},
|
Users: []SSHUser{SSHUser(AutoGroupNonRoot), "root", "ubuntu", "admin"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -558,8 +588,12 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no matching destination",
|
name: "no matching destination",
|
||||||
targetNode: nodeUser2, // Target node2, but policy only allows user1
|
targetNode: nodeTaggedDB, // Target tag:database, but policy only allows tag:server
|
||||||
policy: &Policy{
|
policy: &Policy{
|
||||||
|
TagOwners: TagOwners{
|
||||||
|
Tag("tag:server"): Owners{up("user1@")},
|
||||||
|
Tag("tag:database"): Owners{up("user1@")},
|
||||||
|
},
|
||||||
Groups: Groups{
|
Groups: Groups{
|
||||||
Group("group:admins"): []Username{Username("user2@")},
|
Group("group:admins"): []Username{Username("user2@")},
|
||||||
},
|
},
|
||||||
|
|
@ -567,7 +601,7 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
Sources: SSHSrcAliases{gp("group:admins")},
|
Sources: SSHSrcAliases{gp("group:admins")},
|
||||||
Destinations: SSHDstAliases{up("user1@")}, // Only user1, not user2
|
Destinations: SSHDstAliases{tp("tag:server")}, // Only tag:server, not tag:database
|
||||||
Users: []SSHUser{"ssh-it-user"},
|
Users: []SSHUser{"ssh-it-user"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -600,9 +634,9 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
|
||||||
rule := sshPolicy.Rules[0]
|
rule := sshPolicy.Rules[0]
|
||||||
assert.Equal(t, tt.wantSSHUsers, rule.SSHUsers, "SSH users mapping should match expected")
|
assert.Equal(t, tt.wantSSHUsers, rule.SSHUsers, "SSH users mapping should match expected")
|
||||||
|
|
||||||
// Verify principals are set correctly (should contain user2's IP since that's the source)
|
// Verify principals are set correctly (should contain user2's untagged device IP since that's the source)
|
||||||
require.Len(t, rule.Principals, 1)
|
require.Len(t, rule.Principals, 1)
|
||||||
assert.Equal(t, "100.64.0.2", rule.Principals[0].NodeIP)
|
assert.Equal(t, "100.64.0.3", rule.Principals[0].NodeIP)
|
||||||
|
|
||||||
// Verify action is set correctly
|
// Verify action is set correctly
|
||||||
assert.True(t, rule.Action.Accept)
|
assert.True(t, rule.Action.Accept)
|
||||||
|
|
@ -619,11 +653,13 @@ func TestCompileSSHPolicy_CheckAction(t *testing.T) {
|
||||||
{Name: "user2", Model: gorm.Model{ID: 2}},
|
{Name: "user2", Model: gorm.Model{ID: 2}},
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeUser1 := types.Node{
|
// Use tagged nodes for SSH user mapping tests
|
||||||
Hostname: "user1-device",
|
nodeTaggedServer := types.Node{
|
||||||
|
Hostname: "tagged-server",
|
||||||
IPv4: createAddr("100.64.0.1"),
|
IPv4: createAddr("100.64.0.1"),
|
||||||
UserID: ptr.To(users[0].ID),
|
UserID: ptr.To(users[0].ID),
|
||||||
User: ptr.To(users[0]),
|
User: ptr.To(users[0]),
|
||||||
|
Tags: []string{"tag:server"},
|
||||||
}
|
}
|
||||||
nodeUser2 := types.Node{
|
nodeUser2 := types.Node{
|
||||||
Hostname: "user2-device",
|
Hostname: "user2-device",
|
||||||
|
|
@ -632,9 +668,12 @@ func TestCompileSSHPolicy_CheckAction(t *testing.T) {
|
||||||
User: ptr.To(users[1]),
|
User: ptr.To(users[1]),
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes := types.Nodes{&nodeUser1, &nodeUser2}
|
nodes := types.Nodes{&nodeTaggedServer, &nodeUser2}
|
||||||
|
|
||||||
policy := &Policy{
|
policy := &Policy{
|
||||||
|
TagOwners: TagOwners{
|
||||||
|
Tag("tag:server"): Owners{up("user1@")},
|
||||||
|
},
|
||||||
Groups: Groups{
|
Groups: Groups{
|
||||||
Group("group:admins"): []Username{Username("user2@")},
|
Group("group:admins"): []Username{Username("user2@")},
|
||||||
},
|
},
|
||||||
|
|
@ -643,7 +682,7 @@ func TestCompileSSHPolicy_CheckAction(t *testing.T) {
|
||||||
Action: "check",
|
Action: "check",
|
||||||
CheckPeriod: model.Duration(24 * time.Hour),
|
CheckPeriod: model.Duration(24 * time.Hour),
|
||||||
Sources: SSHSrcAliases{gp("group:admins")},
|
Sources: SSHSrcAliases{gp("group:admins")},
|
||||||
Destinations: SSHDstAliases{up("user1@")},
|
Destinations: SSHDstAliases{tp("tag:server")},
|
||||||
Users: []SSHUser{"ssh-it-user"},
|
Users: []SSHUser{"ssh-it-user"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -652,7 +691,7 @@ func TestCompileSSHPolicy_CheckAction(t *testing.T) {
|
||||||
err := policy.validate()
|
err := policy.validate()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
sshPolicy, err := policy.compileSSHPolicy(users, nodeUser1.View(), nodes.ViewSlice())
|
sshPolicy, err := policy.compileSSHPolicy(users, nodeTaggedServer.View(), nodes.ViewSlice())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, sshPolicy)
|
require.NotNil(t, sshPolicy)
|
||||||
require.Len(t, sshPolicy.Rules, 1)
|
require.Len(t, sshPolicy.Rules, 1)
|
||||||
|
|
@ -697,16 +736,17 @@ func TestSSHIntegrationReproduction(t *testing.T) {
|
||||||
nodes := types.Nodes{node1, node2}
|
nodes := types.Nodes{node1, node2}
|
||||||
|
|
||||||
// Create a simple policy that reproduces the issue
|
// Create a simple policy that reproduces the issue
|
||||||
|
// Updated to use autogroup:self instead of username destination (per Tailscale security model)
|
||||||
policy := &Policy{
|
policy := &Policy{
|
||||||
Groups: Groups{
|
Groups: Groups{
|
||||||
Group("group:integration-test"): []Username{Username("user1@")},
|
Group("group:integration-test"): []Username{Username("user1@"), Username("user2@")},
|
||||||
},
|
},
|
||||||
SSHs: []SSH{
|
SSHs: []SSH{
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
Sources: SSHSrcAliases{gp("group:integration-test")},
|
Sources: SSHSrcAliases{gp("group:integration-test")},
|
||||||
Destinations: SSHDstAliases{up("user2@")}, // Target user2
|
Destinations: SSHDstAliases{agp("autogroup:self")}, // Users can SSH to their own devices
|
||||||
Users: []SSHUser{SSHUser("ssh-it-user")}, // This is the key - specific user
|
Users: []SSHUser{SSHUser("ssh-it-user")}, // This is the key - specific user
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -715,7 +755,7 @@ func TestSSHIntegrationReproduction(t *testing.T) {
|
||||||
err := policy.validate()
|
err := policy.validate()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test SSH policy compilation for node2 (target)
|
// Test SSH policy compilation for node2 (owned by user2, who is in the group)
|
||||||
sshPolicy, err := policy.compileSSHPolicy(users, node2.View(), nodes.ViewSlice())
|
sshPolicy, err := policy.compileSSHPolicy(users, node2.View(), nodes.ViewSlice())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, sshPolicy)
|
require.NotNil(t, sshPolicy)
|
||||||
|
|
|
||||||
|
|
@ -664,7 +664,8 @@ func TestUnmarshalPolicy(t *testing.T) {
|
||||||
input: `
|
input: `
|
||||||
{
|
{
|
||||||
"tagOwners": {
|
"tagOwners": {
|
||||||
"tag:web": ["admin@example.com"]
|
"tag:web": ["admin@example.com"],
|
||||||
|
"tag:server": ["admin@example.com"]
|
||||||
},
|
},
|
||||||
"ssh": [
|
"ssh": [
|
||||||
{
|
{
|
||||||
|
|
@ -673,7 +674,7 @@ func TestUnmarshalPolicy(t *testing.T) {
|
||||||
"tag:web"
|
"tag:web"
|
||||||
],
|
],
|
||||||
"dst": [
|
"dst": [
|
||||||
"admin@example.com"
|
"tag:server"
|
||||||
],
|
],
|
||||||
"users": ["*"]
|
"users": ["*"]
|
||||||
}
|
}
|
||||||
|
|
@ -682,7 +683,8 @@ func TestUnmarshalPolicy(t *testing.T) {
|
||||||
`,
|
`,
|
||||||
want: &Policy{
|
want: &Policy{
|
||||||
TagOwners: TagOwners{
|
TagOwners: TagOwners{
|
||||||
Tag("tag:web"): Owners{ptr.To(Username("admin@example.com"))},
|
Tag("tag:web"): Owners{ptr.To(Username("admin@example.com"))},
|
||||||
|
Tag("tag:server"): Owners{ptr.To(Username("admin@example.com"))},
|
||||||
},
|
},
|
||||||
SSHs: []SSH{
|
SSHs: []SSH{
|
||||||
{
|
{
|
||||||
|
|
@ -691,7 +693,7 @@ func TestUnmarshalPolicy(t *testing.T) {
|
||||||
tp("tag:web"),
|
tp("tag:web"),
|
||||||
},
|
},
|
||||||
Destinations: SSHDstAliases{
|
Destinations: SSHDstAliases{
|
||||||
ptr.To(Username("admin@example.com")),
|
tp("tag:server"),
|
||||||
},
|
},
|
||||||
Users: []SSHUser{
|
Users: []SSHUser{
|
||||||
SSHUser("*"),
|
SSHUser("*"),
|
||||||
|
|
@ -714,7 +716,7 @@ func TestUnmarshalPolicy(t *testing.T) {
|
||||||
"group:admins"
|
"group:admins"
|
||||||
],
|
],
|
||||||
"dst": [
|
"dst": [
|
||||||
"admin@example.com"
|
"autogroup:self"
|
||||||
],
|
],
|
||||||
"users": ["root"],
|
"users": ["root"],
|
||||||
"checkPeriod": "24h"
|
"checkPeriod": "24h"
|
||||||
|
|
@ -733,7 +735,7 @@ func TestUnmarshalPolicy(t *testing.T) {
|
||||||
gp("group:admins"),
|
gp("group:admins"),
|
||||||
},
|
},
|
||||||
Destinations: SSHDstAliases{
|
Destinations: SSHDstAliases{
|
||||||
ptr.To(Username("admin@example.com")),
|
agp("autogroup:self"),
|
||||||
},
|
},
|
||||||
Users: []SSHUser{
|
Users: []SSHUser{
|
||||||
SSHUser("root"),
|
SSHUser("root"),
|
||||||
|
|
@ -1521,6 +1523,249 @@ func TestUnmarshalPolicy(t *testing.T) {
|
||||||
`,
|
`,
|
||||||
wantErr: `tag "tag:child" references undefined tag "tag:nonexistent"`,
|
wantErr: `tag "tag:child" references undefined tag "tag:nonexistent"`,
|
||||||
},
|
},
|
||||||
|
// SSH source/destination validation tests (#3009, #3010)
|
||||||
|
{
|
||||||
|
name: "ssh-tag-to-user-rejected",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"tagOwners": {"tag:server": ["admin@"]},
|
||||||
|
"ssh": [{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["tag:server"],
|
||||||
|
"dst": ["admin@"],
|
||||||
|
"users": ["autogroup:nonroot"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: "tags in SSH source cannot access user-owned devices",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh-autogroup-tagged-to-user-rejected",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"ssh": [{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["autogroup:tagged"],
|
||||||
|
"dst": ["admin@"],
|
||||||
|
"users": ["autogroup:nonroot"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: "tags in SSH source cannot access user-owned devices",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh-tag-to-autogroup-self-rejected",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"tagOwners": {"tag:server": ["admin@"]},
|
||||||
|
"ssh": [{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["tag:server"],
|
||||||
|
"dst": ["autogroup:self"],
|
||||||
|
"users": ["autogroup:nonroot"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: "autogroup:self destination requires source to contain only users or groups",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh-group-to-user-rejected",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"groups": {"group:admins": ["admin@", "user1@"]},
|
||||||
|
"ssh": [{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["group:admins"],
|
||||||
|
"dst": ["admin@"],
|
||||||
|
"users": ["autogroup:nonroot"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: `user destination requires source to contain only that same user "admin@"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh-same-user-to-user-allowed",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"ssh": [{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["admin@"],
|
||||||
|
"dst": ["admin@"],
|
||||||
|
"users": ["autogroup:nonroot"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
want: &Policy{
|
||||||
|
SSHs: []SSH{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: SSHSrcAliases{up("admin@")},
|
||||||
|
Destinations: SSHDstAliases{up("admin@")},
|
||||||
|
Users: []SSHUser{SSHUser(AutoGroupNonRoot)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh-group-to-autogroup-self-allowed",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"groups": {"group:admins": ["admin@", "user1@"]},
|
||||||
|
"ssh": [{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["group:admins"],
|
||||||
|
"dst": ["autogroup:self"],
|
||||||
|
"users": ["autogroup:nonroot"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
want: &Policy{
|
||||||
|
Groups: Groups{
|
||||||
|
Group("group:admins"): []Username{Username("admin@"), Username("user1@")},
|
||||||
|
},
|
||||||
|
SSHs: []SSH{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: SSHSrcAliases{gp("group:admins")},
|
||||||
|
Destinations: SSHDstAliases{agp("autogroup:self")},
|
||||||
|
Users: []SSHUser{SSHUser(AutoGroupNonRoot)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh-autogroup-tagged-to-autogroup-member-rejected",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"ssh": [{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["autogroup:tagged"],
|
||||||
|
"dst": ["autogroup:member"],
|
||||||
|
"users": ["autogroup:nonroot"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: "tags in SSH source cannot access autogroup:member",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh-autogroup-tagged-to-autogroup-tagged-allowed",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"ssh": [{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["autogroup:tagged"],
|
||||||
|
"dst": ["autogroup:tagged"],
|
||||||
|
"users": ["autogroup:nonroot"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
want: &Policy{
|
||||||
|
SSHs: []SSH{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: SSHSrcAliases{agp("autogroup:tagged")},
|
||||||
|
Destinations: SSHDstAliases{agp("autogroup:tagged")},
|
||||||
|
Users: []SSHUser{SSHUser(AutoGroupNonRoot)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh-wildcard-destination-rejected",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"groups": {"group:admins": ["admin@"]},
|
||||||
|
"ssh": [{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["group:admins"],
|
||||||
|
"dst": ["*"],
|
||||||
|
"users": ["autogroup:nonroot"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
wantErr: "wildcard (*) is not supported as SSH destination",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh-group-to-tag-allowed",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"tagOwners": {"tag:server": ["admin@"]},
|
||||||
|
"groups": {"group:admins": ["admin@"]},
|
||||||
|
"ssh": [{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["group:admins"],
|
||||||
|
"dst": ["tag:server"],
|
||||||
|
"users": ["autogroup:nonroot"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
want: &Policy{
|
||||||
|
TagOwners: TagOwners{
|
||||||
|
Tag("tag:server"): Owners{up("admin@")},
|
||||||
|
},
|
||||||
|
Groups: Groups{
|
||||||
|
Group("group:admins"): []Username{Username("admin@")},
|
||||||
|
},
|
||||||
|
SSHs: []SSH{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: SSHSrcAliases{gp("group:admins")},
|
||||||
|
Destinations: SSHDstAliases{tp("tag:server")},
|
||||||
|
Users: []SSHUser{SSHUser(AutoGroupNonRoot)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh-user-to-tag-allowed",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"tagOwners": {"tag:server": ["admin@"]},
|
||||||
|
"ssh": [{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["admin@"],
|
||||||
|
"dst": ["tag:server"],
|
||||||
|
"users": ["autogroup:nonroot"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
want: &Policy{
|
||||||
|
TagOwners: TagOwners{
|
||||||
|
Tag("tag:server"): Owners{up("admin@")},
|
||||||
|
},
|
||||||
|
SSHs: []SSH{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: SSHSrcAliases{up("admin@")},
|
||||||
|
Destinations: SSHDstAliases{tp("tag:server")},
|
||||||
|
Users: []SSHUser{SSHUser(AutoGroupNonRoot)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh-autogroup-member-to-autogroup-tagged-allowed",
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
"ssh": [{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["autogroup:member"],
|
||||||
|
"dst": ["autogroup:tagged"],
|
||||||
|
"users": ["autogroup:nonroot"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
want: &Policy{
|
||||||
|
SSHs: []SSH{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: SSHSrcAliases{agp("autogroup:member")},
|
||||||
|
Destinations: SSHDstAliases{agp("autogroup:tagged")},
|
||||||
|
Users: []SSHUser{SSHUser(AutoGroupNonRoot)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmps := append(util.Comparers,
|
cmps := append(util.Comparers,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue