mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	A hidden thing was implied in this document is that each person should have his own namespace. Hidden information in spicification isn't good. Thank's @kradalby for pointing it out.
		
			
				
	
	
		
			363 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			363 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # ACLs
 | |
| 
 | |
| A key component of tailscale is the notion of Tailnet. This notion is hidden
 | |
| but the implications that it have on how to use tailscale are not.
 | |
| 
 | |
| For tailscale an [tailnet](https://tailscale.com/kb/1136/tailnet/) is the
 | |
| following:
 | |
| 
 | |
| > For personal users, you are a tailnet of many devices and one person. Each
 | |
| > device gets a private Tailscale IP address in the CGNAT range and every
 | |
| > device can talk directly to every other device, wherever they are on the
 | |
| > internet.
 | |
| >
 | |
| > For businesses and organizations, a tailnet is many devices and many users.
 | |
| > It can be based on your Microsoft Active Directory, your Google Workspace, a
 | |
| > GitHub organization, Okta tenancy, or other identity provider namespace. All
 | |
| > of the devices and users in your tailnet can be seen by the tailnet
 | |
| > administrators in the Tailscale admin console. There you can apply
 | |
| > tailnet-wide configuration, such as ACLs that affect visibility of devices
 | |
| > inside your tailnet, DNS settings, and more.
 | |
| 
 | |
| ## Current implementation and issues
 | |
| 
 | |
| Currently in headscale, the namespaces are used both as tailnet and users. The
 | |
| issue is that if we want to use the ACL's we can't use both at the same time.
 | |
| 
 | |
| Tailnet's cannot communicate with each others. So we can't have an ACL that
 | |
| authorize tailnet (namespace) A to talk to tailnet (namespace) B.
 | |
| 
 | |
| We also can't write ACLs based on the users (namespaces in headscale) since all
 | |
| devices belong to the same user.
 | |
| 
 | |
| With the current implementation the only ACL that we can user is to associate
 | |
| each headscale IP to a host manually then write the ACLs according to this
 | |
| manual mapping.
 | |
| 
 | |
| ```json
 | |
| {
 | |
|   "hosts": {
 | |
|     "host1": "100.64.0.1",
 | |
|     "server": "100.64.0.2"
 | |
|   },
 | |
|   "acls": [
 | |
|     { "action": "accept", "users": ["host1"], "ports": ["host2:80,443"] }
 | |
|   ]
 | |
| }
 | |
| ```
 | |
| 
 | |
| While this works, it requires a lot of manual editing on the configuration and
 | |
| to keep track of all devices IP address.
 | |
| 
 | |
| ## Proposition for a next implementation
 | |
| 
 | |
| In order to ease the use of ACL's we need to split the tailnet and users
 | |
| notion.
 | |
| 
 | |
| A solution could be to consider a headscale server (in it's entirety) as a
 | |
| tailnet.
 | |
| 
 | |
| For personal users the default behavior could either allow all communications
 | |
| between all namespaces (like tailscale) or dissallow all communications between
 | |
| namespaces (current behavior).
 | |
| 
 | |
| For businesses and organisations, viewing a headscale instance a single tailnet
 | |
| would allow users (namespace) to talk to each other with the ACLs. As described
 | |
| in tailscale's documentation [[1]], a server should be tagged and personnal
 | |
| devices should be tied to a user. Translated in headscale's terms each user can
 | |
| have multiple devices and all those devices should be in the same namespace.
 | |
| The servers should be tagged and used as such.
 | |
| 
 | |
| This implementation would render useless the sharing feature that is currently
 | |
| implemented since an ACL could do the same. Simplifying to only one user
 | |
| interface to do one thing is easier and less confusing for the users.
 | |
| 
 | |
| To better suit the ACLs in this proposition, it's advised to consider that each
 | |
| namespaces belong to one person. This person can have multiple devices, they
 | |
| will all be considered as the same user in the ACLs. OIDC feature wouldn't need
 | |
| to map people to namespace, just create a namespace if the person isn't
 | |
| registered yet.
 | |
| 
 | |
| As a sidenote, users would like to write ACLs as YAML. We should offer users
 | |
| the ability to rules in either format (HuJSON or YAML).
 | |
| 
 | |
| [1]: https://tailscale.com/kb/1068/acl-tags/
 | |
| 
 | |
| ## Example
 | |
| 
 | |
| Let's build an example use case for a small business (It may be the place where
 | |
| ACL's are the most useful).
 | |
| 
 | |
| We have a small company with a boss, an admin, two developper and an intern.
 | |
| 
 | |
| The boss should have access to all servers but not to the users hosts. Admin
 | |
| should also have access to all hosts except that their permissions should be
 | |
| limited to maintaining the hosts (for example purposes). The developers can do
 | |
| anything they want on dev hosts, but only watch on productions hosts. Intern
 | |
| can only interact with the development servers.
 | |
| 
 | |
| Each user have at least a device connected to the network and we have some
 | |
| servers.
 | |
| 
 | |
| - database.prod
 | |
| - database.dev
 | |
| - app-server1.prod
 | |
| - app-server1.dev
 | |
| - billing.internal
 | |
| 
 | |
| ### Current headscale implementation
 | |
| 
 | |
| Let's create some namespaces
 | |
| 
 | |
| ```bash
 | |
| headscale namespaces create prod
 | |
| headscale namespaces create dev
 | |
| headscale namespaces create internal
 | |
| headscale namespaces create users
 | |
| 
 | |
| headscale nodes register -n users boss-computer
 | |
| headscale nodes register -n users admin1-computer
 | |
| headscale nodes register -n users dev1-computer
 | |
| headscale nodes register -n users dev1-phone
 | |
| headscale nodes register -n users dev2-computer
 | |
| headscale nodes register -n users intern1-computer
 | |
| 
 | |
| headscale nodes register -n prod database
 | |
| headscale nodes register -n prod app-server1
 | |
| 
 | |
| headscale nodes register -n dev database
 | |
| headscale nodes register -n dev app-server1
 | |
| 
 | |
| headscale nodes register -n internal billing
 | |
| 
 | |
| headscale nodes list
 | |
| ID  | Name              | Namespace | IP address
 | |
| 1   | boss-computer     | users     | 100.64.0.1
 | |
| 2   | admin1-computer   | users     | 100.64.0.2
 | |
| 3   | dev1-computer     | users     | 100.64.0.3
 | |
| 4   | dev1-phone        | users     | 100.64.0.4
 | |
| 5   | dev2-computer     | users     | 100.64.0.5
 | |
| 6   | intern1-computer  | users     | 100.64.0.6
 | |
| 7   | database          | prod      | 100.64.0.7
 | |
| 8   | app-server1       | prod      | 100.64.0.8
 | |
| 9   | database          | dev       | 100.64.0.9
 | |
| 10  | app-server1       | dev       | 100.64.0.10
 | |
| 11  | internal          | internal  | 100.64.0.11
 | |
| ```
 | |
| 
 | |
| In order to only allow the communications related to our description above we
 | |
| need to add the following ACLs
 | |
| 
 | |
| ```json
 | |
| {
 | |
|   "hosts": {
 | |
|     "boss-computer": "100.64.0.1",
 | |
|     "admin1-computer": "100.64.0.2",
 | |
|     "dev1-computer": "100.64.0.3",
 | |
|     "dev1-phone": "100.64.0.4",
 | |
|     "dev2-computer": "100.64.0.5",
 | |
|     "intern1-computer": "100.64.0.6",
 | |
|     "prod-app-server1": "100.64.0.8"
 | |
|   },
 | |
|   "groups": {
 | |
|     "group:dev": ["dev1-computer", "dev1-phone", "dev2-computer"],
 | |
|     "group:admin": ["admin1-computer"],
 | |
|     "group:boss": ["boss-computer"],
 | |
|     "group:intern": ["intern1-computer"]
 | |
|   },
 | |
|   "acls": [
 | |
|     // boss have access to all servers but no users hosts
 | |
|     {
 | |
|       "action": "accept",
 | |
|       "users": ["group:boss"],
 | |
|       "ports": ["prod:*", "dev:*", "internal:*"]
 | |
|     },
 | |
| 
 | |
|     // admin have access to adminstration port (lets only consider port 22 here)
 | |
|     {
 | |
|       "action": "accept",
 | |
|       "users": ["group:admin"],
 | |
|       "ports": ["prod:22", "dev:22", "internal:22"]
 | |
|     },
 | |
| 
 | |
|     // dev can do anything on dev servers and check access on prod servers
 | |
|     {
 | |
|       "action": "accept",
 | |
|       "users": ["group:dev"],
 | |
|       "ports": ["dev:*", "prod-app-server1:80,443"]
 | |
|     },
 | |
| 
 | |
|     // interns only have access to port 80 and 443 on dev servers (lame internship)
 | |
|     { "action": "accept", "users": ["group:intern"], "ports": ["dev:80,443"] },
 | |
| 
 | |
|     // users can access their own devices
 | |
|     {
 | |
|       "action": "accept",
 | |
|       "users": ["dev1-computer"],
 | |
|       "ports": ["dev1-phone:*"]
 | |
|     },
 | |
|     {
 | |
|       "action": "accept",
 | |
|       "users": ["dev1-phone"],
 | |
|       "ports": ["dev1-computer:*"]
 | |
|     },
 | |
| 
 | |
|     // internal namespace communications should still be allowed within the namespace
 | |
|     { "action": "accept", "users": ["dev"], "ports": ["dev:*"] },
 | |
|     { "action": "accept", "users": ["prod"], "ports": ["prod:*"] },
 | |
|     { "action": "accept", "users": ["internal"], "ports": ["internal:*"] }
 | |
|   ]
 | |
| }
 | |
| ```
 | |
| 
 | |
| Since communications between namespace isn't possible we also have to share the
 | |
| devices between the namespaces.
 | |
| 
 | |
| ```bash
 | |
| 
 | |
| // add boss host to prod, dev and internal network
 | |
| headscale nodes share -i 1 -n prod
 | |
| headscale nodes share -i 1 -n dev
 | |
| headscale nodes share -i 1 -n internal
 | |
| 
 | |
| // add admin computer to prod, dev and internal network
 | |
| headscale nodes share -i 2 -n prod
 | |
| headscale nodes share -i 2 -n dev
 | |
| headscale nodes share -i 2 -n internal
 | |
| 
 | |
| // add all dev to prod and dev network
 | |
| headscale nodes share -i 3 -n dev
 | |
| headscale nodes share -i 4 -n dev
 | |
| headscale nodes share -i 3 -n prod
 | |
| headscale nodes share -i 4 -n prod
 | |
| headscale nodes share -i 5 -n dev
 | |
| headscale nodes share -i 5 -n prod
 | |
| 
 | |
| headscale nodes share -i 6 -n dev
 | |
| ```
 | |
| 
 | |
| This fake network have not been tested but it should work. Operating it could
 | |
| be quite tedious if the company grows. Each time a new user join we have to add
 | |
| it to a group, and share it to the correct namespaces. If the user want
 | |
| multiple devices we have to allow communication to each of them one by one. If
 | |
| business conduct a change in the organisations we may have to rewrite all acls
 | |
| and reorganise all namespaces.
 | |
| 
 | |
| If we add servers in production we should also update the ACLs to allow dev
 | |
| access to certain category of them (only app servers for example).
 | |
| 
 | |
| ### example based on the proposition in this document
 | |
| 
 | |
| Let's create the namespaces
 | |
| 
 | |
| ```bash
 | |
| headscale namespaces create boss
 | |
| headscale namespaces create admin1
 | |
| headscale namespaces create dev1
 | |
| headscale namespaces create dev2
 | |
| headscale namespaces create intern1
 | |
| ```
 | |
| 
 | |
| We don't need to create namespaces for the servers because the servers will be
 | |
| tagged. When registering the servers we will need to add the flag
 | |
| `--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
 | |
| registering the server should be allowed to do it. Since anyone can add tags to
 | |
| a server they can register, the check of the tags is done on headscale server
 | |
| and only valid tags are applied. A tag is valid if the namespace that is
 | |
| registering it is allowed to do it.
 | |
| 
 | |
| Here are the ACL's to implement the same permissions as above:
 | |
| 
 | |
| ```json
 | |
| {
 | |
|   // groups are simpler and only list the namespaces name
 | |
|   "groups": {
 | |
|     "group:boss": ["boss"],
 | |
|     "group:dev": ["dev1", "dev2"],
 | |
|     "group:admin": ["admin1"],
 | |
|     "group:intern": ["intern1"]
 | |
|   },
 | |
|   "tagOwners": {
 | |
|     // the administrators can add servers in production
 | |
|     "tag:prod-databases": ["group:admin"],
 | |
|     "tag:prod-app-servers": ["group:admin"],
 | |
| 
 | |
|     // the boss can tag any server as internal
 | |
|     "tag:internal": ["group:boss"],
 | |
| 
 | |
|     // dev can add servers for dev purposes as well as admins
 | |
|     "tag:dev-databases": ["group:admin", "group:dev"],
 | |
|     "tag:dev-app-servers": ["group:admin", "group:dev"]
 | |
| 
 | |
|     // interns cannot add servers
 | |
|   },
 | |
|   "acls": [
 | |
|     // boss have access to all servers
 | |
|     {
 | |
|       "action": "accept",
 | |
|       "users": ["group:boss"],
 | |
|       "ports": [
 | |
|         "tag:prod-databases:*",
 | |
|         "tag:prod-app-servers:*",
 | |
|         "tag:internal:*",
 | |
|         "tag:dev-databases:*",
 | |
|         "tag:dev-app-servers:*"
 | |
|       ]
 | |
|     },
 | |
| 
 | |
|     // admin have only access to administrative ports of the servers
 | |
|     {
 | |
|       "action": "accept",
 | |
|       "users": ["group:admin"],
 | |
|       "ports": [
 | |
|         "tag:prod-databases:22",
 | |
|         "tag:prod-app-servers:22",
 | |
|         "tag:internal:22",
 | |
|         "tag:dev-databases:22",
 | |
|         "tag:dev-app-servers:22"
 | |
|       ]
 | |
|     },
 | |
| 
 | |
|     {
 | |
|       "action": "accept",
 | |
|       "users": ["group:dev"],
 | |
|       "ports": [
 | |
|         "tag:dev-databases:*",
 | |
|         "tag:dev-app-servers:*",
 | |
|         "tag:prod-app-servers:80,443"
 | |
|       ]
 | |
|     },
 | |
| 
 | |
|     // servers should be able to talk to database. Database should not be able to initiate connections to server
 | |
|     {
 | |
|       "action": "accept",
 | |
|       "users": ["tag:dev-app-servers"],
 | |
|       "ports": ["tag:dev-databases:5432"]
 | |
|     },
 | |
|     {
 | |
|       "action": "accept",
 | |
|       "users": ["tag:prod-app-servers"],
 | |
|       "ports": ["tag:prod-databases:5432"]
 | |
|     },
 | |
| 
 | |
|     // interns have access to dev-app-servers only in reading mode
 | |
|     {
 | |
|       "action": "accept",
 | |
|       "users": ["group:intern"],
 | |
|       "ports": ["tag:dev-app-servers:80,443"]
 | |
|     },
 | |
| 
 | |
|     // we still have to allow internal namespaces communications since nothing guarantees that each user have their own namespaces. This could be talked over.
 | |
|     { "action": "accept", "users": ["boss"], "ports": ["boss:*"] },
 | |
|     { "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] },
 | |
|     { "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] },
 | |
|     { "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] },
 | |
|     { "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] }
 | |
|   ]
 | |
| }
 | |
| ```
 | |
| 
 | |
| With this implementation, the sharing step is not necessary. Maintenance cost
 | |
| of the ACL file is lower and less tedious (no need to map hostname and IP's
 | |
| into it).
 |