mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Make more granular SSH tests for both Policies (#2555)
* policy/v1: dont consider empty if ssh has rules Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy/v2: replace time.Duration with model.Duration Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy/v2: add autogroup and ssh validation Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy/v2: replace time.Duration with model.Duration Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy: replace old ssh tests with more granular test Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy: skip v1 tests expected to fail (missing error handling) Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * policy: skip v1 group tests, old bugs wont be fixed Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * integration: user valid policy for ssh Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * Changelog, add ssh section Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * nix update Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
		
							parent
							
								
									f317a85ab4
								
							
						
					
					
						commit
						b9868f6516
					
				
							
								
								
									
										16
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @ -62,6 +62,20 @@ new policy code passes all of our tests. | |||||||
|     `@` should be appended at the end. For example, if your user is `john`, it |     `@` should be appended at the end. For example, if your user is `john`, it | ||||||
|     must be written as `john@` in the policy. |     must be written as `john@` in the policy. | ||||||
| 
 | 
 | ||||||
|  | **SSH** | ||||||
|  | 
 | ||||||
|  | The SSH policy has been reworked to be more consistent with the rest of the | ||||||
|  | policy. In addition, several inconsistencies between our implementation and | ||||||
|  | Tailscale's upstream has been closed and this might be a breaking change for | ||||||
|  | some users. Please refer to the | ||||||
|  | [upstream documentation](https://tailscale.com/kb/1337/acl-syntax#tailscale-ssh) | ||||||
|  | for more information on which types are allowed in `src`, `dst` and `users`. | ||||||
|  | 
 | ||||||
|  | There is one large inconsistency left, we allow `*` as a destination as we | ||||||
|  | currently do not support `autogroup:self`, `autogroup:member` and | ||||||
|  | `autogroup:tagged`. The support for `*` will be removed when we have support for | ||||||
|  | the autogroups. | ||||||
|  | 
 | ||||||
| **Current state** | **Current state** | ||||||
| 
 | 
 | ||||||
| The new policy is passing all tests, both integration and unit tests. This does | The new policy is passing all tests, both integration and unit tests. This does | ||||||
| @ -70,8 +84,6 @@ working in v1 and not tested might be broken in v2 (and vice versa). | |||||||
| 
 | 
 | ||||||
| **We do need help testing this code** | **We do need help testing this code** | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| #### Other breaking changes | #### Other breaking changes | ||||||
| 
 | 
 | ||||||
| - Disallow `server_url` and `base_domain` to be equal | - Disallow `server_url` and `base_domain` to be equal | ||||||
|  | |||||||
| @ -20,11 +20,11 @@ | |||||||
|     }, |     }, | ||||||
|     "nixpkgs": { |     "nixpkgs": { | ||||||
|       "locked": { |       "locked": { | ||||||
|         "lastModified": 1746152631, |         "lastModified": 1746300365, | ||||||
|         "narHash": "sha256-zBuvmL6+CUsk2J8GINpyy8Hs1Zp4PP6iBWSmZ4SCQ/s=", |         "narHash": "sha256-thYTdWqCRipwPRxWiTiH1vusLuAy0okjOyzRx4hLWh4=", | ||||||
|         "owner": "NixOS", |         "owner": "NixOS", | ||||||
|         "repo": "nixpkgs", |         "repo": "nixpkgs", | ||||||
|         "rev": "032bc6539bd5f14e9d0c51bd79cfe9a055b094c3", |         "rev": "f21e4546e3ede7ae34d12a84602a22246b31f7e0", | ||||||
|         "type": "github" |         "type": "github" | ||||||
|       }, |       }, | ||||||
|       "original": { |       "original": { | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/netip" | 	"net/netip" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/juanfont/headscale/hscontrol/policy/matcher" | 	"github.com/juanfont/headscale/hscontrol/policy/matcher" | ||||||
| 
 | 
 | ||||||
| @ -1540,3 +1541,408 @@ 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 | ||||||
|  | 
 | ||||||
|  | 		// There are some tests that will not pass on V1 since we do not
 | ||||||
|  | 		// have the same kind of error handling as V2, so we skip them.
 | ||||||
|  | 		skipV1 bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			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, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 
 | ||||||
|  | 			// It looks like the group implementation in v1 is broken, so
 | ||||||
|  | 			// we skip this test for v1 and not let it hold up v2 replacing it.
 | ||||||
|  | 			skipV1: 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, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 
 | ||||||
|  | 			// It looks like the group implementation in v1 is broken, so
 | ||||||
|  | 			// we skip this test for v1 and not let it hold up v2 replacing it.
 | ||||||
|  | 			skipV1: 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, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 
 | ||||||
|  | 			// It looks like the group implementation in v1 is broken, so
 | ||||||
|  | 			// we skip this test for v1 and not let it hold up v2 replacing it.
 | ||||||
|  | 			skipV1: 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", | ||||||
|  | 			skipV1:       true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			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`, | ||||||
|  | 			skipV1:       true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			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", | ||||||
|  | 			skipV1:       true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			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", | ||||||
|  | 			skipV1:       true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	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) { | ||||||
|  | 				if version == 1 && tt.skipV1 { | ||||||
|  | 					t.Skip() | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				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) | ||||||
|  | 				} | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | |||||||
| @ -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) { | func TestParseDestination(t *testing.T) { | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		dest      string | 		dest      string | ||||||
|  | |||||||
| @ -90,7 +90,7 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error { | |||||||
| 
 | 
 | ||||||
| // IsZero is perhaps a bit naive here.
 | // IsZero is perhaps a bit naive here.
 | ||||||
| func (pol ACLPolicy) IsZero() bool { | func (pol ACLPolicy) IsZero() bool { | ||||||
| 	if len(pol.Groups) == 0 && len(pol.Hosts) == 0 && len(pol.ACLs) == 0 { | 	if len(pol.Groups) == 0 && len(pol.Hosts) == 0 && len(pol.ACLs) == 0 && len(pol.SSHs) == 0 { | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -130,7 +130,7 @@ func (pol *Policy) compileSSHPolicy( | |||||||
| 		case "accept": | 		case "accept": | ||||||
| 			action = sshAction(true, 0) | 			action = sshAction(true, 0) | ||||||
| 		case "check": | 		case "check": | ||||||
| 			action = sshAction(true, rule.CheckPeriod) | 			action = sshAction(true, time.Duration(rule.CheckPeriod)) | ||||||
| 		default: | 		default: | ||||||
| 			return nil, fmt.Errorf("parsing SSH policy, unknown action %q, index: %d: %w", rule.Action, index, err) | 			return nil, fmt.Errorf("parsing SSH policy, unknown action %q, index: %d: %w", rule.Action, index, err) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -6,12 +6,12 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/netip" | 	"net/netip" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" |  | ||||||
| 
 | 
 | ||||||
| 	"slices" | 	"slices" | ||||||
| 
 | 
 | ||||||
| 	"github.com/juanfont/headscale/hscontrol/types" | 	"github.com/juanfont/headscale/hscontrol/types" | ||||||
| 	"github.com/juanfont/headscale/hscontrol/util" | 	"github.com/juanfont/headscale/hscontrol/util" | ||||||
|  | 	"github.com/prometheus/common/model" | ||||||
| 	"github.com/tailscale/hujson" | 	"github.com/tailscale/hujson" | ||||||
| 	"go4.org/netipx" | 	"go4.org/netipx" | ||||||
| 	"tailscale.com/net/tsaddr" | 	"tailscale.com/net/tsaddr" | ||||||
| @ -383,6 +383,12 @@ type AutoGroup string | |||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	AutoGroupInternet AutoGroup = "autogroup:internet" | 	AutoGroupInternet AutoGroup = "autogroup:internet" | ||||||
|  | 	AutoGroupNonRoot  AutoGroup = "autogroup:nonroot" | ||||||
|  | 
 | ||||||
|  | 	// These are not yet implemented.
 | ||||||
|  | 	AutoGroupSelf   AutoGroup = "autogroup:self" | ||||||
|  | 	AutoGroupMember AutoGroup = "autogroup:member" | ||||||
|  | 	AutoGroupTagged AutoGroup = "autogroup:tagged" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var autogroups = []AutoGroup{AutoGroupInternet} | var autogroups = []AutoGroup{AutoGroupInternet} | ||||||
| @ -915,6 +921,99 @@ type Policy struct { | |||||||
| 	SSHs          []SSH              `json:"ssh"` | 	SSHs          []SSH              `json:"ssh"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | var ( | ||||||
|  | 	autogroupForSrc       = []AutoGroup{} | ||||||
|  | 	autogroupForDst       = []AutoGroup{AutoGroupInternet} | ||||||
|  | 	autogroupForSSHSrc    = []AutoGroup{} | ||||||
|  | 	autogroupForSSHDst    = []AutoGroup{} | ||||||
|  | 	autogroupForSSHUser   = []AutoGroup{AutoGroupNonRoot} | ||||||
|  | 	autogroupNotSupported = []AutoGroup{AutoGroupSelf, AutoGroupMember, AutoGroupTagged} | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func validateAutogroupSupported(ag *AutoGroup) error { | ||||||
|  | 	if ag == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if slices.Contains(autogroupNotSupported, *ag) { | ||||||
|  | 		return fmt.Errorf("autogroup %q is not supported in headscale", *ag) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func validateAutogroupForSrc(src *AutoGroup) error { | ||||||
|  | 	if src == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if src.Is(AutoGroupInternet) { | ||||||
|  | 		return fmt.Errorf(`"autogroup:internet" used in source, it can only be used in ACL destinations`) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !slices.Contains(autogroupForSrc, *src) { | ||||||
|  | 		return fmt.Errorf("autogroup %q is not supported for ACL sources, can be %v", *src, autogroupForSrc) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func validateAutogroupForDst(dst *AutoGroup) error { | ||||||
|  | 	if dst == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !slices.Contains(autogroupForDst, *dst) { | ||||||
|  | 		return fmt.Errorf("autogroup %q is not supported for ACL destinations, can be %v", *dst, autogroupForDst) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func validateAutogroupForSSHSrc(src *AutoGroup) error { | ||||||
|  | 	if src == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if src.Is(AutoGroupInternet) { | ||||||
|  | 		return fmt.Errorf(`"autogroup:internet" used in SSH source, it can only be used in ACL destinations`) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !slices.Contains(autogroupForSSHSrc, *src) { | ||||||
|  | 		return fmt.Errorf("autogroup %q is not supported for SSH sources, can be %v", *src, autogroupForSSHSrc) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func validateAutogroupForSSHDst(dst *AutoGroup) error { | ||||||
|  | 	if dst == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if dst.Is(AutoGroupInternet) { | ||||||
|  | 		return fmt.Errorf(`"autogroup:internet" used in SSH destination, it can only be used in ACL destinations`) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !slices.Contains(autogroupForSSHDst, *dst) { | ||||||
|  | 		return fmt.Errorf("autogroup %q is not supported for SSH sources, can be %v", *dst, autogroupForSSHDst) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func validateAutogroupForSSHUser(user *AutoGroup) error { | ||||||
|  | 	if user == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !slices.Contains(autogroupForSSHUser, *user) { | ||||||
|  | 		return fmt.Errorf("autogroup %q is not supported for SSH user, can be %v", *user, autogroupForSSHUser) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // validate reports if there are any errors in a policy after
 | // validate reports if there are any errors in a policy after
 | ||||||
| // the unmarshaling process.
 | // the unmarshaling process.
 | ||||||
| // It runs through all rules and checks if there are any inconsistencies
 | // It runs through all rules and checks if there are any inconsistencies
 | ||||||
| @ -938,20 +1037,70 @@ func (p *Policy) validate() error { | |||||||
| 				} | 				} | ||||||
| 			case *AutoGroup: | 			case *AutoGroup: | ||||||
| 				ag := src.(*AutoGroup) | 				ag := src.(*AutoGroup) | ||||||
| 				if ag.Is(AutoGroupInternet) { | 
 | ||||||
| 					errs = append(errs, fmt.Errorf(`"autogroup:internet" used in source, it can only be used in ACL destinations`)) | 				if err := validateAutogroupSupported(ag); err != nil { | ||||||
|  | 					errs = append(errs, err) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if err := validateAutogroupForSrc(ag); err != nil { | ||||||
|  | 					errs = append(errs, err) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for _, dst := range acl.Destinations { | ||||||
|  | 			switch dst.Alias.(type) { | ||||||
|  | 			case *Host: | ||||||
|  | 				h := dst.Alias.(*Host) | ||||||
|  | 				if !p.Hosts.exist(*h) { | ||||||
|  | 					errs = append(errs, fmt.Errorf(`Host %q is not defined in the Policy, please define or remove the reference to it`, *h)) | ||||||
|  | 				} | ||||||
|  | 			case *AutoGroup: | ||||||
|  | 				ag := dst.Alias.(*AutoGroup) | ||||||
|  | 
 | ||||||
|  | 				if err := validateAutogroupSupported(ag); err != nil { | ||||||
|  | 					errs = append(errs, err) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if err := validateAutogroupForDst(ag); err != nil { | ||||||
|  | 					errs = append(errs, err) | ||||||
|  | 					continue | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, ssh := range p.SSHs { | 	for _, ssh := range p.SSHs { | ||||||
|  | 		if ssh.Action != "accept" && ssh.Action != "check" { | ||||||
|  | 			errs = append(errs, fmt.Errorf("SSH action %q is not valid, must be accept or check", ssh.Action)) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for _, user := range ssh.Users { | ||||||
|  | 			if strings.HasPrefix(string(user), "autogroup:") { | ||||||
|  | 				maybeAuto := AutoGroup(user) | ||||||
|  | 				if err := validateAutogroupForSSHUser(&maybeAuto); err != nil { | ||||||
|  | 					errs = append(errs, err) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		for _, src := range ssh.Sources { | 		for _, src := range ssh.Sources { | ||||||
| 			switch src.(type) { | 			switch src.(type) { | ||||||
| 			case *AutoGroup: | 			case *AutoGroup: | ||||||
| 				ag := src.(*AutoGroup) | 				ag := src.(*AutoGroup) | ||||||
| 				if ag.Is(AutoGroupInternet) { | 
 | ||||||
| 					errs = append(errs, fmt.Errorf(`"autogroup:internet" used in SSH source, it can only be used in ACL destinations`)) | 				if err := validateAutogroupSupported(ag); err != nil { | ||||||
|  | 					errs = append(errs, err) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if err := validateAutogroupForSSHSrc(ag); err != nil { | ||||||
|  | 					errs = append(errs, err) | ||||||
|  | 					continue | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -959,8 +1108,14 @@ func (p *Policy) validate() error { | |||||||
| 			switch dst.(type) { | 			switch dst.(type) { | ||||||
| 			case *AutoGroup: | 			case *AutoGroup: | ||||||
| 				ag := dst.(*AutoGroup) | 				ag := dst.(*AutoGroup) | ||||||
| 				if ag.Is(AutoGroupInternet) { | 				if err := validateAutogroupSupported(ag); err != nil { | ||||||
| 					errs = append(errs, fmt.Errorf(`"autogroup:internet" used in SSH destination, it can only be used in ACL destinations`)) | 					errs = append(errs, err) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if err := validateAutogroupForSSHDst(ag); err != nil { | ||||||
|  | 					errs = append(errs, err) | ||||||
|  | 					continue | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -976,11 +1131,11 @@ func (p *Policy) validate() error { | |||||||
| 
 | 
 | ||||||
| // SSH controls who can ssh into which machines.
 | // SSH controls who can ssh into which machines.
 | ||||||
| type SSH struct { | type SSH struct { | ||||||
| 	Action       string        `json:"action"` // TODO(kradalby): add strict type
 | 	Action       string         `json:"action"` | ||||||
| 	Sources      SSHSrcAliases `json:"src"` | 	Sources      SSHSrcAliases  `json:"src"` | ||||||
| 	Destinations SSHDstAliases `json:"dst"` | 	Destinations SSHDstAliases  `json:"dst"` | ||||||
| 	Users        []SSHUser     `json:"users"` | 	Users        []SSHUser      `json:"users"` | ||||||
| 	CheckPeriod  time.Duration `json:"checkPeriod,omitempty"` | 	CheckPeriod  model.Duration `json:"checkPeriod,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SSHSrcAliases is a list of aliases that can be used as sources in an SSH rule.
 | // SSHSrcAliases is a list of aliases that can be used as sources in an SSH rule.
 | ||||||
| @ -997,7 +1152,7 @@ func (a *SSHSrcAliases) UnmarshalJSON(b []byte) error { | |||||||
| 	*a = make([]Alias, len(aliases)) | 	*a = make([]Alias, len(aliases)) | ||||||
| 	for i, alias := range aliases { | 	for i, alias := range aliases { | ||||||
| 		switch alias.Alias.(type) { | 		switch alias.Alias.(type) { | ||||||
| 		case *Username, *Group, *Tag, *AutoGroup: | 		case *Group, *Tag, *AutoGroup: | ||||||
| 			(*a)[i] = alias.Alias | 			(*a)[i] = alias.Alias | ||||||
| 		default: | 		default: | ||||||
| 			return fmt.Errorf("type %T not supported", alias.Alias) | 			return fmt.Errorf("type %T not supported", alias.Alias) | ||||||
| @ -1042,8 +1197,8 @@ func (a *SSHDstAliases) UnmarshalJSON(b []byte) error { | |||||||
| 			// so we will leave it in as there is no other option
 | 			// so we will leave it in as there is no other option
 | ||||||
| 			// to dynamically give all access
 | 			// to dynamically give all access
 | ||||||
| 			// https://tailscale.com/kb/1193/tailscale-ssh#dst
 | 			// https://tailscale.com/kb/1193/tailscale-ssh#dst
 | ||||||
| 			Asterix, | 			// TODO(kradalby): remove this when we support autogroup:tagged and autogroup:member
 | ||||||
| 			*Group: | 			Asterix: | ||||||
| 			(*a)[i] = alias.Alias | 			(*a)[i] = alias.Alias | ||||||
| 		default: | 		default: | ||||||
| 			return fmt.Errorf("type %T not supported", alias.Alias) | 			return fmt.Errorf("type %T not supported", alias.Alias) | ||||||
|  | |||||||
| @ -172,7 +172,7 @@ func TestSSHMultipleUsersAllToAll(t *testing.T) { | |||||||
| 				{ | 				{ | ||||||
| 					Action:       "accept", | 					Action:       "accept", | ||||||
| 					Sources:      []string{"group:integration-test"}, | 					Sources:      []string{"group:integration-test"}, | ||||||
| 					Destinations: []string{"group:integration-test"}, | 					Destinations: []string{"user1@", "user2@"}, | ||||||
| 					Users:        []string{"ssh-it-user"}, | 					Users:        []string{"ssh-it-user"}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| @ -267,7 +267,7 @@ func TestSSHIsBlockedInACL(t *testing.T) { | |||||||
| 				{ | 				{ | ||||||
| 					Action:       "accept", | 					Action:       "accept", | ||||||
| 					Sources:      []string{"group:integration-test"}, | 					Sources:      []string{"group:integration-test"}, | ||||||
| 					Destinations: []string{"group:integration-test"}, | 					Destinations: []string{"user1@"}, | ||||||
| 					Users:        []string{"ssh-it-user"}, | 					Users:        []string{"ssh-it-user"}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| @ -317,13 +317,13 @@ func TestSSHUserOnlyIsolation(t *testing.T) { | |||||||
| 				{ | 				{ | ||||||
| 					Action:       "accept", | 					Action:       "accept", | ||||||
| 					Sources:      []string{"group:ssh1"}, | 					Sources:      []string{"group:ssh1"}, | ||||||
| 					Destinations: []string{"group:ssh1"}, | 					Destinations: []string{"user1@"}, | ||||||
| 					Users:        []string{"ssh-it-user"}, | 					Users:        []string{"ssh-it-user"}, | ||||||
| 				}, | 				}, | ||||||
| 				{ | 				{ | ||||||
| 					Action:       "accept", | 					Action:       "accept", | ||||||
| 					Sources:      []string{"group:ssh2"}, | 					Sources:      []string{"group:ssh2"}, | ||||||
| 					Destinations: []string{"group:ssh2"}, | 					Destinations: []string{"user2@"}, | ||||||
| 					Users:        []string{"ssh-it-user"}, | 					Users:        []string{"ssh-it-user"}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user