Repository: juanfont/headscale

Issue:
[Feature] Query claims from OIDC UserInfo if not available in ID token
### Use case

This was discussed in #2493 - just to keep track of it.

### Description

`EmailVerified` is already implemented, the remaining fields should probably be queried from OIDC UserInfo endpoint in case they are not available in the ID token.

### Contribution

- [ ] I can write the design doc for this feature
- [ ] I can contribute this feature

### How can it be implemented?

_No response_

Discussion:

Solution patch:
diff --git a/.github/workflows/gh-action-integration-generator.go b/.github/workflows/gh-action-integration-generator.go
index 471e3589..f94753b0 100644
--- a/.github/workflows/gh-action-integration-generator.go
+++ b/.github/workflows/gh-action-integration-generator.go
@@ -71,5 +71,4 @@ func main() {
 	}
 
 	updateYAML(quotedTests, "./test-integration.yaml")
-	updateYAML(quotedTests, "./test-integration-policyv2.yaml")
 }
diff --git a/.github/workflows/test-integration-policyv2.yaml b/.github/workflows/test-integration-policyv2.yaml
deleted file mode 100644
index c334a5a7..00000000
--- a/.github/workflows/test-integration-policyv2.yaml
+++ /dev/null
@@ -1,169 +0,0 @@
-name: Integration Tests (policy v2)
-# To debug locally on a branch, and when needing secrets
-# change this to include `push` so the build is ran on
-# the main repository.
-on: [pull_request]
-concurrency:
-  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
-  cancel-in-progress: true
-jobs:
-  integration-test:
-    runs-on: ubuntu-latest
-    strategy:
-      fail-fast: false
-      matrix:
-        test:
-          - TestACLHostsInNetMapTable
-          - TestACLAllowUser80Dst
-          - TestACLDenyAllPort80
-          - TestACLAllowUserDst
-          - TestACLAllowStarDst
-          - TestACLNamedHostsCanReachBySubnet
-          - TestACLNamedHostsCanReach
-          - TestACLDevice1CanAccessDevice2
-          - TestPolicyUpdateWhileRunningWithCLIInDatabase
-          - TestAuthKeyLogoutAndReloginSameUser
-          - TestAuthKeyLogoutAndReloginNewUser
-          - TestAuthKeyLogoutAndReloginSameUserExpiredKey
-          - TestOIDCAuthenticationPingAll
-          - TestOIDCExpireNodesBasedOnTokenExpiry
-          - TestOIDC024UserCreation
-          - TestOIDCAuthenticationWithPKCE
-          - TestOIDCReloginSameNodeNewUser
-          - TestAuthWebFlowAuthenticationPingAll
-          - TestAuthWebFlowLogoutAndRelogin
-          - TestUserCommand
-          - TestPreAuthKeyCommand
-          - TestPreAuthKeyCommandWithoutExpiry
-          - TestPreAuthKeyCommandReusableEphemeral
-          - TestPreAuthKeyCorrectUserLoggedInCommand
-          - TestApiKeyCommand
-          - TestNodeTagCommand
-          - TestNodeAdvertiseTagCommand
-          - TestNodeCommand
-          - TestNodeExpireCommand
-          - TestNodeRenameCommand
-          - TestNodeMoveCommand
-          - TestPolicyCommand
-          - TestPolicyBrokenConfigCommand
-          - TestDERPVerifyEndpoint
-          - TestResolveMagicDNS
-          - TestResolveMagicDNSExtraRecordsPath
-          - TestValidateResolvConf
-          - TestDERPServerScenario
-          - TestDERPServerWebsocketScenario
-          - TestPingAllByIP
-          - TestPingAllByIPPublicDERP
-          - TestEphemeral
-          - TestEphemeralInAlternateTimezone
-          - TestEphemeral2006DeletedTooQuickly
-          - TestPingAllByHostname
-          - TestTaildrop
-          - TestUpdateHostnameFromClient
-          - TestExpireNode
-          - TestNodeOnlineStatus
-          - TestPingAllByIPManyUpDown
-          - Test2118DeletingOnlineNodePanics
-          - TestEnablingRoutes
-          - TestHASubnetRouterFailover
-          - TestSubnetRouteACL
-          - TestEnablingExitRoutes
-          - TestSubnetRouterMultiNetwork
-          - TestSubnetRouterMultiNetworkExitNode
-          - TestAutoApproveMultiNetwork
-          - TestHeadscale
-          - TestTailscaleNodesJoiningHeadcale
-          - TestSSHOneUserToAll
-          - TestSSHMultipleUsersAllToAll
-          - TestSSHNoSSHConfigured
-          - TestSSHIsBlockedInACL
-          - TestSSHUserOnlyIsolation
-        database: [postgres, sqlite]
-    env:
-      # Github does not allow us to access secrets in pull requests,
-      # so this env var is used to check if we have the secret or not.
-      # If we have the secrets, meaning we are running on push in a fork,
-      # there might be secrets available for more debugging.
-      # If TS_OAUTH_CLIENT_ID and TS_OAUTH_SECRET is set, then the job
-      # will join a debug tailscale network, set up SSH and a tmux session.
-      # The SSH will be configured to use the SSH key of the Github user
-      # that triggered the build.
-      HAS_TAILSCALE_SECRET: ${{ secrets.TS_OAUTH_CLIENT_ID }}
-    steps:
-      - uses: actions/checkout@v4
-        with:
-          fetch-depth: 2
-      - name: Get changed files
-        id: changed-files
-        uses: dorny/paths-filter@v3
-        with:
-          filters: |
-            files:
-              - '*.nix'
-              - 'go.*'
-              - '**/*.go'
-              - 'integration_test/'
-              - 'config-example.yaml'
-      - name: Tailscale
-        if: ${{ env.HAS_TAILSCALE_SECRET }}
-        uses: tailscale/github-action@v2
-        with:
-          oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
-          oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
-          tags: tag:gh
-      - name: Setup SSH server for Actor
-        if: ${{ env.HAS_TAILSCALE_SECRET }}
-        uses: alexellis/setup-sshd-actor@master
-      - uses: DeterminateSystems/nix-installer-action@main
-        if: steps.changed-files.outputs.files == 'true'
-      - uses: DeterminateSystems/magic-nix-cache-action@main
-        if: steps.changed-files.outputs.files == 'true'
-      - uses: satackey/action-docker-layer-caching@main
-        if: steps.changed-files.outputs.files == 'true'
-        continue-on-error: true
-      - name: Run Integration Test
-        uses: Wandalen/wretry.action@master
-        if: steps.changed-files.outputs.files == 'true'
-        env:
-          USE_POSTGRES: ${{ matrix.database == 'postgres' && '1' || '0' }}
-        with:
-          # Our integration tests are started like a thundering herd, often
-          # hitting limits of the various external repositories we depend on
-          # like docker hub. This will retry jobs every 5 min, 10 times,
-          # hopefully letting us avoid manual intervention and restarting jobs.
-          # One could of course argue that we should invest in trying to avoid
-          # this, but currently it seems like a larger investment to be cleverer
-          # about this.
-          # Some of the jobs might still require manual restart as they are really
-          # slow and this will cause them to eventually be killed by Github actions.
-          attempt_delay: 300000   # 5 min
-          attempt_limit: 10
-          command: |
-            nix develop --command -- docker run \
-              --tty --rm \
-              --volume ~/.cache/hs-integration-go:/go \
-              --name headscale-test-suite \
-              --volume $PWD:$PWD -w $PWD/integration \
-              --volume /var/run/docker.sock:/var/run/docker.sock \
-              --volume $PWD/control_logs:/tmp/control \
-              --env HEADSCALE_INTEGRATION_POSTGRES=${{env.USE_POSTGRES}} \
-              --env HEADSCALE_EXPERIMENTAL_POLICY_V2=1 \
-              golang:1 \
-                go run gotest.tools/gotestsum@latest -- ./... \
-                  -failfast \
-                  -timeout 120m \
-                  -parallel 1 \
-                  -run "^${{ matrix.test }}$"
-      - uses: actions/upload-artifact@v4
-        if: always() && steps.changed-files.outputs.files == 'true'
-        with:
-          name: ${{ matrix.test }}-${{matrix.database}}-${{matrix.policy}}-logs
-          path: "control_logs/*.log"
-      - uses: actions/upload-artifact@v4
-        if: always() && steps.changed-files.outputs.files == 'true'
-        with:
-          name: ${{ matrix.test }}-${{matrix.database}}-${{matrix.policy}}-pprof
-          path: "control_logs/*.pprof.tar"
-      - name: Setup a blocking tmux session
-        if: ${{ env.HAS_TAILSCALE_SECRET }}
-        uses: alexellis/block-with-tmux-action@master
diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml
index ba2a4e2e..58c5705a 100644
--- a/.github/workflows/test-integration.yaml
+++ b/.github/workflows/test-integration.yaml
@@ -49,7 +49,6 @@ jobs:
           - TestDERPVerifyEndpoint
           - TestResolveMagicDNS
           - TestResolveMagicDNSExtraRecordsPath
-          - TestValidateResolvConf
           - TestDERPServerScenario
           - TestDERPServerWebsocketScenario
           - TestPingAllByIP
@@ -136,7 +135,7 @@ jobs:
           # about this.
           # Some of the jobs might still require manual restart as they are really
           # slow and this will cause them to eventually be killed by Github actions.
-          attempt_delay: 300000   # 5 min
+          attempt_delay: 300000 # 5 min
           attempt_limit: 10
           command: |
             nix develop --command -- docker run \
@@ -147,7 +146,6 @@ jobs:
               --volume /var/run/docker.sock:/var/run/docker.sock \
               --volume $PWD/control_logs:/tmp/control \
               --env HEADSCALE_INTEGRATION_POSTGRES=${{env.USE_POSTGRES}} \
-              --env HEADSCALE_EXPERIMENTAL_POLICY_V2=0 \
               golang:1 \
                 go run gotest.tools/gotestsum@latest -- ./... \
                   -failfast \
@@ -157,12 +155,12 @@ jobs:
       - uses: actions/upload-artifact@v4
         if: always() && steps.changed-files.outputs.files == 'true'
         with:
-          name: ${{ matrix.test }}-${{matrix.database}}-${{matrix.policy}}-logs
+          name: ${{ matrix.test }}-${{matrix.database}}-logs
           path: "control_logs/*.log"
       - uses: actions/upload-artifact@v4
         if: always() && steps.changed-files.outputs.files == 'true'
         with:
-          name: ${{ matrix.test }}-${{matrix.database}}-${{matrix.policy}}-pprof
+          name: ${{ matrix.test }}-${{matrix.database}}-pprof
           path: "control_logs/*.pprof.tar"
       - name: Setup a blocking tmux session
         if: ${{ env.HAS_TAILSCALE_SECRET }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f39c3a2b..18878d8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
 
 ### BREAKING
 
+#### Routes
+
 Route internals have been rewritten, removing the dedicated route table in the
 database. This was done to simplify the codebase, which had grown unnecessarily
 complex after the routes were split into separate tables. The overhead of having
@@ -35,14 +37,15 @@ will be approved.
 - Routes are now managed via the Node API
   [#2422](https://github.com/juanfont/headscale/pull/2422)
 
-### Experimental Policy v2
+#### Policy v2
 
-This release introduces a new experimental version of Headscales policy
-implementation. In this context, experimental means that the feature is not yet
-fully tested and may contain bugs or unexpected behavior and that we are still
-experimenting with how the final interface/behavior will be.
+This release introduces a new policy implementation. The new policy is a
+complete rewrite, and it introduces some significant quality and consistency
+improvements. In principle, there are not really any new features, but some long
+standing bugs should have been resolved, or be easier to fix in the future. The
+new policy code passes all of our tests.
 
-#### Breaking changes
+**Changes**
 
 - The policy is validated and "resolved" when loading, providing errors for
   invalid rules and conditions.
@@ -59,19 +62,26 @@ experimenting with how the final interface/behavior will be.
     `@` should be appended at the end. For example, if your user is `john`, it
     must be written as `john@` in the policy.
 
-#### Current state
+**Current state**
 
 The new policy is passing all tests, both integration and unit tests. This does
 not mean it is perfect, but it is a good start. Corner cases that is currently
 working in v1 and not tested might be broken in v2 (and vice versa).
 
-**We do need help testing this code**, and we think that most of the user facing
-API will not really change. We are not sure yet when this code will replace v1,
-but we are confident that it will, and all new changes and fixes will be made
-towards this code.
+**We do need help testing this code**
+
+
+#### Other breaking
+
+- Disallow `server_url` and `base_domain` to be equal
+  [#2544](https://github.com/juanfont/headscale/pull/2544)
+
+#### Other breaking changes
 
-The new policy can be used by setting the environment variable
-`HEADSCALE_EXPERIMENTAL_POLICY_V2` to `1`.
+- Return full user in API for pre auth keys instead of string
+  [#2542](https://github.com/juanfont/headscale/pull/2542)
+- Pre auth key API/CLI now uses ID over username
+  [#2542](https://github.com/juanfont/headscale/pull/2542)
 
 ### Changes
 
@@ -87,6 +97,8 @@ The new policy can be used by setting the environment variable
   [#2493](https://github.com/juanfont/headscale/pull/2493)
   - If a OIDC provider doesn't include the `email_verified` claim in its ID
     tokens, Headscale will attempt to get it from the UserInfo endpoint.
+- OIDC: Try to populate name, email and username from UserInfo
+  [#2545](https://github.com/juanfont/headscale/pull/2545)
 - Improve performance by only querying relevant nodes from the database for node
   updates [#2509](https://github.com/juanfont/headscale/pull/2509)
 - node FQDNs in the netmap will now contain a dot (".") at the end. This aligns
diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go
index 0074e029..8431149a 100644
--- a/cmd/headscale/cli/preauthkeys.go
+++ b/cmd/headscale/cli/preauthkeys.go
@@ -20,7 +20,7 @@ const (
 
 func init() {
 	rootCmd.AddCommand(preauthkeysCmd)
-	preauthkeysCmd.PersistentFlags().StringP("user", "u", "", "User")
+	preauthkeysCmd.PersistentFlags().Uint64P("user", "u", 0, "User identifier (ID)")
 
 	preauthkeysCmd.PersistentFlags().StringP("namespace", "n", "", "User")
 	pakNamespaceFlag := preauthkeysCmd.PersistentFlags().Lookup("namespace")
@@ -57,7 +57,7 @@ var listPreAuthKeys = &cobra.Command{
 	Run: func(cmd *cobra.Command, args []string) {
 		output, _ := cmd.Flags().GetString("output")
 
-		user, err := cmd.Flags().GetString("user")
+		user, err := cmd.Flags().GetUint64("user")
 		if err != nil {
 			ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output)
 		}
@@ -112,7 +112,7 @@ var listPreAuthKeys = &cobra.Command{
 			aclTags = strings.TrimLeft(aclTags, ",")
 
 			tableData = append(tableData, []string{
-				key.GetId(),
+				strconv.FormatUint(key.GetId(), 64),
 				key.GetKey(),
 				strconv.FormatBool(key.GetReusable()),
 				strconv.FormatBool(key.GetEphemeral()),
@@ -141,7 +141,7 @@ var createPreAuthKeyCmd = &cobra.Command{
 	Run: func(cmd *cobra.Command, args []string) {
 		output, _ := cmd.Flags().GetString("output")
 
-		user, err := cmd.Flags().GetString("user")
+		user, err := cmd.Flags().GetUint64("user")
 		if err != nil {
 			ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output)
 		}
@@ -206,7 +206,7 @@ var expirePreAuthKeyCmd = &cobra.Command{
 	},
 	Run: func(cmd *cobra.Command, args []string) {
 		output, _ := cmd.Flags().GetString("output")
-		user, err := cmd.Flags().GetString("user")
+		user, err := cmd.Flags().GetUint64("user")
 		if err != nil {
 			ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output)
 		}
diff --git a/flake.lock b/flake.lock
index 49e4ab6b..5d42af72 100644
--- a/flake.lock
+++ b/flake.lock
@@ -20,11 +20,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1744536153,
-        "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
+        "lastModified": 1744868846,
+        "narHash": "sha256-5RJTdUHDmj12Qsv7XOhuospjAjATNiTMElplWnJE9Hs=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
+        "rev": "ebe4301cbd8f81c4f8d3244b3632338bbeb6d49c",
         "type": "github"
       },
       "original": {
diff --git a/gen/go/headscale/v1/apikey.pb.go b/gen/go/headscale/v1/apikey.pb.go
index 2fdd8094..6f6a141e 100644
--- a/gen/go/headscale/v1/apikey.pb.go
+++ b/gen/go/headscale/v1/apikey.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.36.5
+// 	protoc-gen-go v1.36.6
 // 	protoc        (unknown)
 // source: headscale/v1/apikey.proto
 
@@ -428,53 +428,33 @@ func (*DeleteApiKeyResponse) Descriptor() ([]byte, []int) {
 
 var File_headscale_v1_apikey_proto protoreflect.FileDescriptor
 
-var file_headscale_v1_apikey_proto_rawDesc = string([]byte{
-	0x0a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61,
-	0x70, 0x69, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x68, 0x65, 0x61,
-	0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
-	0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73,
-	0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe0, 0x01, 0x0a, 0x06, 0x41,
-	0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18,
-	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x3a, 0x0a,
-	0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65,
-	0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65,
-	0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
-	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
-	0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74,
-	0x65, 0x64, 0x41, 0x74, 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65,
-	0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
-	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
-	0x61, 0x6d, 0x70, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x22, 0x51, 0x0a,
-	0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71,
-	0x75, 0x65, 0x73, 0x74, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69,
-	0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
-	0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
-	0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
-	0x22, 0x2f, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79,
-	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x61, 0x70, 0x69, 0x5f,
-	0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x70, 0x69, 0x4b, 0x65,
-	0x79, 0x22, 0x2d, 0x0a, 0x13, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65,
-	0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66,
-	0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78,
-	0x22, 0x16, 0x0a, 0x14, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79,
-	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x14, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74,
-	0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x46,
-	0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73,
-	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x61, 0x70, 0x69, 0x5f, 0x6b, 0x65, 0x79,
-	0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
-	0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x61,
-	0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x2d, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
-	0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a,
-	0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70,
-	0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0x16, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41,
-	0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x29, 0x5a,
-	0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e,
-	0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67,
-	0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
-})
+const file_headscale_v1_apikey_proto_rawDesc = "" +
+	"\n" +
+	"\x19headscale/v1/apikey.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe0\x01\n" +
+	"\x06ApiKey\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\x04R\x02id\x12\x16\n" +
+	"\x06prefix\x18\x02 \x01(\tR\x06prefix\x12:\n" +
+	"\n" +
+	"expiration\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
+	"expiration\x129\n" +
+	"\n" +
+	"created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x127\n" +
+	"\tlast_seen\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\blastSeen\"Q\n" +
+	"\x13CreateApiKeyRequest\x12:\n" +
+	"\n" +
+	"expiration\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
+	"expiration\"/\n" +
+	"\x14CreateApiKeyResponse\x12\x17\n" +
+	"\aapi_key\x18\x01 \x01(\tR\x06apiKey\"-\n" +
+	"\x13ExpireApiKeyRequest\x12\x16\n" +
+	"\x06prefix\x18\x01 \x01(\tR\x06prefix\"\x16\n" +
+	"\x14ExpireApiKeyResponse\"\x14\n" +
+	"\x12ListApiKeysRequest\"F\n" +
+	"\x13ListApiKeysResponse\x12/\n" +
+	"\bapi_keys\x18\x01 \x03(\v2\x14.headscale.v1.ApiKeyR\aapiKeys\"-\n" +
+	"\x13DeleteApiKeyRequest\x12\x16\n" +
+	"\x06prefix\x18\x01 \x01(\tR\x06prefix\"\x16\n" +
+	"\x14DeleteApiKeyResponseB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
 
 var (
 	file_headscale_v1_apikey_proto_rawDescOnce sync.Once
diff --git a/gen/go/headscale/v1/device.pb.go b/gen/go/headscale/v1/device.pb.go
index 641f1f7c..ea44a619 100644
--- a/gen/go/headscale/v1/device.pb.go
+++ b/gen/go/headscale/v1/device.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.36.5
+// 	protoc-gen-go v1.36.6
 // 	protoc        (unknown)
 // source: headscale/v1/device.proto
 
@@ -756,130 +756,71 @@ func (x *EnableDeviceRoutesResponse) GetAdvertisedRoutes() []string {
 
 var File_headscale_v1_device_proto protoreflect.FileDescriptor
 
-var file_headscale_v1_device_proto_rawDesc = string([]byte{
-	0x0a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x64,
-	0x65, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x68, 0x65, 0x61,
-	0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
-	0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73,
-	0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x46, 0x0a, 0x07, 0x4c, 0x61,
-	0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79,
-	0x5f, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x02, 0x52, 0x09, 0x6c, 0x61, 0x74, 0x65, 0x6e,
-	0x63, 0x79, 0x4d, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65,
-	0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72,
-	0x65, 0x64, 0x22, 0x91, 0x01, 0x0a, 0x0e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x75, 0x70,
-	0x70, 0x6f, 0x72, 0x74, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x68, 0x61, 0x69, 0x72, 0x5f, 0x70, 0x69,
-	0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x68, 0x61, 0x69,
-	0x72, 0x50, 0x69, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x70, 0x76, 0x36,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x69, 0x70, 0x76, 0x36, 0x12, 0x10, 0x0a, 0x03,
-	0x70, 0x63, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x63, 0x70, 0x12, 0x10,
-	0x0a, 0x03, 0x70, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x6d, 0x70,
-	0x12, 0x10, 0x0a, 0x03, 0x75, 0x64, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x75,
-	0x64, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x70, 0x6e, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08,
-	0x52, 0x04, 0x75, 0x70, 0x6e, 0x70, 0x22, 0xe3, 0x02, 0x0a, 0x12, 0x43, 0x6c, 0x69, 0x65, 0x6e,
-	0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x12, 0x1c, 0x0a,
-	0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09,
-	0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x64,
-	0x65, 0x72, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x65, 0x72, 0x70, 0x12,
-	0x38, 0x0a, 0x19, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x65,
-	0x73, 0x5f, 0x62, 0x79, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01,
-	0x28, 0x08, 0x52, 0x15, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x72, 0x69, 0x65,
-	0x73, 0x42, 0x79, 0x44, 0x65, 0x73, 0x74, 0x49, 0x70, 0x12, 0x47, 0x0a, 0x07, 0x6c, 0x61, 0x74,
-	0x65, 0x6e, 0x63, 0x79, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x68, 0x65, 0x61,
-	0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
-	0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x4c, 0x61, 0x74,
-	0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e,
-	0x63, 0x79, 0x12, 0x45, 0x0a, 0x0f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x75, 0x70,
-	0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x68, 0x65,
-	0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e,
-	0x74, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x52, 0x0e, 0x63, 0x6c, 0x69, 0x65, 0x6e,
-	0x74, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x1a, 0x51, 0x0a, 0x0c, 0x4c, 0x61, 0x74,
-	0x65, 0x6e, 0x63, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
-	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x05, 0x76,
-	0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61,
-	0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63,
-	0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x22, 0x0a, 0x10,
-	0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
-	0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64,
-	0x22, 0xa0, 0x06, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73,
-	0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65,
-	0x73, 0x73, 0x65, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
-	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08,
-	0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
-	0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x69, 0x65,
-	0x6e, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12,
-	0x29, 0x0a, 0x10, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61,
-	0x62, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x75, 0x70, 0x64, 0x61, 0x74,
-	0x65, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x73,
-	0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x6f, 0x73, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72,
-	0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
-	0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
-	0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
-	0x12, 0x37, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x0a, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
-	0x08, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x6b, 0x65, 0x79,
-	0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64,
-	0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x6b, 0x65, 0x79, 0x45, 0x78, 0x70, 0x69, 0x72,
-	0x79, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70,
-	0x69, 0x72, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
-	0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
-	0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12,
-	0x1e, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x18, 0x0d, 0x20,
-	0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x12,
-	0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x0e,
-	0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
-	0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18,
-	0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4b, 0x65,
-	0x79, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x10, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x3e, 0x0a, 0x1b,
-	0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x5f,
-	0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28,
-	0x08, 0x52, 0x19, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e,
-	0x67, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x0a, 0x0e,
-	0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x12,
-	0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x52, 0x6f, 0x75,
-	0x74, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65,
-	0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x13, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10,
-	0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73,
-	0x12, 0x51, 0x0a, 0x13, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65,
-	0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e,
-	0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x69,
-	0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x52,
-	0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x76,
-	0x69, 0x74, 0x79, 0x22, 0x25, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76,
-	0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
-	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x16, 0x0a, 0x14, 0x44, 0x65,
-	0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x22, 0x28, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52,
-	0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02,
-	0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x6d, 0x0a, 0x17,
-	0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52,
-	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x6e, 0x61, 0x62, 0x6c,
-	0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52,
-	0x0d, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2b,
-	0x0a, 0x11, 0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75,
-	0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x61, 0x64, 0x76, 0x65, 0x72,
-	0x74, 0x69, 0x73, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x43, 0x0a, 0x19, 0x45,
-	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65,
-	0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74,
-	0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73,
-	0x22, 0x70, 0x0a, 0x1a, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65,
-	0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25,
-	0x0a, 0x0e, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73,
-	0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x52,
-	0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69,
-	0x73, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09,
-	0x52, 0x10, 0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74,
-	0x65, 0x73, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
-	0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
-	0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x33,
-})
+const file_headscale_v1_device_proto_rawDesc = "" +
+	"\n" +
+	"\x19headscale/v1/device.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"F\n" +
+	"\aLatency\x12\x1d\n" +
+	"\n" +
+	"latency_ms\x18\x01 \x01(\x02R\tlatencyMs\x12\x1c\n" +
+	"\tpreferred\x18\x02 \x01(\bR\tpreferred\"\x91\x01\n" +
+	"\x0eClientSupports\x12!\n" +
+	"\fhair_pinning\x18\x01 \x01(\bR\vhairPinning\x12\x12\n" +
+	"\x04ipv6\x18\x02 \x01(\bR\x04ipv6\x12\x10\n" +
+	"\x03pcp\x18\x03 \x01(\bR\x03pcp\x12\x10\n" +
+	"\x03pmp\x18\x04 \x01(\bR\x03pmp\x12\x10\n" +
+	"\x03udp\x18\x05 \x01(\bR\x03udp\x12\x12\n" +
+	"\x04upnp\x18\x06 \x01(\bR\x04upnp\"\xe3\x02\n" +
+	"\x12ClientConnectivity\x12\x1c\n" +
+	"\tendpoints\x18\x01 \x03(\tR\tendpoints\x12\x12\n" +
+	"\x04derp\x18\x02 \x01(\tR\x04derp\x128\n" +
+	"\x19mapping_varies_by_dest_ip\x18\x03 \x01(\bR\x15mappingVariesByDestIp\x12G\n" +
+	"\alatency\x18\x04 \x03(\v2-.headscale.v1.ClientConnectivity.LatencyEntryR\alatency\x12E\n" +
+	"\x0fclient_supports\x18\x05 \x01(\v2\x1c.headscale.v1.ClientSupportsR\x0eclientSupports\x1aQ\n" +
+	"\fLatencyEntry\x12\x10\n" +
+	"\x03key\x18\x01 \x01(\tR\x03key\x12+\n" +
+	"\x05value\x18\x02 \x01(\v2\x15.headscale.v1.LatencyR\x05value:\x028\x01\"\"\n" +
+	"\x10GetDeviceRequest\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\tR\x02id\"\xa0\x06\n" +
+	"\x11GetDeviceResponse\x12\x1c\n" +
+	"\taddresses\x18\x01 \x03(\tR\taddresses\x12\x0e\n" +
+	"\x02id\x18\x02 \x01(\tR\x02id\x12\x12\n" +
+	"\x04user\x18\x03 \x01(\tR\x04user\x12\x12\n" +
+	"\x04name\x18\x04 \x01(\tR\x04name\x12\x1a\n" +
+	"\bhostname\x18\x05 \x01(\tR\bhostname\x12%\n" +
+	"\x0eclient_version\x18\x06 \x01(\tR\rclientVersion\x12)\n" +
+	"\x10update_available\x18\a \x01(\bR\x0fupdateAvailable\x12\x0e\n" +
+	"\x02os\x18\b \x01(\tR\x02os\x124\n" +
+	"\acreated\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\acreated\x127\n" +
+	"\tlast_seen\x18\n" +
+	" \x01(\v2\x1a.google.protobuf.TimestampR\blastSeen\x12.\n" +
+	"\x13key_expiry_disabled\x18\v \x01(\bR\x11keyExpiryDisabled\x124\n" +
+	"\aexpires\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\aexpires\x12\x1e\n" +
+	"\n" +
+	"authorized\x18\r \x01(\bR\n" +
+	"authorized\x12\x1f\n" +
+	"\vis_external\x18\x0e \x01(\bR\n" +
+	"isExternal\x12\x1f\n" +
+	"\vmachine_key\x18\x0f \x01(\tR\n" +
+	"machineKey\x12\x19\n" +
+	"\bnode_key\x18\x10 \x01(\tR\anodeKey\x12>\n" +
+	"\x1bblocks_incoming_connections\x18\x11 \x01(\bR\x19blocksIncomingConnections\x12%\n" +
+	"\x0eenabled_routes\x18\x12 \x03(\tR\renabledRoutes\x12+\n" +
+	"\x11advertised_routes\x18\x13 \x03(\tR\x10advertisedRoutes\x12Q\n" +
+	"\x13client_connectivity\x18\x14 \x01(\v2 .headscale.v1.ClientConnectivityR\x12clientConnectivity\"%\n" +
+	"\x13DeleteDeviceRequest\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\tR\x02id\"\x16\n" +
+	"\x14DeleteDeviceResponse\"(\n" +
+	"\x16GetDeviceRoutesRequest\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\tR\x02id\"m\n" +
+	"\x17GetDeviceRoutesResponse\x12%\n" +
+	"\x0eenabled_routes\x18\x01 \x03(\tR\renabledRoutes\x12+\n" +
+	"\x11advertised_routes\x18\x02 \x03(\tR\x10advertisedRoutes\"C\n" +
+	"\x19EnableDeviceRoutesRequest\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" +
+	"\x06routes\x18\x02 \x03(\tR\x06routes\"p\n" +
+	"\x1aEnableDeviceRoutesResponse\x12%\n" +
+	"\x0eenabled_routes\x18\x01 \x03(\tR\renabledRoutes\x12+\n" +
+	"\x11advertised_routes\x18\x02 \x03(\tR\x10advertisedRoutesB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
 
 var (
 	file_headscale_v1_device_proto_rawDescOnce sync.Once
diff --git a/gen/go/headscale/v1/headscale.pb.go b/gen/go/headscale/v1/headscale.pb.go
index 394d2c03..aa3380c6 100644
--- a/gen/go/headscale/v1/headscale.pb.go
+++ b/gen/go/headscale/v1/headscale.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.36.5
+// 	protoc-gen-go v1.36.6
 // 	protoc        (unknown)
 // source: headscale/v1/headscale.proto
 
@@ -23,203 +23,40 @@ const (
 
 var File_headscale_v1_headscale_proto protoreflect.FileDescriptor
 
-var file_headscale_v1_headscale_proto_rawDesc = string([]byte{
-	0x0a, 0x1c, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x68,
-	0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c,
-	0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x67, 0x6f,
-	0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74,
-	0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x68, 0x65, 0x61, 0x64,
-	0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x1a, 0x1d, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76,
-	0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x1a, 0x17, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31,
-	0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x68, 0x65, 0x61,
-	0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79,
-	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
-	0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x32, 0xa3, 0x16, 0x0a, 0x10, 0x48, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x53,
-	0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x68, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
-	0x55, 0x73, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
-	0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
-	0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52,
-	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x3a,
-	0x01, 0x2a, 0x22, 0x0c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72,
-	0x12, 0x80, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12,
-	0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52,
-	0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
-	0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
-	0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x22, 0x2f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x29, 0x22, 0x27, 0x2f, 0x61, 0x70, 0x69,
-	0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x7b, 0x6f, 0x6c, 0x64, 0x5f, 0x69, 0x64,
-	0x7d, 0x2f, 0x72, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x2f, 0x7b, 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61,
-	0x6d, 0x65, 0x7d, 0x12, 0x6a, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65,
-	0x72, 0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
-	0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
-	0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
-	0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x2a, 0x11, 0x2f, 0x61,
-	0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12,
-	0x62, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1e, 0x2e, 0x68,
-	0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74,
-	0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x68,
-	0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74,
-	0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x14, 0x82,
-	0xd3, 0xe4, 0x93, 0x02, 0x0e, 0x12, 0x0c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x75,
-	0x73, 0x65, 0x72, 0x12, 0x80, 0x01, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72,
-	0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73,
-	0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72,
-	0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
-	0x26, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43,
-	0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52,
-	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a,
-	0x01, 0x2a, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61,
-	0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x12, 0x87, 0x01, 0x0a, 0x10, 0x45, 0x78, 0x70, 0x69, 0x72,
-	0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x2e, 0x68, 0x65,
-	0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72,
-	0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
-	0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b,
-	0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93,
-	0x02, 0x1e, 0x3a, 0x01, 0x2a, 0x22, 0x19, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70,
-	0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65,
-	0x12, 0x7a, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b,
-	0x65, 0x79, 0x73, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
-	0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65,
-	0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64,
-	0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65,
-	0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
-	0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76,
-	0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x12, 0x7d, 0x0a, 0x0f,
-	0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12,
-	0x24, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44,
-	0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
-	0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
-	0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3,
-	0xe4, 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x22, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31,
-	0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x12, 0x66, 0x0a, 0x07, 0x47,
-	0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
-	0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71,
-	0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
-	0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
-	0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x61, 0x70,
-	0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f,
-	0x69, 0x64, 0x7d, 0x12, 0x6e, 0x0a, 0x07, 0x53, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x12, 0x1c,
-	0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65,
-	0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68,
-	0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54,
-	0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x26, 0x82, 0xd3, 0xe4,
-	0x93, 0x02, 0x20, 0x3a, 0x01, 0x2a, 0x22, 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f,
-	0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x74,
-	0x61, 0x67, 0x73, 0x12, 0x96, 0x01, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x41, 0x70, 0x70, 0x72, 0x6f,
-	0x76, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x68, 0x65, 0x61, 0x64,
-	0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x41, 0x70, 0x70, 0x72,
-	0x6f, 0x76, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
-	0x74, 0x1a, 0x27, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
-	0x2e, 0x53, 0x65, 0x74, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74,
-	0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x30, 0x82, 0xd3, 0xe4, 0x93,
-	0x02, 0x2a, 0x3a, 0x01, 0x2a, 0x22, 0x25, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e,
-	0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x61, 0x70,
-	0x70, 0x72, 0x6f, 0x76, 0x65, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x74, 0x0a, 0x0c,
-	0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x2e, 0x68,
-	0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69,
-	0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
-	0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52,
-	0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
-	0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x22, 0x15, 0x2f, 0x61, 0x70,
-	0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74,
-	0x65, 0x72, 0x12, 0x6f, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65,
-	0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
-	0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
-	0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
-	0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
-	0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x2a, 0x16, 0x2f, 0x61, 0x70,
-	0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f,
-	0x69, 0x64, 0x7d, 0x12, 0x76, 0x0a, 0x0a, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4e, 0x6f, 0x64,
-	0x65, 0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31,
-	0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
-	0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70,
-	0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x22, 0x1d, 0x2f, 0x61,
-	0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65,
-	0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x12, 0x81, 0x01, 0x0a, 0x0a,
-	0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x68, 0x65, 0x61,
-	0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65,
-	0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, 0x65,
-	0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d,
-	0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x30, 0x82,
-	0xd3, 0xe4, 0x93, 0x02, 0x2a, 0x22, 0x28, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e,
-	0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x72, 0x65,
-	0x6e, 0x61, 0x6d, 0x65, 0x2f, 0x7b, 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12,
-	0x62, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x12, 0x1e, 0x2e, 0x68,
-	0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74,
-	0x4e, 0x6f, 0x64, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x68,
-	0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74,
-	0x4e, 0x6f, 0x64, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x14, 0x82,
-	0xd3, 0xe4, 0x93, 0x02, 0x0e, 0x12, 0x0c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e,
-	0x6f, 0x64, 0x65, 0x12, 0x71, 0x0a, 0x08, 0x4d, 0x6f, 0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12,
-	0x1d, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d,
-	0x6f, 0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e,
-	0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f,
-	0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x26,
-	0x82, 0xd3, 0xe4, 0x93, 0x02, 0x20, 0x3a, 0x01, 0x2a, 0x22, 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f,
-	0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x7b, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64,
-	0x7d, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x12, 0x80, 0x01, 0x0a, 0x0f, 0x42, 0x61, 0x63, 0x6b, 0x66,
-	0x69, 0x6c, 0x6c, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x12, 0x24, 0x2e, 0x68, 0x65, 0x61,
-	0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x66, 0x69,
-	0x6c, 0x6c, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
-	0x1a, 0x25, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
-	0x42, 0x61, 0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x52,
-	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22,
-	0x18, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x62, 0x61,
-	0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, 0x69, 0x70, 0x73, 0x12, 0x70, 0x0a, 0x0c, 0x43, 0x72, 0x65,
-	0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64,
-	0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41,
-	0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68,
-	0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61,
-	0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
-	0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x61, 0x70,
-	0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x12, 0x77, 0x0a, 0x0c, 0x45,
-	0x78, 0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x68, 0x65,
-	0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72,
-	0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22,
-	0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78,
-	0x70, 0x69, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f,
-	0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f, 0x65, 0x78,
-	0x70, 0x69, 0x72, 0x65, 0x12, 0x6a, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b,
-	0x65, 0x79, 0x73, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
-	0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
-	0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x73,
-	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10,
-	0x12, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79,
-	0x12, 0x76, 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79,
-	0x12, 0x21, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
-	0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e,
-	0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x52,
-	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x2a,
-	0x17, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x6b, 0x65, 0x79, 0x2f,
-	0x7b, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x7d, 0x12, 0x64, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x50,
-	0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1e, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
-	0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
-	0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e,
-	0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x67,
-	0x0a, 0x09, 0x53, 0x65, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1e, 0x2e, 0x68, 0x65,
-	0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x6f,
-	0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x68, 0x65,
-	0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x6f,
-	0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3,
-	0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x1a, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31,
-	0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75,
-	0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68,
-	0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f,
-	0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
-})
+const file_headscale_v1_headscale_proto_rawDesc = "" +
+	"\n" +
+	"\x1cheadscale/v1/headscale.proto\x12\fheadscale.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17headscale/v1/user.proto\x1a\x1dheadscale/v1/preauthkey.proto\x1a\x17headscale/v1/node.proto\x1a\x19headscale/v1/apikey.proto\x1a\x19headscale/v1/policy.proto2\xa3\x16\n" +
+	"\x10HeadscaleService\x12h\n" +
+	"\n" +
+	"CreateUser\x12\x1f.headscale.v1.CreateUserRequest\x1a .headscale.v1.CreateUserResponse\"\x17\x82\xd3\xe4\x93\x02\x11:\x01*\"\f/api/v1/user\x12\x80\x01\n" +
+	"\n" +
+	"RenameUser\x12\x1f.headscale.v1.RenameUserRequest\x1a .headscale.v1.RenameUserResponse\"/\x82\xd3\xe4\x93\x02)\"'/api/v1/user/{old_id}/rename/{new_name}\x12j\n" +
+	"\n" +
+	"DeleteUser\x12\x1f.headscale.v1.DeleteUserRequest\x1a .headscale.v1.DeleteUserResponse\"\x19\x82\xd3\xe4\x93\x02\x13*\x11/api/v1/user/{id}\x12b\n" +
+	"\tListUsers\x12\x1e.headscale.v1.ListUsersRequest\x1a\x1f.headscale.v1.ListUsersResponse\"\x14\x82\xd3\xe4\x93\x02\x0e\x12\f/api/v1/user\x12\x80\x01\n" +
+	"\x10CreatePreAuthKey\x12%.headscale.v1.CreatePreAuthKeyRequest\x1a&.headscale.v1.CreatePreAuthKeyResponse\"\x1d\x82\xd3\xe4\x93\x02\x17:\x01*\"\x12/api/v1/preauthkey\x12\x87\x01\n" +
+	"\x10ExpirePreAuthKey\x12%.headscale.v1.ExpirePreAuthKeyRequest\x1a&.headscale.v1.ExpirePreAuthKeyResponse\"$\x82\xd3\xe4\x93\x02\x1e:\x01*\"\x19/api/v1/preauthkey/expire\x12z\n" +
+	"\x0fListPreAuthKeys\x12$.headscale.v1.ListPreAuthKeysRequest\x1a%.headscale.v1.ListPreAuthKeysResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/api/v1/preauthkey\x12}\n" +
+	"\x0fDebugCreateNode\x12$.headscale.v1.DebugCreateNodeRequest\x1a%.headscale.v1.DebugCreateNodeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17:\x01*\"\x12/api/v1/debug/node\x12f\n" +
+	"\aGetNode\x12\x1c.headscale.v1.GetNodeRequest\x1a\x1d.headscale.v1.GetNodeResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/node/{node_id}\x12n\n" +
+	"\aSetTags\x12\x1c.headscale.v1.SetTagsRequest\x1a\x1d.headscale.v1.SetTagsResponse\"&\x82\xd3\xe4\x93\x02 :\x01*\"\x1b/api/v1/node/{node_id}/tags\x12\x96\x01\n" +
+	"\x11SetApprovedRoutes\x12&.headscale.v1.SetApprovedRoutesRequest\x1a'.headscale.v1.SetApprovedRoutesResponse\"0\x82\xd3\xe4\x93\x02*:\x01*\"%/api/v1/node/{node_id}/approve_routes\x12t\n" +
+	"\fRegisterNode\x12!.headscale.v1.RegisterNodeRequest\x1a\".headscale.v1.RegisterNodeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\"\x15/api/v1/node/register\x12o\n" +
+	"\n" +
+	"DeleteNode\x12\x1f.headscale.v1.DeleteNodeRequest\x1a .headscale.v1.DeleteNodeResponse\"\x1e\x82\xd3\xe4\x93\x02\x18*\x16/api/v1/node/{node_id}\x12v\n" +
+	"\n" +
+	"ExpireNode\x12\x1f.headscale.v1.ExpireNodeRequest\x1a .headscale.v1.ExpireNodeResponse\"%\x82\xd3\xe4\x93\x02\x1f\"\x1d/api/v1/node/{node_id}/expire\x12\x81\x01\n" +
+	"\n" +
+	"RenameNode\x12\x1f.headscale.v1.RenameNodeRequest\x1a .headscale.v1.RenameNodeResponse\"0\x82\xd3\xe4\x93\x02*\"(/api/v1/node/{node_id}/rename/{new_name}\x12b\n" +
+	"\tListNodes\x12\x1e.headscale.v1.ListNodesRequest\x1a\x1f.headscale.v1.ListNodesResponse\"\x14\x82\xd3\xe4\x93\x02\x0e\x12\f/api/v1/node\x12q\n" +
+	"\bMoveNode\x12\x1d.headscale.v1.MoveNodeRequest\x1a\x1e.headscale.v1.MoveNodeResponse\"&\x82\xd3\xe4\x93\x02 :\x01*\"\x1b/api/v1/node/{node_id}/user\x12\x80\x01\n" +
+	"\x0fBackfillNodeIPs\x12$.headscale.v1.BackfillNodeIPsRequest\x1a%.headscale.v1.BackfillNodeIPsResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x18/api/v1/node/backfillips\x12p\n" +
+	"\fCreateApiKey\x12!.headscale.v1.CreateApiKeyRequest\x1a\".headscale.v1.CreateApiKeyResponse\"\x19\x82\xd3\xe4\x93\x02\x13:\x01*\"\x0e/api/v1/apikey\x12w\n" +
+	"\fExpireApiKey\x12!.headscale.v1.ExpireApiKeyRequest\x1a\".headscale.v1.ExpireApiKeyResponse\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/apikey/expire\x12j\n" +
+	"\vListApiKeys\x12 .headscale.v1.ListApiKeysRequest\x1a!.headscale.v1.ListApiKeysResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/api/v1/apikey\x12v\n" +
+	"\fDeleteApiKey\x12!.headscale.v1.DeleteApiKeyRequest\x1a\".headscale.v1.DeleteApiKeyResponse\"\x1f\x82\xd3\xe4\x93\x02\x19*\x17/api/v1/apikey/{prefix}\x12d\n" +
+	"\tGetPolicy\x12\x1e.headscale.v1.GetPolicyRequest\x1a\x1f.headscale.v1.GetPolicyResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/api/v1/policy\x12g\n" +
+	"\tSetPolicy\x12\x1e.headscale.v1.SetPolicyRequest\x1a\x1f.headscale.v1.SetPolicyResponse\"\x19\x82\xd3\xe4\x93\x02\x13:\x01*\x1a\x0e/api/v1/policyB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
 
 var file_headscale_v1_headscale_proto_goTypes = []any{
 	(*CreateUserRequest)(nil),         // 0: headscale.v1.CreateUserRequest
diff --git a/gen/go/headscale/v1/node.pb.go b/gen/go/headscale/v1/node.pb.go
index 8649cbec..1c4f2e3c 100644
--- a/gen/go/headscale/v1/node.pb.go
+++ b/gen/go/headscale/v1/node.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.36.5
+// 	protoc-gen-go v1.36.6
 // 	protoc        (unknown)
 // source: headscale/v1/node.proto
 
@@ -1296,160 +1296,94 @@ func (x *BackfillNodeIPsResponse) GetChanges() []string {
 
 var File_headscale_v1_node_proto protoreflect.FileDescriptor
 
-var file_headscale_v1_node_proto_rawDesc = string([]byte{
-	0x0a, 0x17, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6e,
-	0x6f, 0x64, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x68, 0x65, 0x61, 0x64, 0x73,
-	0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
-	0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1d, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
-	0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65,
-	0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
-	0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x22, 0x98, 0x06, 0x0a, 0x04, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x63,
-	0x68, 0x69, 0x6e, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
-	0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f,
-	0x64, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x6f,
-	0x64, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x5f, 0x6b,
-	0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x4b,
-	0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
-	0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72,
-	0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x75, 0x73, 0x65,
-	0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
-	0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65,
-	0x72, 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x08,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
-	0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x32, 0x0a, 0x06, 0x65, 0x78,
-	0x70, 0x69, 0x72, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
-	0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
-	0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x3a,
-	0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0b,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
-	0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0a,
-	0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72,
-	0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
-	0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
-	0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61,
-	0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x45, 0x0a, 0x0f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65,
-	0x72, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c,
-	0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65,
-	0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x0e, 0x72, 0x65,
-	0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1f, 0x0a, 0x0b,
-	0x66, 0x6f, 0x72, 0x63, 0x65, 0x64, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x12, 0x20, 0x03, 0x28,
-	0x09, 0x52, 0x0a, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x64, 0x54, 0x61, 0x67, 0x73, 0x12, 0x21, 0x0a,
-	0x0c, 0x69, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x13, 0x20,
-	0x03, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x54, 0x61, 0x67, 0x73,
-	0x12, 0x1d, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x14,
-	0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x54, 0x61, 0x67, 0x73, 0x12,
-	0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x76, 0x65, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x15, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x69, 0x76, 0x65, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16,
-	0x0a, 0x06, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x16, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06,
-	0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76,
-	0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x17, 0x20, 0x03, 0x28, 0x09, 0x52,
-	0x0e, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12,
-	0x29, 0x0a, 0x10, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x6f, 0x75,
-	0x74, 0x65, 0x73, 0x18, 0x18, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x76, 0x61, 0x69, 0x6c,
-	0x61, 0x62, 0x6c, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x75,
-	0x62, 0x6e, 0x65, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x19, 0x20, 0x03, 0x28,
-	0x09, 0x52, 0x0c, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x4a,
-	0x04, 0x08, 0x09, 0x10, 0x0a, 0x4a, 0x04, 0x08, 0x0e, 0x10, 0x12, 0x22, 0x3b, 0x0a, 0x13, 0x52,
-	0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x3e, 0x0a, 0x14, 0x52, 0x65, 0x67, 0x69,
-	0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
-	0x12, 0x26, 0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12,
-	0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f,
-	0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x22, 0x29, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4e,
-	0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f,
-	0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6e, 0x6f, 0x64,
-	0x65, 0x49, 0x64, 0x22, 0x39, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65,
-	0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x22, 0x3d,
-	0x0a, 0x0e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
-	0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x04, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67,
-	0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x22, 0x39, 0x0a,
-	0x0f, 0x53, 0x65, 0x74, 0x54, 0x61, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
-	0x12, 0x26, 0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12,
-	0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f,
-	0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x22, 0x4b, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x41,
-	0x70, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71,
-	0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a,
-	0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72,
-	0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x43, 0x0a, 0x19, 0x53, 0x65, 0x74, 0x41, 0x70, 0x70, 0x72,
-	0x6f, 0x76, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
-	0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
-	0x4e, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x22, 0x2c, 0x0a, 0x11, 0x44, 0x65,
-	0x6c, 0x65, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
-	0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04,
-	0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x22, 0x14, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65,
-	0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c,
-	0x0a, 0x11, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x22, 0x3c, 0x0a, 0x12,
-	0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
-	0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
-	0x4e, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x22, 0x47, 0x0a, 0x11, 0x52, 0x65,
-	0x6e, 0x61, 0x6d, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
-	0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04,
-	0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f,
-	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x77, 0x4e,
-	0x61, 0x6d, 0x65, 0x22, 0x3c, 0x0a, 0x12, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4e, 0x6f, 0x64,
-	0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6e, 0x6f, 0x64,
-	0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63,
-	0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64,
-	0x65, 0x22, 0x26, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x3d, 0x0a, 0x11, 0x4c, 0x69, 0x73,
-	0x74, 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28,
-	0x0a, 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e,
-	0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x64,
-	0x65, 0x52, 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x22, 0x3e, 0x0a, 0x0f, 0x4d, 0x6f, 0x76, 0x65,
-	0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6e,
-	0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6e, 0x6f,
-	0x64, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x3a, 0x0a, 0x10, 0x4d, 0x6f, 0x76, 0x65,
-	0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04,
-	0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61,
-	0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x04,
-	0x6e, 0x6f, 0x64, 0x65, 0x22, 0x6a, 0x0a, 0x16, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65,
-	0x61, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12,
-	0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73,
-	0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74,
-	0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73,
-	0x22, 0x41, 0x0a, 0x17, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4e,
-	0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x6e,
-	0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64,
-	0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6e,
-	0x6f, 0x64, 0x65, 0x22, 0x36, 0x0a, 0x16, 0x42, 0x61, 0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, 0x4e,
-	0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a,
-	0x09, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
-	0x52, 0x09, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x65, 0x64, 0x22, 0x33, 0x0a, 0x17, 0x42,
-	0x61, 0x63, 0x6b, 0x66, 0x69, 0x6c, 0x6c, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x50, 0x73, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65,
-	0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73,
-	0x2a, 0x82, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74,
-	0x68, 0x6f, 0x64, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f,
-	0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49,
-	0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52,
-	0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x4b, 0x45, 0x59,
-	0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d,
-	0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x43, 0x4c, 0x49, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x52,
-	0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x4f,
-	0x49, 0x44, 0x43, 0x10, 0x03, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
-	0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61,
-	0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31,
-	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
-})
+const file_headscale_v1_node_proto_rawDesc = "" +
+	"\n" +
+	"\x17headscale/v1/node.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1dheadscale/v1/preauthkey.proto\x1a\x17headscale/v1/user.proto\"\x98\x06\n" +
+	"\x04Node\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\x04R\x02id\x12\x1f\n" +
+	"\vmachine_key\x18\x02 \x01(\tR\n" +
+	"machineKey\x12\x19\n" +
+	"\bnode_key\x18\x03 \x01(\tR\anodeKey\x12\x1b\n" +
+	"\tdisco_key\x18\x04 \x01(\tR\bdiscoKey\x12!\n" +
+	"\fip_addresses\x18\x05 \x03(\tR\vipAddresses\x12\x12\n" +
+	"\x04name\x18\x06 \x01(\tR\x04name\x12&\n" +
+	"\x04user\x18\a \x01(\v2\x12.headscale.v1.UserR\x04user\x127\n" +
+	"\tlast_seen\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\blastSeen\x122\n" +
+	"\x06expiry\x18\n" +
+	" \x01(\v2\x1a.google.protobuf.TimestampR\x06expiry\x12:\n" +
+	"\fpre_auth_key\x18\v \x01(\v2\x18.headscale.v1.PreAuthKeyR\n" +
+	"preAuthKey\x129\n" +
+	"\n" +
+	"created_at\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12E\n" +
+	"\x0fregister_method\x18\r \x01(\x0e2\x1c.headscale.v1.RegisterMethodR\x0eregisterMethod\x12\x1f\n" +
+	"\vforced_tags\x18\x12 \x03(\tR\n" +
+	"forcedTags\x12!\n" +
+	"\finvalid_tags\x18\x13 \x03(\tR\vinvalidTags\x12\x1d\n" +
+	"\n" +
+	"valid_tags\x18\x14 \x03(\tR\tvalidTags\x12\x1d\n" +
+	"\n" +
+	"given_name\x18\x15 \x01(\tR\tgivenName\x12\x16\n" +
+	"\x06online\x18\x16 \x01(\bR\x06online\x12'\n" +
+	"\x0fapproved_routes\x18\x17 \x03(\tR\x0eapprovedRoutes\x12)\n" +
+	"\x10available_routes\x18\x18 \x03(\tR\x0favailableRoutes\x12#\n" +
+	"\rsubnet_routes\x18\x19 \x03(\tR\fsubnetRoutesJ\x04\b\t\x10\n" +
+	"J\x04\b\x0e\x10\x12\";\n" +
+	"\x13RegisterNodeRequest\x12\x12\n" +
+	"\x04user\x18\x01 \x01(\tR\x04user\x12\x10\n" +
+	"\x03key\x18\x02 \x01(\tR\x03key\">\n" +
+	"\x14RegisterNodeResponse\x12&\n" +
+	"\x04node\x18\x01 \x01(\v2\x12.headscale.v1.NodeR\x04node\")\n" +
+	"\x0eGetNodeRequest\x12\x17\n" +
+	"\anode_id\x18\x01 \x01(\x04R\x06nodeId\"9\n" +
+	"\x0fGetNodeResponse\x12&\n" +
+	"\x04node\x18\x01 \x01(\v2\x12.headscale.v1.NodeR\x04node\"=\n" +
+	"\x0eSetTagsRequest\x12\x17\n" +
+	"\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12\x12\n" +
+	"\x04tags\x18\x02 \x03(\tR\x04tags\"9\n" +
+	"\x0fSetTagsResponse\x12&\n" +
+	"\x04node\x18\x01 \x01(\v2\x12.headscale.v1.NodeR\x04node\"K\n" +
+	"\x18SetApprovedRoutesRequest\x12\x17\n" +
+	"\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12\x16\n" +
+	"\x06routes\x18\x02 \x03(\tR\x06routes\"C\n" +
+	"\x19SetApprovedRoutesResponse\x12&\n" +
+	"\x04node\x18\x01 \x01(\v2\x12.headscale.v1.NodeR\x04node\",\n" +
+	"\x11DeleteNodeRequest\x12\x17\n" +
+	"\anode_id\x18\x01 \x01(\x04R\x06nodeId\"\x14\n" +
+	"\x12DeleteNodeResponse\",\n" +
+	"\x11ExpireNodeRequest\x12\x17\n" +
+	"\anode_id\x18\x01 \x01(\x04R\x06nodeId\"<\n" +
+	"\x12ExpireNodeResponse\x12&\n" +
+	"\x04node\x18\x01 \x01(\v2\x12.headscale.v1.NodeR\x04node\"G\n" +
+	"\x11RenameNodeRequest\x12\x17\n" +
+	"\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12\x19\n" +
+	"\bnew_name\x18\x02 \x01(\tR\anewName\"<\n" +
+	"\x12RenameNodeResponse\x12&\n" +
+	"\x04node\x18\x01 \x01(\v2\x12.headscale.v1.NodeR\x04node\"&\n" +
+	"\x10ListNodesRequest\x12\x12\n" +
+	"\x04user\x18\x01 \x01(\tR\x04user\"=\n" +
+	"\x11ListNodesResponse\x12(\n" +
+	"\x05nodes\x18\x01 \x03(\v2\x12.headscale.v1.NodeR\x05nodes\">\n" +
+	"\x0fMoveNodeRequest\x12\x17\n" +
+	"\anode_id\x18\x01 \x01(\x04R\x06nodeId\x12\x12\n" +
+	"\x04user\x18\x02 \x01(\tR\x04user\":\n" +
+	"\x10MoveNodeResponse\x12&\n" +
+	"\x04node\x18\x01 \x01(\v2\x12.headscale.v1.NodeR\x04node\"j\n" +
+	"\x16DebugCreateNodeRequest\x12\x12\n" +
+	"\x04user\x18\x01 \x01(\tR\x04user\x12\x10\n" +
+	"\x03key\x18\x02 \x01(\tR\x03key\x12\x12\n" +
+	"\x04name\x18\x03 \x01(\tR\x04name\x12\x16\n" +
+	"\x06routes\x18\x04 \x03(\tR\x06routes\"A\n" +
+	"\x17DebugCreateNodeResponse\x12&\n" +
+	"\x04node\x18\x01 \x01(\v2\x12.headscale.v1.NodeR\x04node\"6\n" +
+	"\x16BackfillNodeIPsRequest\x12\x1c\n" +
+	"\tconfirmed\x18\x01 \x01(\bR\tconfirmed\"3\n" +
+	"\x17BackfillNodeIPsResponse\x12\x18\n" +
+	"\achanges\x18\x01 \x03(\tR\achanges*\x82\x01\n" +
+	"\x0eRegisterMethod\x12\x1f\n" +
+	"\x1bREGISTER_METHOD_UNSPECIFIED\x10\x00\x12\x1c\n" +
+	"\x18REGISTER_METHOD_AUTH_KEY\x10\x01\x12\x17\n" +
+	"\x13REGISTER_METHOD_CLI\x10\x02\x12\x18\n" +
+	"\x14REGISTER_METHOD_OIDC\x10\x03B)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
 
 var (
 	file_headscale_v1_node_proto_rawDescOnce sync.Once
diff --git a/gen/go/headscale/v1/policy.pb.go b/gen/go/headscale/v1/policy.pb.go
index 6ba350d3..f6befedc 100644
--- a/gen/go/headscale/v1/policy.pb.go
+++ b/gen/go/headscale/v1/policy.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.36.5
+// 	protoc-gen-go v1.36.6
 // 	protoc        (unknown)
 // source: headscale/v1/policy.proto
 
@@ -208,33 +208,20 @@ func (x *GetPolicyResponse) GetUpdatedAt() *timestamppb.Timestamp {
 
 var File_headscale_v1_policy_proto protoreflect.FileDescriptor
 
-var file_headscale_v1_policy_proto_rawDesc = string([]byte{
-	0x0a, 0x19, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70,
-	0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x68, 0x65, 0x61,
-	0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
-	0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73,
-	0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2a, 0x0a, 0x10, 0x53, 0x65,
-	0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16,
-	0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
-	0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x66, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x50, 0x6f, 0x6c,
-	0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70,
-	0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6c,
-	0x69, 0x63, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61,
-	0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
-	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
-	0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x12,
-	0x0a, 0x10, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x22, 0x66, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52,
-	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63,
-	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12,
-	0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
-	0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69,
-	0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e,
-	0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f,
-	0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
-})
+const file_headscale_v1_policy_proto_rawDesc = "" +
+	"\n" +
+	"\x19headscale/v1/policy.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"*\n" +
+	"\x10SetPolicyRequest\x12\x16\n" +
+	"\x06policy\x18\x01 \x01(\tR\x06policy\"f\n" +
+	"\x11SetPolicyResponse\x12\x16\n" +
+	"\x06policy\x18\x01 \x01(\tR\x06policy\x129\n" +
+	"\n" +
+	"updated_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\x12\n" +
+	"\x10GetPolicyRequest\"f\n" +
+	"\x11GetPolicyResponse\x12\x16\n" +
+	"\x06policy\x18\x01 \x01(\tR\x06policy\x129\n" +
+	"\n" +
+	"updated_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAtB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
 
 var (
 	file_headscale_v1_policy_proto_rawDescOnce sync.Once
diff --git a/gen/go/headscale/v1/preauthkey.pb.go b/gen/go/headscale/v1/preauthkey.pb.go
index acdb38e5..cd712c77 100644
--- a/gen/go/headscale/v1/preauthkey.pb.go
+++ b/gen/go/headscale/v1/preauthkey.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.36.5
+// 	protoc-gen-go v1.36.6
 // 	protoc        (unknown)
 // source: headscale/v1/preauthkey.proto
 
@@ -24,8 +24,8 @@ const (
 
 type PreAuthKey struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
-	User          string                 `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
-	Id            string                 `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
+	User          *User                  `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+	Id            uint64                 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
 	Key           string                 `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"`
 	Reusable      bool                   `protobuf:"varint,4,opt,name=reusable,proto3" json:"reusable,omitempty"`
 	Ephemeral     bool                   `protobuf:"varint,5,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"`
@@ -67,18 +67,18 @@ func (*PreAuthKey) Descriptor() ([]byte, []int) {
 	return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{0}
 }
 
-func (x *PreAuthKey) GetUser() string {
+func (x *PreAuthKey) GetUser() *User {
 	if x != nil {
 		return x.User
 	}
-	return ""
+	return nil
 }
 
-func (x *PreAuthKey) GetId() string {
+func (x *PreAuthKey) GetId() uint64 {
 	if x != nil {
 		return x.Id
 	}
-	return ""
+	return 0
 }
 
 func (x *PreAuthKey) GetKey() string {
@@ -132,7 +132,7 @@ func (x *PreAuthKey) GetAclTags() []string {
 
 type CreatePreAuthKeyRequest struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
-	User          string                 `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+	User          uint64                 `protobuf:"varint,1,opt,name=user,proto3" json:"user,omitempty"`
 	Reusable      bool                   `protobuf:"varint,2,opt,name=reusable,proto3" json:"reusable,omitempty"`
 	Ephemeral     bool                   `protobuf:"varint,3,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"`
 	Expiration    *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiration,proto3" json:"expiration,omitempty"`
@@ -171,11 +171,11 @@ func (*CreatePreAuthKeyRequest) Descriptor() ([]byte, []int) {
 	return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{1}
 }
 
-func (x *CreatePreAuthKeyRequest) GetUser() string {
+func (x *CreatePreAuthKeyRequest) GetUser() uint64 {
 	if x != nil {
 		return x.User
 	}
-	return ""
+	return 0
 }
 
 func (x *CreatePreAuthKeyRequest) GetReusable() bool {
@@ -252,7 +252,7 @@ func (x *CreatePreAuthKeyResponse) GetPreAuthKey() *PreAuthKey {
 
 type ExpirePreAuthKeyRequest struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
-	User          string                 `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+	User          uint64                 `protobuf:"varint,1,opt,name=user,proto3" json:"user,omitempty"`
 	Key           string                 `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"`
 	unknownFields protoimpl.UnknownFields
 	sizeCache     protoimpl.SizeCache
@@ -288,11 +288,11 @@ func (*ExpirePreAuthKeyRequest) Descriptor() ([]byte, []int) {
 	return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{3}
 }
 
-func (x *ExpirePreAuthKeyRequest) GetUser() string {
+func (x *ExpirePreAuthKeyRequest) GetUser() uint64 {
 	if x != nil {
 		return x.User
 	}
-	return ""
+	return 0
 }
 
 func (x *ExpirePreAuthKeyRequest) GetKey() string {
@@ -340,7 +340,7 @@ func (*ExpirePreAuthKeyResponse) Descriptor() ([]byte, []int) {
 
 type ListPreAuthKeysRequest struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
-	User          string                 `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+	User          uint64                 `protobuf:"varint,1,opt,name=user,proto3" json:"user,omitempty"`
 	unknownFields protoimpl.UnknownFields
 	sizeCache     protoimpl.SizeCache
 }
@@ -375,11 +375,11 @@ func (*ListPreAuthKeysRequest) Descriptor() ([]byte, []int) {
 	return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{5}
 }
 
-func (x *ListPreAuthKeysRequest) GetUser() string {
+func (x *ListPreAuthKeysRequest) GetUser() uint64 {
 	if x != nil {
 		return x.User
 	}
-	return ""
+	return 0
 }
 
 type ListPreAuthKeysResponse struct {
@@ -428,67 +428,42 @@ func (x *ListPreAuthKeysResponse) GetPreAuthKeys() []*PreAuthKey {
 
 var File_headscale_v1_preauthkey_proto protoreflect.FileDescriptor
 
-var file_headscale_v1_preauthkey_proto_rawDesc = string([]byte{
-	0x0a, 0x1d, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70,
-	0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
-	0x0c, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67,
-	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74,
-	0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa2,
-	0x02, 0x0a, 0x0a, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a,
-	0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65,
-	0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
-	0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
-	0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x18,
-	0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12,
-	0x1c, 0x0a, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01,
-	0x28, 0x08, 0x52, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x12, 0x12, 0x0a,
-	0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x75, 0x73, 0x65,
-	0x64, 0x12, 0x3a, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18,
-	0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
-	0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a,
-	0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63,
-	0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x63, 0x6c, 0x5f,
-	0x74, 0x61, 0x67, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x6c, 0x54,
-	0x61, 0x67, 0x73, 0x22, 0xbe, 0x01, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72,
-	0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
-	0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75,
-	0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x18,
-	0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x75, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12,
-	0x1c, 0x0a, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01,
-	0x28, 0x08, 0x52, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x12, 0x3a, 0x0a,
-	0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65,
-	0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x63, 0x6c,
-	0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x6c,
-	0x54, 0x61, 0x67, 0x73, 0x22, 0x56, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72,
-	0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
-	0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79,
-	0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
-	0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79,
-	0x52, 0x0a, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x22, 0x3f, 0x0a, 0x17,
-	0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b,
-	0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x1a, 0x0a,
-	0x18, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65,
-	0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0x0a, 0x16, 0x4c, 0x69, 0x73,
-	0x74, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x57, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x50,
-	0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b,
-	0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64,
-	0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68,
-	0x4b, 0x65, 0x79, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x73,
-	0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a,
-	0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c,
-	0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x33,
-})
+const file_headscale_v1_preauthkey_proto_rawDesc = "" +
+	"\n" +
+	"\x1dheadscale/v1/preauthkey.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17headscale/v1/user.proto\"\xb6\x02\n" +
+	"\n" +
+	"PreAuthKey\x12&\n" +
+	"\x04user\x18\x01 \x01(\v2\x12.headscale.v1.UserR\x04user\x12\x0e\n" +
+	"\x02id\x18\x02 \x01(\x04R\x02id\x12\x10\n" +
+	"\x03key\x18\x03 \x01(\tR\x03key\x12\x1a\n" +
+	"\breusable\x18\x04 \x01(\bR\breusable\x12\x1c\n" +
+	"\tephemeral\x18\x05 \x01(\bR\tephemeral\x12\x12\n" +
+	"\x04used\x18\x06 \x01(\bR\x04used\x12:\n" +
+	"\n" +
+	"expiration\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\n" +
+	"expiration\x129\n" +
+	"\n" +
+	"created_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x19\n" +
+	"\bacl_tags\x18\t \x03(\tR\aaclTags\"\xbe\x01\n" +
+	"\x17CreatePreAuthKeyRequest\x12\x12\n" +
+	"\x04user\x18\x01 \x01(\x04R\x04user\x12\x1a\n" +
+	"\breusable\x18\x02 \x01(\bR\breusable\x12\x1c\n" +
+	"\tephemeral\x18\x03 \x01(\bR\tephemeral\x12:\n" +
+	"\n" +
+	"expiration\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
+	"expiration\x12\x19\n" +
+	"\bacl_tags\x18\x05 \x03(\tR\aaclTags\"V\n" +
+	"\x18CreatePreAuthKeyResponse\x12:\n" +
+	"\fpre_auth_key\x18\x01 \x01(\v2\x18.headscale.v1.PreAuthKeyR\n" +
+	"preAuthKey\"?\n" +
+	"\x17ExpirePreAuthKeyRequest\x12\x12\n" +
+	"\x04user\x18\x01 \x01(\x04R\x04user\x12\x10\n" +
+	"\x03key\x18\x02 \x01(\tR\x03key\"\x1a\n" +
+	"\x18ExpirePreAuthKeyResponse\",\n" +
+	"\x16ListPreAuthKeysRequest\x12\x12\n" +
+	"\x04user\x18\x01 \x01(\x04R\x04user\"W\n" +
+	"\x17ListPreAuthKeysResponse\x12<\n" +
+	"\rpre_auth_keys\x18\x01 \x03(\v2\x18.headscale.v1.PreAuthKeyR\vpreAuthKeysB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
 
 var (
 	file_headscale_v1_preauthkey_proto_rawDescOnce sync.Once
@@ -511,19 +486,21 @@ var file_headscale_v1_preauthkey_proto_goTypes = []any{
 	(*ExpirePreAuthKeyResponse)(nil), // 4: headscale.v1.ExpirePreAuthKeyResponse
 	(*ListPreAuthKeysRequest)(nil),   // 5: headscale.v1.ListPreAuthKeysRequest
 	(*ListPreAuthKeysResponse)(nil),  // 6: headscale.v1.ListPreAuthKeysResponse
-	(*timestamppb.Timestamp)(nil),    // 7: google.protobuf.Timestamp
+	(*User)(nil),                     // 7: headscale.v1.User
+	(*timestamppb.Timestamp)(nil),    // 8: google.protobuf.Timestamp
 }
 var file_headscale_v1_preauthkey_proto_depIdxs = []int32{
-	7, // 0: headscale.v1.PreAuthKey.expiration:type_name -> google.protobuf.Timestamp
-	7, // 1: headscale.v1.PreAuthKey.created_at:type_name -> google.protobuf.Timestamp
-	7, // 2: headscale.v1.CreatePreAuthKeyRequest.expiration:type_name -> google.protobuf.Timestamp
-	0, // 3: headscale.v1.CreatePreAuthKeyResponse.pre_auth_key:type_name -> headscale.v1.PreAuthKey
-	0, // 4: headscale.v1.ListPreAuthKeysResponse.pre_auth_keys:type_name -> headscale.v1.PreAuthKey
-	5, // [5:5] is the sub-list for method output_type
-	5, // [5:5] is the sub-list for method input_type
-	5, // [5:5] is the sub-list for extension type_name
-	5, // [5:5] is the sub-list for extension extendee
-	0, // [0:5] is the sub-list for field type_name
+	7, // 0: headscale.v1.PreAuthKey.user:type_name -> headscale.v1.User
+	8, // 1: headscale.v1.PreAuthKey.expiration:type_name -> google.protobuf.Timestamp
+	8, // 2: headscale.v1.PreAuthKey.created_at:type_name -> google.protobuf.Timestamp
+	8, // 3: headscale.v1.CreatePreAuthKeyRequest.expiration:type_name -> google.protobuf.Timestamp
+	0, // 4: headscale.v1.CreatePreAuthKeyResponse.pre_auth_key:type_name -> headscale.v1.PreAuthKey
+	0, // 5: headscale.v1.ListPreAuthKeysResponse.pre_auth_keys:type_name -> headscale.v1.PreAuthKey
+	6, // [6:6] is the sub-list for method output_type
+	6, // [6:6] is the sub-list for method input_type
+	6, // [6:6] is the sub-list for extension type_name
+	6, // [6:6] is the sub-list for extension extendee
+	0, // [0:6] is the sub-list for field type_name
 }
 
 func init() { file_headscale_v1_preauthkey_proto_init() }
@@ -531,6 +508,7 @@ func file_headscale_v1_preauthkey_proto_init() {
 	if File_headscale_v1_preauthkey_proto != nil {
 		return
 	}
+	file_headscale_v1_user_proto_init()
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
diff --git a/gen/go/headscale/v1/user.pb.go b/gen/go/headscale/v1/user.pb.go
index a8a238f1..a937f1b6 100644
--- a/gen/go/headscale/v1/user.pb.go
+++ b/gen/go/headscale/v1/user.pb.go
@@ -1,6 +1,6 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.36.5
+// 	protoc-gen-go v1.36.6
 // 	protoc        (unknown)
 // source: headscale/v1/user.proto
 
@@ -516,65 +516,42 @@ func (x *ListUsersResponse) GetUsers() []*User {
 
 var File_headscale_v1_user_proto protoreflect.FileDescriptor
 
-var file_headscale_v1_user_proto_rawDesc = string([]byte{
-	0x0a, 0x17, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x75,
-	0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x68, 0x65, 0x61, 0x64, 0x73,
-	0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
-	0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x83, 0x02, 0x0a, 0x04, 0x55, 0x73, 0x65,
-	0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69,
-	0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
-	0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
-	0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
-	0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74,
-	0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65,
-	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e,
-	0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x6f,
-	0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
-	0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72,
-	0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72,
-	0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c,
-	0x65, 0x5f, 0x70, 0x69, 0x63, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x0d, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x55, 0x72, 0x6c, 0x22, 0x81,
-	0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71,
-	0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70,
-	0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
-	0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65,
-	0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69,
-	0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x75, 0x72, 0x6c,
-	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x55,
-	0x72, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72,
-	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72,
-	0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61,
-	0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72,
-	0x22, 0x45, 0x0a, 0x11, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6f, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08,
-	0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
-	0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3c, 0x0a, 0x12, 0x52, 0x65, 0x6e, 0x61, 0x6d,
-	0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a,
-	0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65,
-	0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52,
-	0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x23, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55,
-	0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
-	0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x22, 0x14, 0x0a, 0x12, 0x44, 0x65,
-	0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
-	0x22, 0x4c, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71,
-	0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04,
-	0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69,
-	0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x3d,
-	0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
-	0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03,
-	0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
-	0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x42, 0x29, 0x5a,
-	0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e,
-	0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67,
-	0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
-})
+const file_headscale_v1_user_proto_rawDesc = "" +
+	"\n" +
+	"\x17headscale/v1/user.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x83\x02\n" +
+	"\x04User\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\x04R\x02id\x12\x12\n" +
+	"\x04name\x18\x02 \x01(\tR\x04name\x129\n" +
+	"\n" +
+	"created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12!\n" +
+	"\fdisplay_name\x18\x04 \x01(\tR\vdisplayName\x12\x14\n" +
+	"\x05email\x18\x05 \x01(\tR\x05email\x12\x1f\n" +
+	"\vprovider_id\x18\x06 \x01(\tR\n" +
+	"providerId\x12\x1a\n" +
+	"\bprovider\x18\a \x01(\tR\bprovider\x12&\n" +
+	"\x0fprofile_pic_url\x18\b \x01(\tR\rprofilePicUrl\"\x81\x01\n" +
+	"\x11CreateUserRequest\x12\x12\n" +
+	"\x04name\x18\x01 \x01(\tR\x04name\x12!\n" +
+	"\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12\x14\n" +
+	"\x05email\x18\x03 \x01(\tR\x05email\x12\x1f\n" +
+	"\vpicture_url\x18\x04 \x01(\tR\n" +
+	"pictureUrl\"<\n" +
+	"\x12CreateUserResponse\x12&\n" +
+	"\x04user\x18\x01 \x01(\v2\x12.headscale.v1.UserR\x04user\"E\n" +
+	"\x11RenameUserRequest\x12\x15\n" +
+	"\x06old_id\x18\x01 \x01(\x04R\x05oldId\x12\x19\n" +
+	"\bnew_name\x18\x02 \x01(\tR\anewName\"<\n" +
+	"\x12RenameUserResponse\x12&\n" +
+	"\x04user\x18\x01 \x01(\v2\x12.headscale.v1.UserR\x04user\"#\n" +
+	"\x11DeleteUserRequest\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\x04R\x02id\"\x14\n" +
+	"\x12DeleteUserResponse\"L\n" +
+	"\x10ListUsersRequest\x12\x0e\n" +
+	"\x02id\x18\x01 \x01(\x04R\x02id\x12\x12\n" +
+	"\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" +
+	"\x05email\x18\x03 \x01(\tR\x05email\"=\n" +
+	"\x11ListUsersResponse\x12(\n" +
+	"\x05users\x18\x01 \x03(\v2\x12.headscale.v1.UserR\x05usersB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
 
 var (
 	file_headscale_v1_user_proto_rawDescOnce sync.Once
diff --git a/gen/openapiv2/headscale/v1/headscale.swagger.json b/gen/openapiv2/headscale/v1/headscale.swagger.json
index ef35ff11..e75b7ee2 100644
--- a/gen/openapiv2/headscale/v1/headscale.swagger.json
+++ b/gen/openapiv2/headscale/v1/headscale.swagger.json
@@ -580,7 +580,8 @@
             "name": "user",
             "in": "query",
             "required": false,
-            "type": "string"
+            "type": "string",
+            "format": "uint64"
           }
         ],
         "tags": [
@@ -909,7 +910,8 @@
       "type": "object",
       "properties": {
         "user": {
-          "type": "string"
+          "type": "string",
+          "format": "uint64"
         },
         "reusable": {
           "type": "boolean"
@@ -1022,7 +1024,8 @@
       "type": "object",
       "properties": {
         "user": {
-          "type": "string"
+          "type": "string",
+          "format": "uint64"
         },
         "key": {
           "type": "string"
@@ -1202,10 +1205,11 @@
       "type": "object",
       "properties": {
         "user": {
-          "type": "string"
+          "$ref": "#/definitions/v1User"
         },
         "id": {
-          "type": "string"
+          "type": "string",
+          "format": "uint64"
         },
         "key": {
           "type": "string"
diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go
index 72fbf2c1..d299771f 100644
--- a/hscontrol/db/db.go
+++ b/hscontrol/db/db.go
@@ -652,7 +652,7 @@ AND auth_key_id NOT IN (
 
 					for nodeID, routes := range nodeRoutes {
 						tsaddr.SortPrefixes(routes)
-						slices.Compact(routes)
+						routes = slices.Compact(routes)
 
 						data, err := json.Marshal(routes)
 
diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go
index f1e5b3ea..d924a1fb 100644
--- a/hscontrol/grpcv1.go
+++ b/hscontrol/grpcv1.go
@@ -161,7 +161,7 @@ func (api headscaleV1APIServer) CreatePreAuthKey(
 		}
 	}
 
-	user, err := api.h.db.GetUserByName(request.GetUser())
+	user, err := api.h.db.GetUserByID(types.UserID(request.GetUser()))
 	if err != nil {
 		return nil, err
 	}
@@ -190,7 +190,7 @@ func (api headscaleV1APIServer) ExpirePreAuthKey(
 			return err
 		}
 
-		if preAuthKey.User.Name != request.GetUser() {
+		if uint64(preAuthKey.User.ID) != request.GetUser() {
 			return fmt.Errorf("preauth key does not belong to user")
 		}
 
@@ -207,7 +207,7 @@ func (api headscaleV1APIServer) ListPreAuthKeys(
 	ctx context.Context,
 	request *v1.ListPreAuthKeysRequest,
 ) (*v1.ListPreAuthKeysResponse, error) {
-	user, err := api.h.db.GetUserByName(request.GetUser())
+	user, err := api.h.db.GetUserByID(types.UserID(request.GetUser()))
 	if err != nil {
 		return nil, err
 	}
diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go
index b85bf3b0..e49057e7 100644
--- a/hscontrol/mapper/mapper.go
+++ b/hscontrol/mapper/mapper.go
@@ -111,7 +111,7 @@ func generateUserProfiles(
 	}
 
 	slices.Sort(ids)
-	slices.Compact(ids)
+	ids = slices.Compact(ids)
 	var profiles []tailcfg.UserProfile
 	for _, id := range ids {
 		if userMap[id] != nil {
diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go
index 85566d0f..e566d64a 100644
--- a/hscontrol/oidc.go
+++ b/hscontrol/oidc.go
@@ -2,6 +2,7 @@ package hscontrol
 
 import (
 	"bytes"
+	"cmp"
 	"context"
 	_ "embed"
 	"errors"
@@ -280,14 +281,28 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
 		return
 	}
 
-	// If EmailVerified is missing, we can try to get it from UserInfo
-	if !claims.EmailVerified {
-		var userinfo *oidc.UserInfo
-		userinfo, err = a.oidcProvider.UserInfo(req.Context(), oauth2.StaticTokenSource(oauth2Token))
-		if err != nil {
-			util.LogErr(err, "could not get userinfo; email cannot be verified")
+	var userinfo *oidc.UserInfo
+	userinfo, err = a.oidcProvider.UserInfo(req.Context(), oauth2.StaticTokenSource(oauth2Token))
+	if err != nil {
+		util.LogErr(err, "could not get userinfo; only checking claim")
+	}
+
+	// If the userinfo is available, we can check if the subject matches the
+	// claims, then use some of the userinfo fields to update the user.
+	// https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
+	if userinfo != nil && userinfo.Subject == claims.Sub {
+		claims.Email = cmp.Or(claims.Email, userinfo.Email)
+		claims.EmailVerified = cmp.Or(claims.EmailVerified, types.FlexibleBoolean(userinfo.EmailVerified))
+
+		// The userinfo has some extra fields that we can use to update the user but they are only
+		// available in the underlying claims struct.
+		// TODO(kradalby): there might be more interesting fields here that we have not found yet.
+		var userinfo2 types.OIDCUserInfo
+		if err := userinfo.Claims(&userinfo2); err == nil {
+			claims.Username = cmp.Or(claims.Username, userinfo2.PreferredUsername)
+			claims.Name = cmp.Or(claims.Name, userinfo2.Name)
+			claims.ProfilePictureURL = cmp.Or(claims.ProfilePictureURL, userinfo2.Picture)
 		}
-		claims.EmailVerified = types.FlexibleBoolean(userinfo.EmailVerified)
 	}
 
 	user, err := a.createOrUpdateUserFromClaim(&claims)
diff --git a/hscontrol/policy/pm.go b/hscontrol/policy/pm.go
index 24f68ca1..29b55fc1 100644
--- a/hscontrol/policy/pm.go
+++ b/hscontrol/policy/pm.go
@@ -11,7 +11,7 @@ import (
 )
 
 var (
-	polv2 = envknob.Bool("HEADSCALE_EXPERIMENTAL_POLICY_V2")
+	polv1 = envknob.Bool("HEADSCALE_POLICY_V1")
 )
 
 type PolicyManager interface {
@@ -35,13 +35,13 @@ type PolicyManager interface {
 func NewPolicyManager(pol []byte, users []types.User, nodes types.Nodes) (PolicyManager, error) {
 	var polMan PolicyManager
 	var err error
-	if polv2 {
-		polMan, err = policyv2.NewPolicyManager(pol, users, nodes)
+	if polv1 {
+		polMan, err = policyv1.NewPolicyManager(pol, users, nodes)
 		if err != nil {
 			return nil, err
 		}
 	} else {
-		polMan, err = policyv1.NewPolicyManager(pol, users, nodes)
+		polMan, err = policyv2.NewPolicyManager(pol, users, nodes)
 		if err != nil {
 			return nil, err
 		}
diff --git a/hscontrol/policy/v1/policy.go b/hscontrol/policy/v1/policy.go
index 0ac49d04..89625ce3 100644
--- a/hscontrol/policy/v1/policy.go
+++ b/hscontrol/policy/v1/policy.go
@@ -7,6 +7,8 @@ import (
 	"os"
 	"sync"
 
+	"slices"
+
 	"github.com/juanfont/headscale/hscontrol/types"
 	"github.com/rs/zerolog/log"
 	"tailscale.com/tailcfg"
@@ -145,13 +147,7 @@ func (pm *PolicyManager) NodeCanHaveTag(node *types.Node, tag string) bool {
 	tags, invalid := pm.pol.TagsOfNode(pm.users, node)
 	log.Debug().Strs("authorised_tags", tags).Strs("unauthorised_tags", invalid).Uint64("node.id", node.ID.Uint64()).Msg("tags provided by policy")
 
-	for _, t := range tags {
-		if t == tag {
-			return true
-		}
-	}
-
-	return false
+	return slices.Contains(tags, tag)
 }
 
 func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefix) bool {
@@ -174,7 +170,7 @@ func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefi
 			}
 
 			// approvedIPs should contain all of node's IPs if it matches the rule, so check for first
-			if ips.Contains(*node.IPv4) {
+			if ips != nil && ips.Contains(*node.IPv4) {
 				return true
 			}
 		}
diff --git a/hscontrol/policy/v2/filter.go b/hscontrol/policy/v2/filter.go
index 2d6c3f12..b94620a3 100644
--- a/hscontrol/policy/v2/filter.go
+++ b/hscontrol/policy/v2/filter.go
@@ -38,7 +38,7 @@ func (pol *Policy) compileFilterRules(
 			log.Trace().Err(err).Msgf("resolving source ips")
 		}
 
-		if len(srcIPs.Prefixes()) == 0 {
+		if srcIPs == nil || len(srcIPs.Prefixes()) == 0 {
 			continue
 		}
 
@@ -56,6 +56,10 @@ func (pol *Policy) compileFilterRules(
 				log.Trace().Err(err).Msgf("resolving destination ips")
 			}
 
+			if ips == nil {
+				continue
+			}
+
 			for _, pref := range ips.Prefixes() {
 				for _, port := range dest.Ports {
 					pr := tailcfg.NetPortRange{
@@ -162,6 +166,10 @@ func (pol *Policy) compileSSHPolicy(
 func ipSetToPrefixStringList(ips *netipx.IPSet) []string {
 	var out []string
 
+	if ips == nil {
+		return out
+	}
+
 	for _, pref := range ips.Prefixes() {
 		out = append(out, pref.String())
 	}
diff --git a/hscontrol/policy/v2/policy.go b/hscontrol/policy/v2/policy.go
index 41f51487..4060b6a6 100644
--- a/hscontrol/policy/v2/policy.go
+++ b/hscontrol/policy/v2/policy.go
@@ -7,6 +7,8 @@ import (
 	"strings"
 	"sync"
 
+	"slices"
+
 	"github.com/juanfont/headscale/hscontrol/types"
 	"go4.org/netipx"
 	"tailscale.com/net/tsaddr"
@@ -174,10 +176,8 @@ func (pm *PolicyManager) NodeCanHaveTag(node *types.Node, tag string) bool {
 	defer pm.mu.Unlock()
 
 	if ips, ok := pm.tagOwnerMap[Tag(tag)]; ok {
-		for _, nodeAddr := range node.IPs() {
-			if ips.Contains(nodeAddr) {
-				return true
-			}
+		if slices.ContainsFunc(node.IPs(), ips.Contains) {
+			return true
 		}
 	}
 
@@ -196,10 +196,8 @@ func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefi
 	// where there is an exact entry, e.g. 10.0.0.0/8, then
 	// check and return quickly
 	if _, ok := pm.autoApproveMap[route]; ok {
-		for _, nodeAddr := range node.IPs() {
-			if pm.autoApproveMap[route].Contains(nodeAddr) {
-				return true
-			}
+		if slices.ContainsFunc(node.IPs(), pm.autoApproveMap[route].Contains) {
+			return true
 		}
 	}
 
@@ -220,10 +218,8 @@ func (pm *PolicyManager) NodeCanApproveRoute(node *types.Node, route netip.Prefi
 		// Check if prefix is larger (so containing) and then overlaps
 		// the route to see if the node can approve a subset of an autoapprover
 		if prefix.Bits() <= route.Bits() && prefix.Overlaps(route) {
-			for _, nodeAddr := range node.IPs() {
-				if approveAddrs.Contains(nodeAddr) {
-					return true
-				}
+			if slices.ContainsFunc(node.IPs(), approveAddrs.Contains) {
+				return true
 			}
 		}
 	}
@@ -279,5 +275,8 @@ func (pm *PolicyManager) DebugString() string {
 		}
 	}
 
+	sb.WriteString("\n\n")
+	sb.WriteString(pm.nodes.DebugString())
+
 	return sb.String()
 }
diff --git a/hscontrol/policy/v2/types.go b/hscontrol/policy/v2/types.go
index e533bafb..55376b97 100644
--- a/hscontrol/policy/v2/types.go
+++ b/hscontrol/policy/v2/types.go
@@ -162,6 +162,10 @@ func (g Group) CanBeAutoApprover() bool {
 	return true
 }
 
+func (g Group) String() string {
+	return string(g)
+}
+
 func (g Group) Resolve(p *Policy, users types.Users, nodes types.Nodes) (*netipx.IPSet, error) {
 	var ips netipx.IPSetBuilder
 	var errs []error
@@ -235,6 +239,10 @@ func (t Tag) CanBeAutoApprover() bool {
 	return true
 }
 
+func (t Tag) String() string {
+	return string(t)
+}
+
 // Host is a string that represents a hostname.
 type Host string
 
@@ -590,6 +598,7 @@ func unmarshalPointer[T any](
 type AutoApprover interface {
 	CanBeAutoApprover() bool
 	UnmarshalJSON([]byte) error
+	String() string
 }
 
 type AutoApprovers []AutoApprover
diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go
index 588d6a71..a0fcfd45 100644
--- a/hscontrol/types/config.go
+++ b/hscontrol/types/config.go
@@ -33,6 +33,7 @@ const (
 var (
 	errOidcMutuallyExclusive = errors.New("oidc_client_secret and oidc_client_secret_path are mutually exclusive")
 	errServerURLSuffix       = errors.New("server_url cannot be part of base_domain in a way that could make the DERP and headscale server unreachable")
+	errServerURLSame         = errors.New("server_url cannot use the same domain as base_domain in a way that could make the DERP and headscale server unreachable")
 	errInvalidPKCEMethod     = errors.New("pkce.method must be either 'plain' or 'S256'")
 )
 
@@ -999,6 +1000,10 @@ func isSafeServerURL(serverURL, baseDomain string) error {
 		return err
 	}
 
+	if server.Hostname() == baseDomain {
+		return errServerURLSame
+	}
+
 	serverDomainParts := strings.Split(server.Host, ".")
 	baseDomainParts := strings.Split(baseDomain, ".")
 
diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go
index 3af43473..3567c4f1 100644
--- a/hscontrol/types/node.go
+++ b/hscontrol/types/node.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net/netip"
 	"slices"
+	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -194,19 +195,26 @@ func (node *Node) IsTagged() bool {
 // Currently, this function only handles tags set
 // via CLI ("forced tags" and preauthkeys)
 func (node *Node) HasTag(tag string) bool {
-	if slices.Contains(node.ForcedTags, tag) {
-		return true
-	}
+	return slices.Contains(node.Tags(), tag)
+}
 
-	if node.AuthKey != nil && slices.Contains(node.AuthKey.Tags, tag) {
-		return true
+func (node *Node) Tags() []string {
+	var tags []string
+
+	if node.AuthKey != nil {
+		tags = append(tags, node.AuthKey.Tags...)
 	}
 
 	// TODO(kradalby): Figure out how tagging should work
 	// and hostinfo.requestedtags.
 	// Do this in other work.
+	// #2417
 
-	return false
+	tags = append(tags, node.ForcedTags...)
+	sort.Strings(tags)
+	tags = slices.Compact(tags)
+
+	return tags
 }
 
 func (node *Node) RequestTags() []string {
@@ -549,3 +557,25 @@ func (nodes Nodes) IDMap() map[NodeID]*Node {
 
 	return ret
 }
+
+func (nodes Nodes) DebugString() string {
+	var sb strings.Builder
+	sb.WriteString("Nodes:\n")
+	for _, node := range nodes {
+		sb.WriteString(node.DebugString())
+		sb.WriteString("\n")
+	}
+	return sb.String()
+}
+
+func (node Node) DebugString() string {
+	var sb strings.Builder
+	fmt.Fprintf(&sb, "%s(%s):\n", node.Hostname, node.ID)
+	fmt.Fprintf(&sb, "\tUser: %s (%d, %q)\n", node.User.Display(), node.User.ID, node.User.Username())
+	fmt.Fprintf(&sb, "\tTags: %v\n", node.Tags())
+	fmt.Fprintf(&sb, "\tIPs: %v\n", node.IPs())
+	fmt.Fprintf(&sb, "\tApprovedRoutes: %v\n", node.ApprovedRoutes)
+	fmt.Fprintf(&sb, "\tSubnetRoutes: %v\n", node.SubnetRoutes())
+	sb.WriteString("\n")
+	return sb.String()
+}
diff --git a/hscontrol/types/preauth_key.go b/hscontrol/types/preauth_key.go
index 7fa67366..3e4441dd 100644
--- a/hscontrol/types/preauth_key.go
+++ b/hscontrol/types/preauth_key.go
@@ -1,11 +1,9 @@
 package types
 
 import (
-	"strconv"
 	"time"
 
 	v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
-	"github.com/juanfont/headscale/hscontrol/util"
 	"google.golang.org/protobuf/types/known/timestamppb"
 )
 
@@ -31,8 +29,8 @@ type PreAuthKey struct {
 
 func (key *PreAuthKey) Proto() *v1.PreAuthKey {
 	protoKey := v1.PreAuthKey{
-		User:      key.User.Username(),
-		Id:        strconv.FormatUint(key.ID, util.Base10),
+		User:      key.User.Proto(),
+		Id:        key.ID,
 		Key:       key.Key,
 		Ephemeral: key.Ephemeral,
 		Reusable:  key.Reusable,
diff --git a/hscontrol/types/users.go b/hscontrol/types/users.go
index 96988a0a..471cb1e5 100644
--- a/hscontrol/types/users.go
+++ b/hscontrol/types/users.go
@@ -157,7 +157,7 @@ func (u *User) Proto() *v1.User {
 type FlexibleBoolean bool
 
 func (bit *FlexibleBoolean) UnmarshalJSON(data []byte) error {
-	var val interface{}
+	var val any
 	err := json.Unmarshal(data, &val)
 	if err != nil {
 		return fmt.Errorf("could not unmarshal data: %w", err)
@@ -203,6 +203,17 @@ func (c *OIDCClaims) Identifier() string {
 	return c.Iss + "/" + c.Sub
 }
 
+type OIDCUserInfo struct {
+	Sub               string          `json:"sub"`
+	Name              string          `json:"name"`
+	GivenName         string          `json:"given_name"`
+	FamilyName        string          `json:"family_name"`
+	PreferredUsername string          `json:"preferred_username"`
+	Email             string          `json:"email"`
+	EmailVerified     FlexibleBoolean `json:"email_verified,omitempty"`
+	Picture           string          `json:"picture"`
+}
+
 // FromClaim overrides a User from OIDC claims.
 // All fields will be updated, except for the ID.
 func (u *User) FromClaim(claims *OIDCClaims) {
diff --git a/hscontrol/util/util.go b/hscontrol/util/util.go
index a41ee6f8..4f6660be 100644
--- a/hscontrol/util/util.go
+++ b/hscontrol/util/util.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net/netip"
 	"net/url"
+	"os"
 	"regexp"
 	"strconv"
 	"strings"
@@ -173,3 +174,15 @@ func ParseTraceroute(output string) (Traceroute, error) {
 
 	return result, nil
 }
+
+func IsCI() bool {
+	if _, ok := os.LookupEnv("CI"); ok {
+		return true
+	}
+
+	if _, ok := os.LookupEnv("GITHUB_RUN_ID"); ok {
+		return true
+	}
+
+	return false
+}
diff --git a/integration/control.go b/integration/control.go
index edbd7b33..9dfe150c 100644
--- a/integration/control.go
+++ b/integration/control.go
@@ -18,10 +18,11 @@ type ControlServer interface {
 	GetHealthEndpoint() string
 	GetEndpoint() string
 	WaitForRunning() error
-	CreateUser(user string) error
-	CreateAuthKey(user string, reusable bool, ephemeral bool) (*v1.PreAuthKey, error)
+	CreateUser(user string) (*v1.User, error)
+	CreateAuthKey(user uint64, reusable bool, ephemeral bool) (*v1.PreAuthKey, error)
 	ListNodes(users ...string) ([]*v1.Node, error)
 	ListUsers() ([]*v1.User, error)
+	MapUsers() (map[string]*v1.User, error)
 	ApproveRoutes(uint64, []netip.Prefix) (*v1.Node, error)
 	GetCert() []byte
 	GetHostname() string
diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go
index f60889f4..27e18697 100644
--- a/integration/hsic/hsic.go
+++ b/integration/hsic/hsic.go
@@ -28,6 +28,7 @@ import (
 	"github.com/ory/dockertest/v3/docker"
 	"gopkg.in/yaml.v3"
 	"tailscale.com/tailcfg"
+	"tailscale.com/util/mak"
 )
 
 const (
@@ -70,7 +71,6 @@ type HeadscaleInContainer struct {
 	tlsKey           []byte
 	filesInContainer []fileInContainer
 	postgres         bool
-	policyV2         bool
 	policyMode       types.PolicyMode
 }
 
@@ -188,11 +188,10 @@ func WithPostgres() Option {
 	}
 }
 
-// WithPolicyV2 tells the integration test to use the new v2 filter.
-func WithPolicyV2() Option {
+// WithPolicyV1 tells the integration test to use the old v1 filter.
+func WithPolicyV1() Option {
 	return func(hsic *HeadscaleInContainer) {
-		hsic.policyV2 = true
-		hsic.env["HEADSCALE_EXPERIMENTAL_POLICY_V2"] = "1"
+		hsic.env["HEADSCALE_POLICY_V1"] = "1"
 	}
 }
 
@@ -705,32 +704,38 @@ func (t *HeadscaleInContainer) WaitForRunning() error {
 // CreateUser adds a new user to the Headscale instance.
 func (t *HeadscaleInContainer) CreateUser(
 	user string,
-) error {
-	command := []string{"headscale", "users", "create", user, fmt.Sprintf("--email=%s@test.no", user)}
+) (*v1.User, error) {
+	command := []string{"headscale", "users", "create", user, fmt.Sprintf("--email=%s@test.no", user), "--output", "json"}
 
-	_, _, err := dockertestutil.ExecuteCommand(
+	result, _, err := dockertestutil.ExecuteCommand(
 		t.container,
 		command,
 		[]string{},
 	)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	return nil
+	var u v1.User
+	err = json.Unmarshal([]byte(result), &u)
+	if err != nil {
+		return nil, fmt.Errorf("failed to unmarshal user: %w", err)
+	}
+
+	return &u, nil
 }
 
 // CreateAuthKey creates a new "authorisation key" for a User that can be used
 // to authorise a TailscaleClient with the Headscale instance.
 func (t *HeadscaleInContainer) CreateAuthKey(
-	user string,
+	user uint64,
 	reusable bool,
 	ephemeral bool,
 ) (*v1.PreAuthKey, error) {
 	command := []string{
 		"headscale",
 		"--user",
-		user,
+		strconv.FormatUint(user, 10),
 		"preauthkeys",
 		"create",
 		"--expiration",
@@ -836,6 +841,22 @@ func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
 	return users, nil
 }
 
+// MapUsers returns a map of users from Headscale. It is keyed by the
+// user name.
+func (t *HeadscaleInContainer) MapUsers() (map[string]*v1.User, error) {
+	users, err := t.ListUsers()
+	if err != nil {
+		return nil, err
+	}
+
+	var userMap map[string]*v1.User
+	for _, user := range users {
+		mak.Set(&userMap, user.Name, user)
+	}
+
+	return userMap, nil
+}
+
 func (h *HeadscaleInContainer) SetPolicy(pol *policyv1.ACLPolicy) error {
 	err := h.writePolicy(pol)
 	if err != nil {
diff --git a/integration/scenario.go b/integration/scenario.go
index 94b37e16..7d4d62d1 100644
--- a/integration/scenario.go
+++ b/integration/scenario.go
@@ -47,7 +47,7 @@ const (
 )
 
 var usePostgresForTest = envknob.Bool("HEADSCALE_INTEGRATION_POSTGRES")
-var usePolicyV2ForTest = envknob.Bool("HEADSCALE_EXPERIMENTAL_POLICY_V2")
+var usePolicyV1ForTest = envknob.Bool("HEADSCALE_POLICY_V1")
 
 var (
 	errNoHeadscaleAvailable = errors.New("no headscale available")
@@ -109,6 +109,9 @@ type Scenario struct {
 
 	spec          ScenarioSpec
 	userToNetwork map[string]*dockertest.Network
+
+	testHashPrefix     string
+	testDefaultNetwork string
 }
 
 // ScenarioSpec describes the users, nodes, and network topology to
@@ -150,11 +153,8 @@ type ScenarioSpec struct {
 	MaxWait time.Duration
 }
 
-var TestHashPrefix = "hs-" + util.MustGenerateRandomStringDNSSafe(scenarioHashLength)
-var TestDefaultNetwork = TestHashPrefix + "-default"
-
-func prefixedNetworkName(name string) string {
-	return TestHashPrefix + "-" + name
+func (s *Scenario) prefixedNetworkName(name string) string {
+	return s.testHashPrefix + "-" + name
 }
 
 // NewScenario creates a test Scenario which can be used to bootstraps a ControlServer with
@@ -169,6 +169,7 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
 	// This might be a no op, but it is worth a try as we sometime
 	// dont clean up nicely after ourselves.
 	dockertestutil.CleanUnreferencedNetworks(pool)
+	dockertestutil.CleanImagesInCI(pool)
 
 	if spec.MaxWait == 0 {
 		pool.MaxWait = dockertestMaxWait()
@@ -176,18 +177,22 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
 		pool.MaxWait = spec.MaxWait
 	}
 
+	testHashPrefix := "hs-" + util.MustGenerateRandomStringDNSSafe(scenarioHashLength)
 	s := &Scenario{
 		controlServers: xsync.NewMapOf[string, ControlServer](),
 		users:          make(map[string]*User),
 
 		pool: pool,
 		spec: spec,
+
+		testHashPrefix:     testHashPrefix,
+		testDefaultNetwork: testHashPrefix + "-default",
 	}
 
 	var userToNetwork map[string]*dockertest.Network
 	if spec.Networks != nil || len(spec.Networks) != 0 {
 		for name, users := range s.spec.Networks {
-			networkName := TestHashPrefix + "-" + name
+			networkName := testHashPrefix + "-" + name
 			network, err := s.AddNetwork(networkName)
 			if err != nil {
 				return nil, err
@@ -201,7 +206,7 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
 			}
 		}
 	} else {
-		_, err := s.AddNetwork(TestDefaultNetwork)
+		_, err := s.AddNetwork(s.testDefaultNetwork)
 		if err != nil {
 			return nil, err
 		}
@@ -213,7 +218,7 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
 			if err != nil {
 				return nil, err
 			}
-			mak.Set(&s.extraServices, prefixedNetworkName(network), append(s.extraServices[prefixedNetworkName(network)], svc))
+			mak.Set(&s.extraServices, s.prefixedNetworkName(network), append(s.extraServices[s.prefixedNetworkName(network)], svc))
 		}
 	}
 
@@ -261,7 +266,7 @@ func (s *Scenario) Networks() []*dockertest.Network {
 }
 
 func (s *Scenario) Network(name string) (*dockertest.Network, error) {
-	net, ok := s.networks[prefixedNetworkName(name)]
+	net, ok := s.networks[s.prefixedNetworkName(name)]
 	if !ok {
 		return nil, fmt.Errorf("no network named: %s", name)
 	}
@@ -270,7 +275,7 @@ func (s *Scenario) Network(name string) (*dockertest.Network, error) {
 }
 
 func (s *Scenario) SubnetOfNetwork(name string) (*netip.Prefix, error) {
-	net, ok := s.networks[prefixedNetworkName(name)]
+	net, ok := s.networks[s.prefixedNetworkName(name)]
 	if !ok {
 		return nil, fmt.Errorf("no network named: %s", name)
 	}
@@ -288,7 +293,7 @@ func (s *Scenario) SubnetOfNetwork(name string) (*netip.Prefix, error) {
 }
 
 func (s *Scenario) Services(name string) ([]*dockertest.Resource, error) {
-	res, ok := s.extraServices[prefixedNetworkName(name)]
+	res, ok := s.extraServices[s.prefixedNetworkName(name)]
 	if !ok {
 		return nil, fmt.Errorf("no network named: %s", name)
 	}
@@ -298,6 +303,7 @@ func (s *Scenario) Services(name string) ([]*dockertest.Resource, error) {
 
 func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
 	defer dockertestutil.CleanUnreferencedNetworks(s.pool)
+	defer dockertestutil.CleanImagesInCI(s.pool)
 
 	s.controlServers.Range(func(_ string, control ControlServer) bool {
 		stdoutPath, stderrPath, err := control.Shutdown()
@@ -408,8 +414,8 @@ func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
 		opts = append(opts, hsic.WithPostgres())
 	}
 
-	if usePolicyV2ForTest {
-		opts = append(opts, hsic.WithPolicyV2())
+	if usePolicyV1ForTest {
+		opts = append(opts, hsic.WithPolicyV1())
 	}
 
 	headscale, err := hsic.New(s.pool, s.Networks(), opts...)
@@ -430,7 +436,7 @@ func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
 // CreatePreAuthKey creates a "pre authentorised key" to be created in the
 // Headscale instance on behalf of the Scenario.
 func (s *Scenario) CreatePreAuthKey(
-	user string,
+	user uint64,
 	reusable bool,
 	ephemeral bool,
 ) (*v1.PreAuthKey, error) {
@@ -448,21 +454,21 @@ func (s *Scenario) CreatePreAuthKey(
 
 // CreateUser creates a User to be created in the
 // Headscale instance on behalf of the Scenario.
-func (s *Scenario) CreateUser(user string) error {
+func (s *Scenario) CreateUser(user string) (*v1.User, error) {
 	if headscale, err := s.Headscale(); err == nil {
-		err := headscale.CreateUser(user)
+		u, err := headscale.CreateUser(user)
 		if err != nil {
-			return fmt.Errorf("failed to create user: %w", err)
+			return nil, fmt.Errorf("failed to create user: %w", err)
 		}
 
 		s.users[user] = &User{
 			Clients: make(map[string]TailscaleClient),
 		}
 
-		return nil
+		return u, nil
 	}
 
-	return fmt.Errorf("failed to create user: %w", errNoHeadscaleAvailable)
+	return nil, fmt.Errorf("failed to create user: %w", errNoHeadscaleAvailable)
 }
 
 /// Client related stuff
@@ -493,8 +499,7 @@ func (s *Scenario) CreateTailscaleNode(
 	)
 	if err != nil {
 		return nil, fmt.Errorf(
-			"failed to create tailscale (%s) node: %w",
-			tsClient.Hostname(),
+			"failed to create tailscale node: %w",
 			err,
 		)
 	}
@@ -698,7 +703,7 @@ func (s *Scenario) createHeadscaleEnv(
 
 	sort.Strings(s.spec.Users)
 	for _, user := range s.spec.Users {
-		err = s.CreateUser(user)
+		u, err := s.CreateUser(user)
 		if err != nil {
 			return err
 		}
@@ -707,7 +712,7 @@ func (s *Scenario) createHeadscaleEnv(
 		if s.userToNetwork != nil {
 			opts = append(tsOpts, tsic.WithNetwork(s.userToNetwork[user]))
 		} else {
-			opts = append(tsOpts, tsic.WithNetwork(s.networks[TestDefaultNetwork]))
+			opts = append(tsOpts, tsic.WithNetwork(s.networks[s.testDefaultNetwork]))
 		}
 
 		err = s.CreateTailscaleNodesInUser(user, "all", s.spec.NodesPerUser, opts...)
@@ -721,7 +726,7 @@ func (s *Scenario) createHeadscaleEnv(
 				return err
 			}
 		} else {
-			key, err := s.CreatePreAuthKey(user, true, false)
+			key, err := s.CreatePreAuthKey(u.GetId(), true, false)
 			if err != nil {
 				return err
 			}
@@ -1181,7 +1186,7 @@ func Webservice(s *Scenario, networkName string) (*dockertest.Resource, error) {
 
 	hostname := fmt.Sprintf("hs-webservice-%s", hash)
 
-	network, ok := s.networks[prefixedNetworkName(networkName)]
+	network, ok := s.networks[s.prefixedNetworkName(networkName)]
 	if !ok {
 		return nil, fmt.Errorf("network does not exist: %s", networkName)
 	}
diff --git a/integration/tailscale.go b/integration/tailscale.go
index 552fc759..94b08364 100644
--- a/integration/tailscale.go
+++ b/integration/tailscale.go
@@ -5,6 +5,7 @@ import (
 	"net/netip"
 	"net/url"
 
+	"github.com/juanfont/headscale/hscontrol/types"
 	"github.com/juanfont/headscale/hscontrol/util"
 	"github.com/juanfont/headscale/integration/dockertestutil"
 	"github.com/juanfont/headscale/integration/tsic"
@@ -43,7 +44,8 @@ type TailscaleClient interface {
 	Ping(hostnameOrIP string, opts ...tsic.PingOption) error
 	Curl(url string, opts ...tsic.CurlOption) (string, error)
 	Traceroute(netip.Addr) (util.Traceroute, error)
-	ID() string
+	ContainerID() string
+	MustID() types.NodeID
 	ReadFile(path string) ([]byte, error)
 
 	// FailingPeersAsString returns a formatted-ish multi-line-string of peers in the client
diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go
index 0c8ba734..57770d41 100644
--- a/integration/tsic/tsic.go
+++ b/integration/tsic/tsic.go
@@ -18,6 +18,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/juanfont/headscale/hscontrol/types"
 	"github.com/juanfont/headscale/hscontrol/util"
 	"github.com/juanfont/headscale/integration/dockertestutil"
 	"github.com/juanfont/headscale/integration/integrationutil"
@@ -194,7 +195,7 @@ func WithBuildTag(tag string) Option {
 // as part of the Login function.
 func WithExtraLoginArgs(args []string) Option {
 	return func(tsic *TailscaleInContainer) {
-		tsic.extraLoginArgs = args
+		tsic.extraLoginArgs = append(tsic.extraLoginArgs, args...)
 	}
 }
 
@@ -383,7 +384,7 @@ func (t *TailscaleInContainer) Version() string {
 
 // ID returns the Docker container ID of the TailscaleInContainer
 // instance.
-func (t *TailscaleInContainer) ID() string {
+func (t *TailscaleInContainer) ContainerID() string {
 	return t.container.Container.ID
 }
 
@@ -426,20 +427,21 @@ func (t *TailscaleInContainer) Logs(stdout, stderr io.Writer) error {
 	)
 }
 
-// Up runs the login routine on the given Tailscale instance.
-// This login mechanism uses the authorised key for authentication.
-func (t *TailscaleInContainer) Login(
+func (t *TailscaleInContainer) buildLoginCommand(
 	loginServer, authKey string,
-) error {
+) []string {
 	command := []string{
 		"tailscale",
 		"up",
 		"--login-server=" + loginServer,
-		"--authkey=" + authKey,
 		"--hostname=" + t.hostname,
 		fmt.Sprintf("--accept-routes=%t", t.withAcceptRoutes),
 	}
 
+	if authKey != "" {
+		command = append(command, "--authkey="+authKey)
+	}
+
 	if t.extraLoginArgs != nil {
 		command = append(command, t.extraLoginArgs...)
 	}
@@ -458,6 +460,16 @@ func (t *TailscaleInContainer) Login(
 		)
 	}
 
+	return command
+}
+
+// Login runs the login routine on the given Tailscale instance.
+// This login mechanism uses the authorised key for authentication.
+func (t *TailscaleInContainer) Login(
+	loginServer, authKey string,
+) error {
+	command := t.buildLoginCommand(loginServer, authKey)
+
 	if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil {
 		return fmt.Errorf(
 			"%s failed to join tailscale client (%s): %w",
@@ -475,17 +487,7 @@ func (t *TailscaleInContainer) Login(
 func (t *TailscaleInContainer) LoginWithURL(
 	loginServer string,
 ) (loginURL *url.URL, err error) {
-	command := []string{
-		"tailscale",
-		"up",
-		"--login-server=" + loginServer,
-		"--hostname=" + t.hostname,
-		"--accept-routes=false",
-	}
-
-	if t.extraLoginArgs != nil {
-		command = append(command, t.extraLoginArgs...)
-	}
+	command := t.buildLoginCommand(loginServer, "")
 
 	stdout, stderr, err := t.Execute(command)
 	if errors.Is(err, errTailscaleNotLoggedIn) {
@@ -646,7 +648,7 @@ func (t *TailscaleInContainer) Status(save ...bool) (*ipnstate.Status, error) {
 	return &status, err
 }
 
-// Status returns the ipnstate.Status of the Tailscale instance.
+// MustStatus returns the ipnstate.Status of the Tailscale instance.
 func (t *TailscaleInContainer) MustStatus() *ipnstate.Status {
 	status, err := t.Status()
 	if err != nil {
@@ -656,6 +658,21 @@ func (t *TailscaleInContainer) MustStatus() *ipnstate.Status {
 	return status
 }
 
+// MustID returns the ID of the Tailscale instance.
+func (t *TailscaleInContainer) MustID() types.NodeID {
+	status, err := t.Status()
+	if err != nil {
+		panic(err)
+	}
+
+	id, err := strconv.ParseUint(string(status.Self.ID), 10, 64)
+	if err != nil {
+		panic(fmt.Sprintf("failed to parse ID: %s", err))
+	}
+
+	return types.NodeID(id)
+}
+
 // Netmap returns the current Netmap (netmap.NetworkMap) of the Tailscale instance.
 // Only works with Tailscale 1.56 and newer.
 // Panics if version is lower then minimum.
diff --git a/integration/utils.go b/integration/utils.go
index 1fcdf6c7..440fa663 100644
--- a/integration/utils.go
+++ b/integration/utils.go
@@ -5,7 +5,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"os"
 	"strings"
 	"sync"
 	"testing"
@@ -344,22 +343,10 @@ func isSelfClient(client TailscaleClient, addr string) bool {
 	return false
 }
 
-func isCI() bool {
-	if _, ok := os.LookupEnv("CI"); ok {
-		return true
-	}
-
-	if _, ok := os.LookupEnv("GITHUB_RUN_ID"); ok {
-		return true
-	}
-
-	return false
-}
-
 func dockertestMaxWait() time.Duration {
 	wait := 120 * time.Second //nolint
 
-	if isCI() {
+	if util.IsCI() {
 		wait = 300 * time.Second //nolint
 	}
 
diff --git a/proto/headscale/v1/preauthkey.proto b/proto/headscale/v1/preauthkey.proto
index 9b8a4e03..de75af11 100644
--- a/proto/headscale/v1/preauthkey.proto
+++ b/proto/headscale/v1/preauthkey.proto
@@ -3,10 +3,11 @@ package headscale.v1;
 option go_package = "github.com/juanfont/headscale/gen/go/v1";
 
 import "google/protobuf/timestamp.proto";
+import "headscale/v1/user.proto";
 
 message PreAuthKey {
-  string user = 1;
-  string id = 2;
+  User user = 1;
+  uint64 id = 2;
   string key = 3;
   bool reusable = 4;
   bool ephemeral = 5;
@@ -17,7 +18,7 @@ message PreAuthKey {
 }
 
 message CreatePreAuthKeyRequest {
-  string user = 1;
+  uint64 user = 1;
   bool reusable = 2;
   bool ephemeral = 3;
   google.protobuf.Timestamp expiration = 4;
@@ -27,12 +28,12 @@ message CreatePreAuthKeyRequest {
 message CreatePreAuthKeyResponse { PreAuthKey pre_auth_key = 1; }
 
 message ExpirePreAuthKeyRequest {
-  string user = 1;
+  uint64 user = 1;
   string key = 2;
 }
 
 message ExpirePreAuthKeyResponse {}
 
-message ListPreAuthKeysRequest { string user = 1; }
+message ListPreAuthKeysRequest { uint64 user = 1; }
 
 message ListPreAuthKeysResponse { repeated PreAuthKey pre_auth_keys = 1; }



Test patch:
diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go
index ced0c9f4..5d718b54 100644
--- a/hscontrol/mapper/mapper_test.go
+++ b/hscontrol/mapper/mapper_test.go
@@ -346,7 +346,7 @@ func Test_fullMapResponse(t *testing.T) {
 						{
 							"action": "accept",
 							"src": ["100.64.0.2"],
-							"dst": ["user1:*"],
+							"dst": ["user1@:*"],
 						},
 					],
 				}
@@ -382,7 +382,7 @@ func Test_fullMapResponse(t *testing.T) {
 						},
 					},
 				},
-				SSHPolicy: &tailcfg.SSHPolicy{},
+				SSHPolicy: nil,
 				UserProfiles: []tailcfg.UserProfile{
 					{ID: tailcfg.UserID(user1.ID), LoginName: "user1", DisplayName: "user1"},
 					{ID: tailcfg.UserID(user2.ID), LoginName: "user2", DisplayName: "user2"},
diff --git a/hscontrol/types/config_test.go b/hscontrol/types/config_test.go
index e7afee69..7ae3db59 100644
--- a/hscontrol/types/config_test.go
+++ b/hscontrol/types/config_test.go
@@ -423,6 +423,7 @@ func TestSafeServerURL(t *testing.T) {
 		{
 			serverURL:  "https://headscale.com",
 			baseDomain: "headscale.com",
+			wantErr:    errServerURLSame.Error(),
 		},
 		{
 			serverURL:  "https://headscale.com",
diff --git a/integration/acl_test.go b/integration/acl_test.go
index 72f44cc0..bb18b3b3 100644
--- a/integration/acl_test.go
+++ b/integration/acl_test.go
@@ -1054,7 +1054,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
 	// Initially all nodes can reach each other
 	for _, client := range all {
 		for _, peer := range all {
-			if client.ID() == peer.ID() {
+			if client.ContainerID() == peer.ContainerID() {
 				continue
 			}
 
diff --git a/integration/auth_key_test.go b/integration/auth_key_test.go
index 9d219fca..ca5c8d0d 100644
--- a/integration/auth_key_test.go
+++ b/integration/auth_key_test.go
@@ -3,9 +3,12 @@ package integration
 import (
 	"fmt"
 	"net/netip"
+	"strconv"
 	"testing"
 	"time"
 
+	"slices"
+
 	"github.com/juanfont/headscale/integration/hsic"
 	"github.com/juanfont/headscale/integration/tsic"
 	"github.com/samber/lo"
@@ -84,8 +87,11 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
 				time.Sleep(5 * time.Minute)
 			}
 
+			userMap, err := headscale.MapUsers()
+			assertNoErr(t, err)
+
 			for _, userName := range spec.Users {
-				key, err := scenario.CreatePreAuthKey(userName, true, false)
+				key, err := scenario.CreatePreAuthKey(userMap[userName].GetId(), true, false)
 				if err != nil {
 					t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
 				}
@@ -121,16 +127,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
 				}
 
 				for _, ip := range ips {
-					found := false
-					for _, oldIP := range clientIPs[client] {
-						if ip == oldIP {
-							found = true
-
-							break
-						}
-					}
-
-					if !found {
+					if !slices.Contains(clientIPs[client], ip) {
 						t.Fatalf(
 							"IPs changed for client %s. Used to be %v now %v",
 							client.Hostname(),
@@ -195,8 +192,11 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
 
 	t.Logf("all clients logged out")
 
+	userMap, err := headscale.MapUsers()
+	assertNoErr(t, err)
+
 	// Create a new authkey for user1, to be used for all clients
-	key, err := scenario.CreatePreAuthKey("user1", true, false)
+	key, err := scenario.CreatePreAuthKey(userMap["user1"].GetId(), true, false)
 	if err != nil {
 		t.Fatalf("failed to create pre-auth key for user1: %s", err)
 	}
@@ -300,8 +300,11 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
 				time.Sleep(5 * time.Minute)
 			}
 
+			userMap, err := headscale.MapUsers()
+			assertNoErr(t, err)
+
 			for _, userName := range spec.Users {
-				key, err := scenario.CreatePreAuthKey(userName, true, false)
+				key, err := scenario.CreatePreAuthKey(userMap[userName].GetId(), true, false)
 				if err != nil {
 					t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
 				}
@@ -312,7 +315,7 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
 						"headscale",
 						"preauthkeys",
 						"--user",
-						userName,
+						strconv.FormatUint(userMap[userName].GetId(), 10),
 						"expire",
 						key.Key,
 					})
diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go
index a036fdd0..53c74577 100644
--- a/integration/auth_oidc_test.go
+++ b/integration/auth_oidc_test.go
@@ -442,7 +442,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
 	assertNoErr(t, err)
 	assert.Len(t, listUsers, 0)
 
-	ts, err := scenario.CreateTailscaleNode("unstable", tsic.WithNetwork(scenario.networks[TestDefaultNetwork]))
+	ts, err := scenario.CreateTailscaleNode("unstable", tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]))
 	assertNoErr(t, err)
 
 	u, err := ts.LoginWithURL(headscale.GetEndpoint())
diff --git a/integration/auth_web_flow_test.go b/integration/auth_web_flow_test.go
index 034ad5ae..64cace7b 100644
--- a/integration/auth_web_flow_test.go
+++ b/integration/auth_web_flow_test.go
@@ -26,7 +26,7 @@ func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
 	}
 	defer scenario.ShutdownAssertNoPanics(t)
 
-	err = scenario.CreateHeadscaleEnv(
+	err = scenario.CreateHeadscaleEnvWithLoginURL(
 		nil,
 		hsic.WithTestName("webauthping"),
 		hsic.WithEmbeddedDERPServerOnly(),
@@ -66,7 +66,7 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) {
 	assertNoErr(t, err)
 	defer scenario.ShutdownAssertNoPanics(t)
 
-	err = scenario.CreateHeadscaleEnv(
+	err = scenario.CreateHeadscaleEnvWithLoginURL(
 		nil,
 		hsic.WithTestName("weblogout"),
 		hsic.WithTLS(),
diff --git a/integration/cli_test.go b/integration/cli_test.go
index df3eb775..a474f5c2 100644
--- a/integration/cli_test.go
+++ b/integration/cli_test.go
@@ -4,6 +4,7 @@ import (
 	"cmp"
 	"encoding/json"
 	"fmt"
+	"strconv"
 	"strings"
 	"testing"
 	"time"
@@ -271,7 +272,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
 				"headscale",
 				"preauthkeys",
 				"--user",
-				user,
+				"1",
 				"create",
 				"--reusable",
 				"--expiration",
@@ -297,7 +298,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
 			"headscale",
 			"preauthkeys",
 			"--user",
-			user,
+			"1",
 			"list",
 			"--output",
 			"json",
@@ -311,8 +312,8 @@ func TestPreAuthKeyCommand(t *testing.T) {
 
 	assert.Equal(
 		t,
-		[]string{keys[0].GetId(), keys[1].GetId(), keys[2].GetId()},
-		[]string{
+		[]uint64{keys[0].GetId(), keys[1].GetId(), keys[2].GetId()},
+		[]uint64{
 			listedPreAuthKeys[1].GetId(),
 			listedPreAuthKeys[2].GetId(),
 			listedPreAuthKeys[3].GetId(),
@@ -354,7 +355,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
 			"headscale",
 			"preauthkeys",
 			"--user",
-			user,
+			"1",
 			"expire",
 			listedPreAuthKeys[1].GetKey(),
 		},
@@ -368,7 +369,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
 			"headscale",
 			"preauthkeys",
 			"--user",
-			user,
+			"1",
 			"list",
 			"--output",
 			"json",
@@ -408,7 +409,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
 			"headscale",
 			"preauthkeys",
 			"--user",
-			user,
+			"1",
 			"create",
 			"--reusable",
 			"--output",
@@ -425,7 +426,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
 			"headscale",
 			"preauthkeys",
 			"--user",
-			user,
+			"1",
 			"list",
 			"--output",
 			"json",
@@ -470,7 +471,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
 			"headscale",
 			"preauthkeys",
 			"--user",
-			user,
+			"1",
 			"create",
 			"--reusable=true",
 			"--output",
@@ -487,7 +488,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
 			"headscale",
 			"preauthkeys",
 			"--user",
-			user,
+			"1",
 			"create",
 			"--ephemeral=true",
 			"--output",
@@ -507,7 +508,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
 			"headscale",
 			"preauthkeys",
 			"--user",
-			user,
+			"1",
 			"list",
 			"--output",
 			"json",
@@ -547,7 +548,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
 	headscale, err := scenario.Headscale()
 	assertNoErr(t, err)
 
-	err = headscale.CreateUser(user2)
+	u2, err := headscale.CreateUser(user2)
 	assertNoErr(t, err)
 
 	var user2Key v1.PreAuthKey
@@ -558,7 +559,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
 			"headscale",
 			"preauthkeys",
 			"--user",
-			user2,
+			strconv.FormatUint(u2.GetId(), 10),
 			"create",
 			"--reusable",
 			"--expiration",
diff --git a/integration/dns_test.go b/integration/dns_test.go
index 77b0f639..ef6c479b 100644
--- a/integration/dns_test.go
+++ b/integration/dns_test.go
@@ -238,166 +238,3 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
 		assertCommandOutputContains(t, client, []string{"dig", "copy.myvpn.example.com"}, "8.8.8.8")
 	}
 }
-
-// TestValidateResolvConf validates that the resolv.conf file
-// ends up as expected in our Tailscale containers.
-// All the containers are based on Alpine, meaning Tailscale
-// will overwrite the resolv.conf file.
-// On other platform, Tailscale will integrate with a dns manager
-// if available (like systemd-resolved).
-func TestValidateResolvConf(t *testing.T) {
-	IntegrationSkip(t)
-
-	resolvconf := func(conf string) string {
-		return strings.ReplaceAll(`# resolv.conf(5) file generated by tailscale
-# For more info, see https://tailscale.com/s/resolvconf-overwrite
-# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN
-`+conf, "\t", "")
-	}
-
-	tests := []struct {
-		name                string
-		conf                map[string]string
-		wantConfCompareFunc func(*testing.T, string)
-	}{
-		// New config
-		{
-			name: "no-config",
-			conf: map[string]string{
-				"HEADSCALE_DNS_BASE_DOMAIN":        "",
-				"HEADSCALE_DNS_MAGIC_DNS":          "false",
-				"HEADSCALE_DNS_NAMESERVERS_GLOBAL": "",
-			},
-			wantConfCompareFunc: func(t *testing.T, got string) {
-				assert.Contains(t, got, "Generated by Docker Engine")
-			},
-		},
-		{
-			name: "global-only",
-			conf: map[string]string{
-				"HEADSCALE_DNS_BASE_DOMAIN":        "",
-				"HEADSCALE_DNS_MAGIC_DNS":          "false",
-				"HEADSCALE_DNS_NAMESERVERS_GLOBAL": "8.8.8.8 1.1.1.1",
-			},
-			wantConfCompareFunc: func(t *testing.T, got string) {
-				want := resolvconf(`
-					nameserver 100.100.100.100
-				`)
-				assert.Equal(t, want, got)
-			},
-		},
-		{
-			name: "base-integration-config",
-			conf: map[string]string{
-				"HEADSCALE_DNS_BASE_DOMAIN": "very-unique-domain.net",
-			},
-			wantConfCompareFunc: func(t *testing.T, got string) {
-				want := resolvconf(`
-					nameserver 100.100.100.100
-					search very-unique-domain.net
-				`)
-				assert.Equal(t, want, got)
-			},
-		},
-		{
-			name: "base-magic-dns-off",
-			conf: map[string]string{
-				"HEADSCALE_DNS_MAGIC_DNS":   "false",
-				"HEADSCALE_DNS_BASE_DOMAIN": "very-unique-domain.net",
-			},
-			wantConfCompareFunc: func(t *testing.T, got string) {
-				want := resolvconf(`
-					nameserver 100.100.100.100
-					search very-unique-domain.net
-				`)
-				assert.Equal(t, want, got)
-			},
-		},
-		{
-			name: "base-extra-search-domains",
-			conf: map[string]string{
-				"HEADSCALE_DNS_SEARCH_DOMAINS": "test1.no test2.no",
-				"HEADSCALE_DNS_BASE_DOMAIN":    "with-local-dns.net",
-			},
-			wantConfCompareFunc: func(t *testing.T, got string) {
-				want := resolvconf(`
-					nameserver 100.100.100.100
-					search with-local-dns.net test1.no test2.no
-				`)
-				assert.Equal(t, want, got)
-			},
-		},
-		{
-			name: "base-nameservers-split",
-			conf: map[string]string{
-				"HEADSCALE_DNS_NAMESERVERS_SPLIT": `{foo.bar.com: ["1.1.1.1"]}`,
-				"HEADSCALE_DNS_BASE_DOMAIN":       "with-local-dns.net",
-			},
-			wantConfCompareFunc: func(t *testing.T, got string) {
-				want := resolvconf(`
-					nameserver 100.100.100.100
-					search with-local-dns.net
-				`)
-				assert.Equal(t, want, got)
-			},
-		},
-		{
-			name: "base-full-no-magic",
-			conf: map[string]string{
-				"HEADSCALE_DNS_MAGIC_DNS":          "false",
-				"HEADSCALE_DNS_BASE_DOMAIN":        "all-of.it",
-				"HEADSCALE_DNS_NAMESERVERS_GLOBAL": `8.8.8.8`,
-				"HEADSCALE_DNS_SEARCH_DOMAINS":     "test1.no test2.no",
-				// TODO(kradalby): this currently isn't working, need to fix it
-				// "HEADSCALE_DNS_NAMESERVERS_SPLIT": `{foo.bar.com: ["1.1.1.1"]}`,
-				// "HEADSCALE_DNS_EXTRA_RECORDS":     `[{ name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.4" }]`,
-			},
-			wantConfCompareFunc: func(t *testing.T, got string) {
-				want := resolvconf(`
-					nameserver 100.100.100.100
-					search all-of.it test1.no test2.no
-				`)
-				assert.Equal(t, want, got)
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			spec := ScenarioSpec{
-				NodesPerUser: 3,
-				Users:        []string{"user1", "user2"},
-			}
-
-			scenario, err := NewScenario(spec)
-			assertNoErr(t, err)
-			defer scenario.ShutdownAssertNoPanics(t)
-
-			err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("resolvconf"), hsic.WithConfigEnv(tt.conf))
-			assertNoErrHeadscaleEnv(t, err)
-
-			allClients, err := scenario.ListTailscaleClients()
-			assertNoErrListClients(t, err)
-
-			err = scenario.WaitForTailscaleSync()
-			assertNoErrSync(t, err)
-
-			// Poor mans cache
-			_, err = scenario.ListTailscaleClientsFQDNs()
-			assertNoErrListFQDN(t, err)
-
-			_, err = scenario.ListTailscaleClientsIPs()
-			assertNoErrListClientIPs(t, err)
-
-			time.Sleep(30 * time.Second)
-
-			for _, client := range allClients {
-				b, err := client.ReadFile("/etc/resolv.conf")
-				assertNoErr(t, err)
-
-				t.Logf("comparing resolv conf of %s", client.Hostname())
-				tt.wantConfCompareFunc(t, string(b))
-			}
-		})
-	}
-}
diff --git a/integration/dockertestutil/network.go b/integration/dockertestutil/network.go
index 9b51986b..83fc08c4 100644
--- a/integration/dockertestutil/network.go
+++ b/integration/dockertestutil/network.go
@@ -6,6 +6,7 @@ import (
 	"log"
 	"net"
 
+	"github.com/juanfont/headscale/hscontrol/util"
 	"github.com/ory/dockertest/v3"
 	"github.com/ory/dockertest/v3/docker"
 )
@@ -105,3 +106,23 @@ func CleanUnreferencedNetworks(pool *dockertest.Pool) error {
 
 	return nil
 }
+
+// CleanImagesInCI removes images if running in CI.
+func CleanImagesInCI(pool *dockertest.Pool) error {
+	if !util.IsCI() {
+		log.Println("Skipping image cleanup outside of CI")
+		return nil
+	}
+
+	images, err := pool.Client.ListImages(docker.ListImagesOptions{})
+	if err != nil {
+		return fmt.Errorf("getting images: %w", err)
+	}
+
+	for _, image := range images {
+		log.Printf("removing image: %s, %v", image.ID, image.RepoTags)
+		_ = pool.Client.RemoveImage(image.ID)
+	}
+
+	return nil
+}
diff --git a/integration/general_test.go b/integration/general_test.go
index 02936f16..292eb5ca 100644
--- a/integration/general_test.go
+++ b/integration/general_test.go
@@ -133,17 +133,17 @@ func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) {
 	assertNoErrHeadscaleEnv(t, err)
 
 	for _, userName := range spec.Users {
-		err = scenario.CreateUser(userName)
+		user, err := scenario.CreateUser(userName)
 		if err != nil {
 			t.Fatalf("failed to create user %s: %s", userName, err)
 		}
 
-		err = scenario.CreateTailscaleNodesInUser(userName, "all", spec.NodesPerUser, tsic.WithNetwork(scenario.networks[TestDefaultNetwork]))
+		err = scenario.CreateTailscaleNodesInUser(userName, "all", spec.NodesPerUser, tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]))
 		if err != nil {
 			t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err)
 		}
 
-		key, err := scenario.CreatePreAuthKey(userName, true, true)
+		key, err := scenario.CreatePreAuthKey(user.GetId(), true, true)
 		if err != nil {
 			t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
 		}
@@ -211,17 +211,17 @@ func TestEphemeral2006DeletedTooQuickly(t *testing.T) {
 	assertNoErrHeadscaleEnv(t, err)
 
 	for _, userName := range spec.Users {
-		err = scenario.CreateUser(userName)
+		user, err := scenario.CreateUser(userName)
 		if err != nil {
 			t.Fatalf("failed to create user %s: %s", userName, err)
 		}
 
-		err = scenario.CreateTailscaleNodesInUser(userName, "all", spec.NodesPerUser, tsic.WithNetwork(scenario.networks[TestDefaultNetwork]))
+		err = scenario.CreateTailscaleNodesInUser(userName, "all", spec.NodesPerUser, tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]))
 		if err != nil {
 			t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err)
 		}
 
-		key, err := scenario.CreatePreAuthKey(userName, true, true)
+		key, err := scenario.CreatePreAuthKey(user.GetId(), true, true)
 		if err != nil {
 			t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
 		}
diff --git a/integration/route_test.go b/integration/route_test.go
index ece89909..8801767d 100644
--- a/integration/route_test.go
+++ b/integration/route_test.go
@@ -287,9 +287,9 @@ func TestHASubnetRouterFailover(t *testing.T) {
 	require.NoError(t, err)
 	assert.Len(t, nodes, 6)
 
-	assertNodeRouteCount(t, nodes[0], 1, 0, 0)
-	assertNodeRouteCount(t, nodes[1], 1, 0, 0)
-	assertNodeRouteCount(t, nodes[2], 1, 0, 0)
+	requireNodeRouteCount(t, nodes[0], 1, 0, 0)
+	requireNodeRouteCount(t, nodes[1], 1, 0, 0)
+	requireNodeRouteCount(t, nodes[2], 1, 0, 0)
 
 	// Verify that no routes has been sent to the client,
 	// they are not yet enabled.
@@ -319,9 +319,9 @@ func TestHASubnetRouterFailover(t *testing.T) {
 	require.NoError(t, err)
 	assert.Len(t, nodes, 6)
 
-	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
-	assertNodeRouteCount(t, nodes[1], 1, 0, 0)
-	assertNodeRouteCount(t, nodes[2], 1, 0, 0)
+	requireNodeRouteCount(t, nodes[0], 1, 1, 1)
+	requireNodeRouteCount(t, nodes[1], 1, 0, 0)
+	requireNodeRouteCount(t, nodes[2], 1, 0, 0)
 
 	// Verify that the client has routes from the primary machine and can access
 	// the webservice.
@@ -375,9 +375,9 @@ func TestHASubnetRouterFailover(t *testing.T) {
 	require.NoError(t, err)
 	assert.Len(t, nodes, 6)
 
-	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
-	assertNodeRouteCount(t, nodes[1], 1, 1, 0)
-	assertNodeRouteCount(t, nodes[2], 1, 0, 0)
+	requireNodeRouteCount(t, nodes[0], 1, 1, 1)
+	requireNodeRouteCount(t, nodes[1], 1, 1, 0)
+	requireNodeRouteCount(t, nodes[2], 1, 0, 0)
 
 	// Verify that the client has routes from the primary machine
 	srs1 = subRouter1.MustStatus()
@@ -431,9 +431,9 @@ func TestHASubnetRouterFailover(t *testing.T) {
 	require.NoError(t, err)
 	assert.Len(t, nodes, 6)
 
-	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
-	assertNodeRouteCount(t, nodes[1], 1, 1, 0)
-	assertNodeRouteCount(t, nodes[2], 1, 1, 0)
+	requireNodeRouteCount(t, nodes[0], 1, 1, 1)
+	requireNodeRouteCount(t, nodes[1], 1, 1, 0)
+	requireNodeRouteCount(t, nodes[2], 1, 1, 0)
 
 	// Verify that the client has routes from the primary machine
 	srs1 = subRouter1.MustStatus()
@@ -645,9 +645,9 @@ func TestHASubnetRouterFailover(t *testing.T) {
 	require.NoError(t, err)
 	assert.Len(t, nodes, 6)
 
-	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
-	assertNodeRouteCount(t, nodes[1], 1, 1, 0)
-	assertNodeRouteCount(t, nodes[2], 1, 0, 0)
+	requireNodeRouteCount(t, nodes[0], 1, 1, 1)
+	requireNodeRouteCount(t, nodes[1], 1, 1, 0)
+	requireNodeRouteCount(t, nodes[2], 1, 0, 0)
 
 	// Verify that the route is announced from subnet router 1
 	clientStatus, err = client.Status()
@@ -690,9 +690,9 @@ func TestHASubnetRouterFailover(t *testing.T) {
 	require.NoError(t, err)
 	assert.Len(t, nodes, 6)
 
-	assertNodeRouteCount(t, nodes[0], 1, 0, 0)
-	assertNodeRouteCount(t, nodes[1], 1, 1, 1)
-	assertNodeRouteCount(t, nodes[2], 1, 0, 0)
+	requireNodeRouteCount(t, nodes[0], 1, 0, 0)
+	requireNodeRouteCount(t, nodes[1], 1, 1, 1)
+	requireNodeRouteCount(t, nodes[2], 1, 0, 0)
 
 	// Verify that the route is announced from subnet router 1
 	clientStatus, err = client.Status()
@@ -738,9 +738,9 @@ func TestHASubnetRouterFailover(t *testing.T) {
 	require.NoError(t, err)
 	assert.Len(t, nodes, 6)
 
-	assertNodeRouteCount(t, nodes[0], 1, 1, 0)
-	assertNodeRouteCount(t, nodes[1], 1, 1, 1)
-	assertNodeRouteCount(t, nodes[2], 1, 0, 0)
+	requireNodeRouteCount(t, nodes[0], 1, 1, 0)
+	requireNodeRouteCount(t, nodes[1], 1, 1, 1)
+	requireNodeRouteCount(t, nodes[2], 1, 0, 0)
 
 	// Verify that the route is announced from subnet router 1
 	clientStatus, err = client.Status()
@@ -870,8 +870,8 @@ func TestSubnetRouteACL(t *testing.T) {
 	require.NoError(t, err)
 	require.Len(t, nodes, 2)
 
-	assertNodeRouteCount(t, nodes[0], 1, 0, 0)
-	assertNodeRouteCount(t, nodes[1], 0, 0, 0)
+	requireNodeRouteCount(t, nodes[0], 1, 0, 0)
+	requireNodeRouteCount(t, nodes[1], 0, 0, 0)
 
 	// Verify that no routes has been sent to the client,
 	// they are not yet enabled.
@@ -899,8 +899,8 @@ func TestSubnetRouteACL(t *testing.T) {
 	require.NoError(t, err)
 	require.Len(t, nodes, 2)
 
-	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
-	assertNodeRouteCount(t, nodes[1], 0, 0, 0)
+	requireNodeRouteCount(t, nodes[0], 1, 1, 1)
+	requireNodeRouteCount(t, nodes[1], 0, 0, 0)
 
 	// Verify that the client has routes from the primary machine
 	srs1, _ := subRouter1.Status()
@@ -1034,8 +1034,8 @@ func TestEnablingExitRoutes(t *testing.T) {
 	require.NoError(t, err)
 	require.Len(t, nodes, 2)
 
-	assertNodeRouteCount(t, nodes[0], 2, 0, 0)
-	assertNodeRouteCount(t, nodes[1], 2, 0, 0)
+	requireNodeRouteCount(t, nodes[0], 2, 0, 0)
+	requireNodeRouteCount(t, nodes[1], 2, 0, 0)
 
 	// Verify that no routes has been sent to the client,
 	// they are not yet enabled.
@@ -1067,8 +1067,8 @@ func TestEnablingExitRoutes(t *testing.T) {
 	require.NoError(t, err)
 	require.Len(t, nodes, 2)
 
-	assertNodeRouteCount(t, nodes[0], 2, 2, 2)
-	assertNodeRouteCount(t, nodes[1], 2, 2, 2)
+	requireNodeRouteCount(t, nodes[0], 2, 2, 2)
+	requireNodeRouteCount(t, nodes[1], 2, 2, 2)
 
 	time.Sleep(5 * time.Second)
 
@@ -1158,7 +1158,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
 	nodes, err := headscale.ListNodes()
 	require.NoError(t, err)
 	assert.Len(t, nodes, 2)
-	assertNodeRouteCount(t, nodes[0], 1, 0, 0)
+	requireNodeRouteCount(t, nodes[0], 1, 0, 0)
 
 	// Verify that no routes has been sent to the client,
 	// they are not yet enabled.
@@ -1184,7 +1184,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
 	nodes, err = headscale.ListNodes()
 	require.NoError(t, err)
 	assert.Len(t, nodes, 2)
-	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
+	requireNodeRouteCount(t, nodes[0], 1, 1, 1)
 
 	// Verify that the routes have been sent to the client.
 	status, err = user2c.Status()
@@ -1282,7 +1282,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
 	nodes, err := headscale.ListNodes()
 	require.NoError(t, err)
 	assert.Len(t, nodes, 2)
-	assertNodeRouteCount(t, nodes[0], 2, 0, 0)
+	requireNodeRouteCount(t, nodes[0], 2, 0, 0)
 
 	// Verify that no routes has been sent to the client,
 	// they are not yet enabled.
@@ -1305,7 +1305,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
 	nodes, err = headscale.ListNodes()
 	require.NoError(t, err)
 	assert.Len(t, nodes, 2)
-	assertNodeRouteCount(t, nodes[0], 2, 2, 2)
+	requireNodeRouteCount(t, nodes[0], 2, 2, 2)
 
 	// Verify that the routes have been sent to the client.
 	status, err = user2c.Status()
@@ -1349,6 +1349,15 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
 	require.NoError(t, err)
 }
 
+func MustFindNode(hostname string, nodes []*v1.Node) *v1.Node {
+	for _, node := range nodes {
+		if node.GetName() == hostname {
+			return node
+		}
+	}
+	panic("node not found")
+}
+
 // TestAutoApproveMultiNetwork tests auto approving of routes
 // by setting up two networks where network1 has three subnet
 // routers:
@@ -1367,358 +1376,604 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
 //   - Verify that routes can now be seen by peers.
 func TestAutoApproveMultiNetwork(t *testing.T) {
 	IntegrationSkip(t)
-	t.Parallel()
-
-	spec := ScenarioSpec{
-		NodesPerUser: 3,
-		Users:        []string{"user1", "user2"},
-		Networks: map[string][]string{
-			"usernet1": {"user1"},
-			"usernet2": {"user2"},
-		},
-		ExtraService: map[string][]extraServiceFunc{
-			"usernet1": {Webservice},
-		},
-		// We build the head image with curl and traceroute, so only use
-		// that for this test.
-		Versions: []string{"head"},
-	}
-
-	rootRoute := netip.MustParsePrefix("10.42.0.0/16")
+	bigRoute := netip.MustParsePrefix("10.42.0.0/16")
 	subRoute := netip.MustParsePrefix("10.42.7.0/24")
 	notApprovedRoute := netip.MustParsePrefix("192.168.0.0/24")
 
-	scenario, err := NewScenario(spec)
-	require.NoErrorf(t, err, "failed to create scenario: %s", err)
-	defer scenario.ShutdownAssertNoPanics(t)
-
-	pol := &policyv1.ACLPolicy{
-		ACLs: []policyv1.ACL{
-			{
-				Action:       "accept",
-				Sources:      []string{"*"},
-				Destinations: []string{"*:*"},
+	tests := []struct {
+		name     string
+		pol      *policyv1.ACLPolicy
+		approver string
+		spec     ScenarioSpec
+		withURL  bool
+	}{
+		{
+			name: "authkey-tag",
+			pol: &policyv1.ACLPolicy{
+				ACLs: []policyv1.ACL{
+					{
+						Action:       "accept",
+						Sources:      []string{"*"},
+						Destinations: []string{"*:*"},
+					},
+				},
+				TagOwners: map[string][]string{
+					"tag:approve": {"user1@"},
+				},
+				AutoApprovers: policyv1.AutoApprovers{
+					Routes: map[string][]string{
+						bigRoute.String(): {"tag:approve"},
+					},
+					ExitNode: []string{"tag:approve"},
+				},
+			},
+			approver: "tag:approve",
+			spec: ScenarioSpec{
+				NodesPerUser: 3,
+				Users:        []string{"user1", "user2"},
+				Networks: map[string][]string{
+					"usernet1": {"user1"},
+					"usernet2": {"user2"},
+				},
+				ExtraService: map[string][]extraServiceFunc{
+					"usernet1": {Webservice},
+				},
+				// We build the head image with curl and traceroute, so only use
+				// that for this test.
+				Versions: []string{"head"},
 			},
 		},
-		TagOwners: map[string][]string{
-			"tag:approve": {"user1@"},
+		{
+			name: "authkey-user",
+			pol: &policyv1.ACLPolicy{
+				ACLs: []policyv1.ACL{
+					{
+						Action:       "accept",
+						Sources:      []string{"*"},
+						Destinations: []string{"*:*"},
+					},
+				},
+				AutoApprovers: policyv1.AutoApprovers{
+					Routes: map[string][]string{
+						bigRoute.String(): {"user1@"},
+					},
+					ExitNode: []string{"user1@"},
+				},
+			},
+			approver: "user1@",
+			spec: ScenarioSpec{
+				NodesPerUser: 3,
+				Users:        []string{"user1", "user2"},
+				Networks: map[string][]string{
+					"usernet1": {"user1"},
+					"usernet2": {"user2"},
+				},
+				ExtraService: map[string][]extraServiceFunc{
+					"usernet1": {Webservice},
+				},
+				// We build the head image with curl and traceroute, so only use
+				// that for this test.
+				Versions: []string{"head"},
+			},
 		},
-		AutoApprovers: policyv1.AutoApprovers{
-			Routes: map[string][]string{
-				rootRoute.String(): {"tag:approve"},
+		{
+			name: "authkey-group",
+			pol: &policyv1.ACLPolicy{
+				ACLs: []policyv1.ACL{
+					{
+						Action:       "accept",
+						Sources:      []string{"*"},
+						Destinations: []string{"*:*"},
+					},
+				},
+				Groups: policyv1.Groups{
+					"group:approve": []string{"user1@"},
+				},
+				AutoApprovers: policyv1.AutoApprovers{
+					Routes: map[string][]string{
+						bigRoute.String(): {"group:approve"},
+					},
+					ExitNode: []string{"group:approve"},
+				},
+			},
+			approver: "group:approve",
+			spec: ScenarioSpec{
+				NodesPerUser: 3,
+				Users:        []string{"user1", "user2"},
+				Networks: map[string][]string{
+					"usernet1": {"user1"},
+					"usernet2": {"user2"},
+				},
+				ExtraService: map[string][]extraServiceFunc{
+					"usernet1": {Webservice},
+				},
+				// We build the head image with curl and traceroute, so only use
+				// that for this test.
+				Versions: []string{"head"},
 			},
-			ExitNode: []string{"tag:approve"},
+		},
+		{
+			name: "webauth-user",
+			pol: &policyv1.ACLPolicy{
+				ACLs: []policyv1.ACL{
+					{
+						Action:       "accept",
+						Sources:      []string{"*"},
+						Destinations: []string{"*:*"},
+					},
+				},
+				AutoApprovers: policyv1.AutoApprovers{
+					Routes: map[string][]string{
+						bigRoute.String(): {"user1@"},
+					},
+					ExitNode: []string{"user1@"},
+				},
+			},
+			approver: "user1@",
+			spec: ScenarioSpec{
+				NodesPerUser: 3,
+				Users:        []string{"user1", "user2"},
+				Networks: map[string][]string{
+					"usernet1": {"user1"},
+					"usernet2": {"user2"},
+				},
+				ExtraService: map[string][]extraServiceFunc{
+					"usernet1": {Webservice},
+				},
+				// We build the head image with curl and traceroute, so only use
+				// that for this test.
+				Versions: []string{"head"},
+			},
+			withURL: true,
+		},
+		{
+			name: "webauth-tag",
+			pol: &policyv1.ACLPolicy{
+				ACLs: []policyv1.ACL{
+					{
+						Action:       "accept",
+						Sources:      []string{"*"},
+						Destinations: []string{"*:*"},
+					},
+				},
+				TagOwners: map[string][]string{
+					"tag:approve": {"user1@"},
+				},
+				AutoApprovers: policyv1.AutoApprovers{
+					Routes: map[string][]string{
+						bigRoute.String(): {"tag:approve"},
+					},
+					ExitNode: []string{"tag:approve"},
+				},
+			},
+			approver: "tag:approve",
+			spec: ScenarioSpec{
+				NodesPerUser: 3,
+				Users:        []string{"user1", "user2"},
+				Networks: map[string][]string{
+					"usernet1": {"user1"},
+					"usernet2": {"user2"},
+				},
+				ExtraService: map[string][]extraServiceFunc{
+					"usernet1": {Webservice},
+				},
+				// We build the head image with curl and traceroute, so only use
+				// that for this test.
+				Versions: []string{"head"},
+			},
+			withURL: true,
+		},
+		{
+			name: "webauth-group",
+			pol: &policyv1.ACLPolicy{
+				ACLs: []policyv1.ACL{
+					{
+						Action:       "accept",
+						Sources:      []string{"*"},
+						Destinations: []string{"*:*"},
+					},
+				},
+				Groups: policyv1.Groups{
+					"group:approve": []string{"user1@"},
+				},
+				AutoApprovers: policyv1.AutoApprovers{
+					Routes: map[string][]string{
+						bigRoute.String(): {"group:approve"},
+					},
+					ExitNode: []string{"group:approve"},
+				},
+			},
+			approver: "group:approve",
+			spec: ScenarioSpec{
+				NodesPerUser: 3,
+				Users:        []string{"user1", "user2"},
+				Networks: map[string][]string{
+					"usernet1": {"user1"},
+					"usernet2": {"user2"},
+				},
+				ExtraService: map[string][]extraServiceFunc{
+					"usernet1": {Webservice},
+				},
+				// We build the head image with curl and traceroute, so only use
+				// that for this test.
+				Versions: []string{"head"},
+			},
+			withURL: true,
 		},
 	}
 
-	err = scenario.CreateHeadscaleEnv([]tsic.Option{
-		tsic.WithAcceptRoutes(),
-		tsic.WithTags([]string{"tag:approve"}),
-	},
-		hsic.WithTestName("clienableroute"),
-		hsic.WithEmbeddedDERPServerOnly(),
-		hsic.WithTLS(),
-		hsic.WithACLPolicy(pol),
-		hsic.WithPolicyMode(types.PolicyModeDB),
-	)
-	assertNoErrHeadscaleEnv(t, err)
-
-	allClients, err := scenario.ListTailscaleClients()
-	assertNoErrListClients(t, err)
-
-	err = scenario.WaitForTailscaleSync()
-	assertNoErrSync(t, err)
-
-	headscale, err := scenario.Headscale()
-	assertNoErrGetHeadscale(t, err)
-	assert.NotNil(t, headscale)
-
-	route, err := scenario.SubnetOfNetwork("usernet1")
-	require.NoError(t, err)
-
-	// Set the route of usernet1 to be autoapproved
-	pol.AutoApprovers.Routes[route.String()] = []string{"tag:approve"}
-	err = headscale.SetPolicy(pol)
-	require.NoError(t, err)
-
-	services, err := scenario.Services("usernet1")
-	require.NoError(t, err)
-	require.Len(t, services, 1)
-
-	usernet1, err := scenario.Network("usernet1")
-	require.NoError(t, err)
-
-	web := services[0]
-	webip := netip.MustParseAddr(web.GetIPInNetwork(usernet1))
-	weburl := fmt.Sprintf("http://%s/etc/hostname", webip)
-	t.Logf("webservice: %s, %s", webip.String(), weburl)
-
-	// Sort nodes by ID
-	sort.SliceStable(allClients, func(i, j int) bool {
-		statusI := allClients[i].MustStatus()
-		statusJ := allClients[j].MustStatus()
-
-		return statusI.Self.ID < statusJ.Self.ID
-	})
-
-	// This is ok because the scenario makes users in order, so the three first
-	// nodes, which are subnet routes, will be created first, and the last user
-	// will be created with the second.
-	routerUsernet1 := allClients[0]
-	routerSubRoute := allClients[1]
-	routerExitNode := allClients[2]
-
-	client := allClients[3]
-
-	// Advertise the route for the dockersubnet of user1
-	command := []string{
-		"tailscale",
-		"set",
-		"--advertise-routes=" + route.String(),
-	}
-	_, _, err = routerUsernet1.Execute(command)
-	require.NoErrorf(t, err, "failed to advertise route: %s", err)
-
-	time.Sleep(5 * time.Second)
-
-	// These route should auto approve, so the node is expected to have a route
-	// for all counts.
-	nodes, err := headscale.ListNodes()
-	require.NoError(t, err)
-	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
-
-	// Verify that the routes have been sent to the client.
-	status, err := client.Status()
-	require.NoError(t, err)
-
-	for _, peerKey := range status.Peers() {
-		peerStatus := status.Peer[peerKey]
-
-		if peerStatus.ID == "1" {
-			assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
-			requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
-		} else {
-			requirePeerSubnetRoutes(t, peerStatus, nil)
-		}
-	}
-
-	url := fmt.Sprintf("http://%s/etc/hostname", webip)
-	t.Logf("url from %s to %s", client.Hostname(), url)
-
-	result, err := client.Curl(url)
-	require.NoError(t, err)
-	assert.Len(t, result, 13)
-
-	tr, err := client.Traceroute(webip)
-	require.NoError(t, err)
-	assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4())
-
-	// Remove the auto approval from the policy, any routes already enabled should be allowed.
-	delete(pol.AutoApprovers.Routes, route.String())
-	err = headscale.SetPolicy(pol)
-	require.NoError(t, err)
-
-	time.Sleep(5 * time.Second)
-
-	// These route should auto approve, so the node is expected to have a route
-	// for all counts.
-	nodes, err = headscale.ListNodes()
-	require.NoError(t, err)
-	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
-
-	// Verify that the routes have been sent to the client.
-	status, err = client.Status()
-	require.NoError(t, err)
-
-	for _, peerKey := range status.Peers() {
-		peerStatus := status.Peer[peerKey]
-
-		if peerStatus.ID == "1" {
-			assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
-			requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
-		} else {
-			requirePeerSubnetRoutes(t, peerStatus, nil)
-		}
-	}
-
-	url = fmt.Sprintf("http://%s/etc/hostname", webip)
-	t.Logf("url from %s to %s", client.Hostname(), url)
-
-	result, err = client.Curl(url)
-	require.NoError(t, err)
-	assert.Len(t, result, 13)
-
-	tr, err = client.Traceroute(webip)
-	require.NoError(t, err)
-	assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4())
-
-	// Disable the route, making it unavailable since it is no longer auto-approved
-	_, err = headscale.ApproveRoutes(
-		nodes[0].GetId(),
-		[]netip.Prefix{},
-	)
-	require.NoError(t, err)
-
-	time.Sleep(5 * time.Second)
-
-	// These route should auto approve, so the node is expected to have a route
-	// for all counts.
-	nodes, err = headscale.ListNodes()
-	require.NoError(t, err)
-	assertNodeRouteCount(t, nodes[0], 1, 0, 0)
-
-	// Verify that the routes have been sent to the client.
-	status, err = client.Status()
-	require.NoError(t, err)
-
-	for _, peerKey := range status.Peers() {
-		peerStatus := status.Peer[peerKey]
-		requirePeerSubnetRoutes(t, peerStatus, nil)
-	}
-
-	// Add the route back to the auto approver in the policy, the route should
-	// now become available again.
-	pol.AutoApprovers.Routes[route.String()] = []string{"tag:approve"}
-	err = headscale.SetPolicy(pol)
-	require.NoError(t, err)
-
-	time.Sleep(5 * time.Second)
-
-	// These route should auto approve, so the node is expected to have a route
-	// for all counts.
-	nodes, err = headscale.ListNodes()
-	require.NoError(t, err)
-	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
-
-	// Verify that the routes have been sent to the client.
-	status, err = client.Status()
-	require.NoError(t, err)
-
-	for _, peerKey := range status.Peers() {
-		peerStatus := status.Peer[peerKey]
-
-		if peerStatus.ID == "1" {
-			require.NotNil(t, peerStatus.PrimaryRoutes)
-			assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
-			requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
-		} else {
-			requirePeerSubnetRoutes(t, peerStatus, nil)
-		}
-	}
-
-	url = fmt.Sprintf("http://%s/etc/hostname", webip)
-	t.Logf("url from %s to %s", client.Hostname(), url)
-
-	result, err = client.Curl(url)
-	require.NoError(t, err)
-	assert.Len(t, result, 13)
-
-	tr, err = client.Traceroute(webip)
-	require.NoError(t, err)
-	assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4())
-
-	// Advertise and validate a subnet of an auto approved route, /24 inside the
-	// auto approved /16.
-	command = []string{
-		"tailscale",
-		"set",
-		"--advertise-routes=" + subRoute.String(),
-	}
-	_, _, err = routerSubRoute.Execute(command)
-	require.NoErrorf(t, err, "failed to advertise route: %s", err)
-
-	time.Sleep(5 * time.Second)
-
-	// These route should auto approve, so the node is expected to have a route
-	// for all counts.
-	nodes, err = headscale.ListNodes()
-	require.NoError(t, err)
-	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
-	assertNodeRouteCount(t, nodes[1], 1, 1, 1)
-
-	// Verify that the routes have been sent to the client.
-	status, err = client.Status()
-	require.NoError(t, err)
-
-	for _, peerKey := range status.Peers() {
-		peerStatus := status.Peer[peerKey]
-
-		if peerStatus.ID == "1" {
-			assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
-			requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
-		} else if peerStatus.ID == "2" {
-			assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), subRoute)
-			requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{subRoute})
-		} else {
-			requirePeerSubnetRoutes(t, peerStatus, nil)
-		}
-	}
-
-	// Advertise a not approved route will not end up anywhere
-	command = []string{
-		"tailscale",
-		"set",
-		"--advertise-routes=" + notApprovedRoute.String(),
-	}
-	_, _, err = routerSubRoute.Execute(command)
-	require.NoErrorf(t, err, "failed to advertise route: %s", err)
-
-	time.Sleep(5 * time.Second)
-
-	// These route should auto approve, so the node is expected to have a route
-	// for all counts.
-	nodes, err = headscale.ListNodes()
-	require.NoError(t, err)
-	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
-	assertNodeRouteCount(t, nodes[1], 1, 1, 0)
-	assertNodeRouteCount(t, nodes[2], 0, 0, 0)
-
-	// Verify that the routes have been sent to the client.
-	status, err = client.Status()
-	require.NoError(t, err)
-
-	for _, peerKey := range status.Peers() {
-		peerStatus := status.Peer[peerKey]
-
-		if peerStatus.ID == "1" {
-			assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
-			requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
-		} else {
-			requirePeerSubnetRoutes(t, peerStatus, nil)
-		}
-	}
-
-	// Exit routes are also automatically approved
-	command = []string{
-		"tailscale",
-		"set",
-		"--advertise-exit-node",
-	}
-	_, _, err = routerExitNode.Execute(command)
-	require.NoErrorf(t, err, "failed to advertise route: %s", err)
-
-	time.Sleep(5 * time.Second)
-
-	nodes, err = headscale.ListNodes()
-	require.NoError(t, err)
-	assertNodeRouteCount(t, nodes[0], 1, 1, 1)
-	assertNodeRouteCount(t, nodes[1], 1, 1, 0)
-	assertNodeRouteCount(t, nodes[2], 2, 2, 2)
-
-	// Verify that the routes have been sent to the client.
-	status, err = client.Status()
-	require.NoError(t, err)
-
-	for _, peerKey := range status.Peers() {
-		peerStatus := status.Peer[peerKey]
-
-		if peerStatus.ID == "1" {
-			assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
-			requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
-		} else if peerStatus.ID == "3" {
-			requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()})
-		} else {
-			requirePeerSubnetRoutes(t, peerStatus, nil)
+	for _, tt := range tests {
+		for _, dbMode := range []types.PolicyMode{types.PolicyModeDB, types.PolicyModeFile} {
+			for _, advertiseDuringUp := range []bool{false, true} {
+				name := fmt.Sprintf("%s-advertiseduringup-%t-pol-%s", tt.name, advertiseDuringUp, dbMode)
+				t.Run(name, func(t *testing.T) {
+					scenario, err := NewScenario(tt.spec)
+					require.NoErrorf(t, err, "failed to create scenario: %s", err)
+					defer scenario.ShutdownAssertNoPanics(t)
+
+					opts := []hsic.Option{
+						hsic.WithTestName("autoapprovemulti"),
+						hsic.WithEmbeddedDERPServerOnly(),
+						hsic.WithTLS(),
+						hsic.WithACLPolicy(tt.pol),
+						hsic.WithPolicyMode(dbMode),
+					}
+
+					tsOpts := []tsic.Option{
+						tsic.WithAcceptRoutes(),
+					}
+
+					if tt.approver == "tag:approve" {
+						tsOpts = append(tsOpts,
+							tsic.WithTags([]string{"tag:approve"}),
+						)
+					}
+
+					route, err := scenario.SubnetOfNetwork("usernet1")
+					require.NoError(t, err)
+
+					err = scenario.createHeadscaleEnv(tt.withURL, tsOpts,
+						opts...,
+					)
+					assertNoErrHeadscaleEnv(t, err)
+
+					allClients, err := scenario.ListTailscaleClients()
+					assertNoErrListClients(t, err)
+
+					err = scenario.WaitForTailscaleSync()
+					assertNoErrSync(t, err)
+
+					services, err := scenario.Services("usernet1")
+					require.NoError(t, err)
+					require.Len(t, services, 1)
+
+					usernet1, err := scenario.Network("usernet1")
+					require.NoError(t, err)
+
+					headscale, err := scenario.Headscale()
+					assertNoErrGetHeadscale(t, err)
+					assert.NotNil(t, headscale)
+
+					if advertiseDuringUp {
+						tsOpts = append(tsOpts,
+							tsic.WithExtraLoginArgs([]string{"--advertise-routes=" + route.String()}),
+						)
+					}
+
+					tsOpts = append(tsOpts, tsic.WithNetwork(usernet1))
+
+					// This whole dance is to add a node _after_ all the other nodes
+					// with an additional tsOpt which advertises the route as part
+					// of the `tailscale up` command. If we do this as part of the
+					// scenario creation, it will be added to all nodes and turn
+					// into a HA node, which isnt something we are testing here.
+					routerUsernet1, err := scenario.CreateTailscaleNode("head", tsOpts...)
+					require.NoError(t, err)
+					defer routerUsernet1.Shutdown()
+
+					if tt.withURL {
+						u, err := routerUsernet1.LoginWithURL(headscale.GetEndpoint())
+						assertNoErr(t, err)
+
+						body, err := doLoginURL(routerUsernet1.Hostname(), u)
+						assertNoErr(t, err)
+
+						scenario.runHeadscaleRegister("user1", body)
+					} else {
+						userMap, err := headscale.MapUsers()
+						assertNoErr(t, err)
+
+						pak, err := scenario.CreatePreAuthKey(userMap["user1"].GetId(), false, false)
+						assertNoErr(t, err)
+
+						err = routerUsernet1.Login(headscale.GetEndpoint(), pak.Key)
+						assertNoErr(t, err)
+					}
+					// extra creation end.
+
+					// Set the route of usernet1 to be autoapproved
+					tt.pol.AutoApprovers.Routes[route.String()] = []string{tt.approver}
+					err = headscale.SetPolicy(tt.pol)
+					require.NoError(t, err)
+
+					routerUsernet1ID := routerUsernet1.MustID()
+
+					web := services[0]
+					webip := netip.MustParseAddr(web.GetIPInNetwork(usernet1))
+					weburl := fmt.Sprintf("http://%s/etc/hostname", webip)
+					t.Logf("webservice: %s, %s", webip.String(), weburl)
+
+					// Sort nodes by ID
+					sort.SliceStable(allClients, func(i, j int) bool {
+						statusI := allClients[i].MustStatus()
+						statusJ := allClients[j].MustStatus()
+
+						return statusI.Self.ID < statusJ.Self.ID
+					})
+
+					// This is ok because the scenario makes users in order, so the three first
+					// nodes, which are subnet routes, will be created first, and the last user
+					// will be created with the second.
+					routerSubRoute := allClients[1]
+					routerExitNode := allClients[2]
+
+					client := allClients[3]
+
+					if !advertiseDuringUp {
+						// Advertise the route for the dockersubnet of user1
+						command := []string{
+							"tailscale",
+							"set",
+							"--advertise-routes=" + route.String(),
+						}
+						_, _, err = routerUsernet1.Execute(command)
+						require.NoErrorf(t, err, "failed to advertise route: %s", err)
+					}
+
+					time.Sleep(5 * time.Second)
+
+					// These route should auto approve, so the node is expected to have a route
+					// for all counts.
+					nodes, err := headscale.ListNodes()
+					require.NoError(t, err)
+					requireNodeRouteCount(t, MustFindNode(routerUsernet1.Hostname(), nodes), 1, 1, 1)
+
+					// Verify that the routes have been sent to the client.
+					status, err := client.Status()
+					require.NoError(t, err)
+
+					for _, peerKey := range status.Peers() {
+						peerStatus := status.Peer[peerKey]
+
+						if peerStatus.ID == routerUsernet1ID.StableID() {
+							assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
+							requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
+						} else {
+							requirePeerSubnetRoutes(t, peerStatus, nil)
+						}
+					}
+
+					url := fmt.Sprintf("http://%s/etc/hostname", webip)
+					t.Logf("url from %s to %s", client.Hostname(), url)
+
+					result, err := client.Curl(url)
+					require.NoError(t, err)
+					assert.Len(t, result, 13)
+
+					tr, err := client.Traceroute(webip)
+					require.NoError(t, err)
+					assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4())
+
+					// Remove the auto approval from the policy, any routes already enabled should be allowed.
+					delete(tt.pol.AutoApprovers.Routes, route.String())
+					err = headscale.SetPolicy(tt.pol)
+					require.NoError(t, err)
+
+					time.Sleep(5 * time.Second)
+
+					// These route should auto approve, so the node is expected to have a route
+					// for all counts.
+					nodes, err = headscale.ListNodes()
+					require.NoError(t, err)
+					requireNodeRouteCount(t, MustFindNode(routerUsernet1.Hostname(), nodes), 1, 1, 1)
+
+					// Verify that the routes have been sent to the client.
+					status, err = client.Status()
+					require.NoError(t, err)
+
+					for _, peerKey := range status.Peers() {
+						peerStatus := status.Peer[peerKey]
+
+						if peerStatus.ID == routerUsernet1ID.StableID() {
+							assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
+							requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
+						} else {
+							requirePeerSubnetRoutes(t, peerStatus, nil)
+						}
+					}
+
+					url = fmt.Sprintf("http://%s/etc/hostname", webip)
+					t.Logf("url from %s to %s", client.Hostname(), url)
+
+					result, err = client.Curl(url)
+					require.NoError(t, err)
+					assert.Len(t, result, 13)
+
+					tr, err = client.Traceroute(webip)
+					require.NoError(t, err)
+					assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4())
+
+					// Disable the route, making it unavailable since it is no longer auto-approved
+					_, err = headscale.ApproveRoutes(
+						MustFindNode(routerUsernet1.Hostname(), nodes).GetId(),
+						[]netip.Prefix{},
+					)
+					require.NoError(t, err)
+
+					time.Sleep(5 * time.Second)
+
+					// These route should auto approve, so the node is expected to have a route
+					// for all counts.
+					nodes, err = headscale.ListNodes()
+					require.NoError(t, err)
+					requireNodeRouteCount(t, MustFindNode(routerUsernet1.Hostname(), nodes), 1, 0, 0)
+
+					// Verify that the routes have been sent to the client.
+					status, err = client.Status()
+					require.NoError(t, err)
+
+					for _, peerKey := range status.Peers() {
+						peerStatus := status.Peer[peerKey]
+						requirePeerSubnetRoutes(t, peerStatus, nil)
+					}
+
+					// Add the route back to the auto approver in the policy, the route should
+					// now become available again.
+					tt.pol.AutoApprovers.Routes[route.String()] = []string{tt.approver}
+					err = headscale.SetPolicy(tt.pol)
+					require.NoError(t, err)
+
+					time.Sleep(5 * time.Second)
+
+					// These route should auto approve, so the node is expected to have a route
+					// for all counts.
+					nodes, err = headscale.ListNodes()
+					require.NoError(t, err)
+					requireNodeRouteCount(t, MustFindNode(routerUsernet1.Hostname(), nodes), 1, 1, 1)
+
+					// Verify that the routes have been sent to the client.
+					status, err = client.Status()
+					require.NoError(t, err)
+
+					for _, peerKey := range status.Peers() {
+						peerStatus := status.Peer[peerKey]
+
+						if peerStatus.ID == routerUsernet1ID.StableID() {
+							require.NotNil(t, peerStatus.PrimaryRoutes)
+							assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
+							requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
+						} else {
+							requirePeerSubnetRoutes(t, peerStatus, nil)
+						}
+					}
+
+					url = fmt.Sprintf("http://%s/etc/hostname", webip)
+					t.Logf("url from %s to %s", client.Hostname(), url)
+
+					result, err = client.Curl(url)
+					require.NoError(t, err)
+					assert.Len(t, result, 13)
+
+					tr, err = client.Traceroute(webip)
+					require.NoError(t, err)
+					assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4())
+
+					// Advertise and validate a subnet of an auto approved route, /24 inside the
+					// auto approved /16.
+					command := []string{
+						"tailscale",
+						"set",
+						"--advertise-routes=" + subRoute.String(),
+					}
+					_, _, err = routerSubRoute.Execute(command)
+					require.NoErrorf(t, err, "failed to advertise route: %s", err)
+
+					time.Sleep(5 * time.Second)
+
+					// These route should auto approve, so the node is expected to have a route
+					// for all counts.
+					nodes, err = headscale.ListNodes()
+					require.NoError(t, err)
+					requireNodeRouteCount(t, MustFindNode(routerUsernet1.Hostname(), nodes), 1, 1, 1)
+					requireNodeRouteCount(t, nodes[1], 1, 1, 1)
+
+					// Verify that the routes have been sent to the client.
+					status, err = client.Status()
+					require.NoError(t, err)
+
+					for _, peerKey := range status.Peers() {
+						peerStatus := status.Peer[peerKey]
+
+						if peerStatus.ID == routerUsernet1ID.StableID() {
+							assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
+							requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
+						} else if peerStatus.ID == "2" {
+							assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), subRoute)
+							requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{subRoute})
+						} else {
+							requirePeerSubnetRoutes(t, peerStatus, nil)
+						}
+					}
+
+					// Advertise a not approved route will not end up anywhere
+					command = []string{
+						"tailscale",
+						"set",
+						"--advertise-routes=" + notApprovedRoute.String(),
+					}
+					_, _, err = routerSubRoute.Execute(command)
+					require.NoErrorf(t, err, "failed to advertise route: %s", err)
+
+					time.Sleep(5 * time.Second)
+
+					// These route should auto approve, so the node is expected to have a route
+					// for all counts.
+					nodes, err = headscale.ListNodes()
+					require.NoError(t, err)
+					requireNodeRouteCount(t, MustFindNode(routerUsernet1.Hostname(), nodes), 1, 1, 1)
+					requireNodeRouteCount(t, nodes[1], 1, 1, 0)
+					requireNodeRouteCount(t, nodes[2], 0, 0, 0)
+
+					// Verify that the routes have been sent to the client.
+					status, err = client.Status()
+					require.NoError(t, err)
+
+					for _, peerKey := range status.Peers() {
+						peerStatus := status.Peer[peerKey]
+
+						if peerStatus.ID == routerUsernet1ID.StableID() {
+							assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
+							requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
+						} else {
+							requirePeerSubnetRoutes(t, peerStatus, nil)
+						}
+					}
+
+					// Exit routes are also automatically approved
+					command = []string{
+						"tailscale",
+						"set",
+						"--advertise-exit-node",
+					}
+					_, _, err = routerExitNode.Execute(command)
+					require.NoErrorf(t, err, "failed to advertise route: %s", err)
+
+					time.Sleep(5 * time.Second)
+
+					nodes, err = headscale.ListNodes()
+					require.NoError(t, err)
+					requireNodeRouteCount(t, MustFindNode(routerUsernet1.Hostname(), nodes), 1, 1, 1)
+					requireNodeRouteCount(t, nodes[1], 1, 1, 0)
+					requireNodeRouteCount(t, nodes[2], 2, 2, 2)
+
+					// Verify that the routes have been sent to the client.
+					status, err = client.Status()
+					require.NoError(t, err)
+
+					for _, peerKey := range status.Peers() {
+						peerStatus := status.Peer[peerKey]
+
+						if peerStatus.ID == routerUsernet1ID.StableID() {
+							assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *route)
+							requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*route})
+						} else if peerStatus.ID == "3" {
+							requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()})
+						} else {
+							requirePeerSubnetRoutes(t, peerStatus, nil)
+						}
+					}
+				})
+			}
 		}
 	}
 }
@@ -1757,9 +2012,9 @@ func requirePeerSubnetRoutes(t *testing.T, status *ipnstate.PeerStatus, expected
 	}
 }
 
-func assertNodeRouteCount(t *testing.T, node *v1.Node, announced, approved, subnet int) {
+func requireNodeRouteCount(t *testing.T, node *v1.Node, announced, approved, subnet int) {
 	t.Helper()
-	assert.Len(t, node.GetAvailableRoutes(), announced)
-	assert.Len(t, node.GetApprovedRoutes(), approved)
-	assert.Len(t, node.GetSubnetRoutes(), subnet)
+	require.Lenf(t, node.GetAvailableRoutes(), announced, "expected %q announced routes(%v) to have %d route, had %d", node.GetName(), node.GetAvailableRoutes(), announced, len(node.GetAvailableRoutes()))
+	require.Lenf(t, node.GetApprovedRoutes(), approved, "expected %q approved routes(%v) to have %d route, had %d", node.GetName(), node.GetApprovedRoutes(), approved, len(node.GetApprovedRoutes()))
+	require.Lenf(t, node.GetSubnetRoutes(), subnet, "expected %q subnet routes(%v) to have %d route, had %d", node.GetName(), node.GetSubnetRoutes(), subnet, len(node.GetSubnetRoutes()))
 }
diff --git a/integration/scenario_test.go b/integration/scenario_test.go
index 7f34fa77..ac0ff238 100644
--- a/integration/scenario_test.go
+++ b/integration/scenario_test.go
@@ -51,7 +51,7 @@ func TestHeadscale(t *testing.T) {
 	})
 
 	t.Run("create-user", func(t *testing.T) {
-		err := scenario.CreateUser(user)
+		_, err := scenario.CreateUser(user)
 		if err != nil {
 			t.Fatalf("failed to create user: %s", err)
 		}
@@ -62,7 +62,7 @@ func TestHeadscale(t *testing.T) {
 	})
 
 	t.Run("create-auth-key", func(t *testing.T) {
-		_, err := scenario.CreatePreAuthKey(user, true, false)
+		_, err := scenario.CreatePreAuthKey(1, true, false)
 		if err != nil {
 			t.Fatalf("failed to create preauthkey: %s", err)
 		}
@@ -100,7 +100,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
 	})
 
 	t.Run("create-user", func(t *testing.T) {
-		err := scenario.CreateUser(user)
+		_, err := scenario.CreateUser(user)
 		if err != nil {
 			t.Fatalf("failed to create user: %s", err)
 		}
@@ -111,7 +111,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
 	})
 
 	t.Run("create-tailscale", func(t *testing.T) {
-		err := scenario.CreateTailscaleNodesInUser(user, "unstable", count, tsic.WithNetwork(scenario.networks[TestDefaultNetwork]))
+		err := scenario.CreateTailscaleNodesInUser(user, "unstable", count, tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]))
 		if err != nil {
 			t.Fatalf("failed to add tailscale nodes: %s", err)
 		}
@@ -122,7 +122,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
 	})
 
 	t.Run("join-headscale", func(t *testing.T) {
-		key, err := scenario.CreatePreAuthKey(user, true, false)
+		key, err := scenario.CreatePreAuthKey(1, true, false)
 		if err != nil {
 			t.Fatalf("failed to create preauthkey: %s", err)
 		}
diff --git a/integration/ssh_test.go b/integration/ssh_test.go
index 20aefdfd..f6e0e66d 100644
--- a/integration/ssh_test.go
+++ b/integration/ssh_test.go
@@ -410,7 +410,7 @@ func assertSSHHostname(t *testing.T, client TailscaleClient, peer TailscaleClien
 	result, _, err := doSSH(t, client, peer)
 	assertNoErr(t, err)
 
-	assertContains(t, peer.ID(), strings.ReplaceAll(result, "\n", ""))
+	assertContains(t, peer.ContainerID(), strings.ReplaceAll(result, "\n", ""))
 }
 
 func assertSSHPermissionDenied(t *testing.T, client TailscaleClient, peer TailscaleClient) {