Browse Source

util/syspolicy/*: move syspolicy keys to new const leaf "pkey" package

This is step 1 of ~3, breaking up #14720 into reviewable chunks, with
the aim to make syspolicy be a build-time configurable feature.

In this first (very noisy) step, all the syspolicy string key
constants move to a new constant-only (code-free) package. This will
make future steps more reviewable, without this movement noise.

There are no code or behavior changes here.

The future steps of this series can be seen in #14720: removing global
funcs from syspolicy resolution and using an interface that's plumbed
around instead. Then adding build tags.

Updates #12614

Change-Id: If73bf2c28b9c9b1a408fe868b0b6a25b03eeabd1
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick 6 months ago
parent
commit
cc532efc20
48 changed files with 601 additions and 554 deletions
  1. 1 0
      cmd/derper/depaware.txt
  2. 1 0
      cmd/k8s-operator/depaware.txt
  3. 1 0
      cmd/tailscale/depaware.txt
  4. 1 0
      cmd/tailscaled/depaware.txt
  5. 2 1
      cmd/tailscaled/tailscaled.go
  6. 3 3
      cmd/tailscaled/tailscaled_windows.go
  7. 1 0
      cmd/tsidp/depaware.txt
  8. 2 1
      control/controlclient/direct.go
  9. 2 1
      control/controlclient/sign_supported.go
  10. 2 1
      ipn/desktop/extension.go
  11. 3 2
      ipn/ipnauth/policy.go
  12. 2 1
      ipn/ipnlocal/c2n.go
  13. 27 26
      ipn/ipnlocal/local.go
  14. 59 58
      ipn/ipnlocal/local_test.go
  15. 2 1
      ipn/prefs.go
  16. 2 1
      logpolicy/logpolicy.go
  17. 3 2
      net/dns/manager_windows.go
  18. 2 1
      posture/serialnumber_syspolicy.go
  19. 1 0
      tsnet/depaware.txt
  20. 1 0
      tstest/integration/tailscaled_deps_test_darwin.go
  21. 1 0
      tstest/integration/tailscaled_deps_test_freebsd.go
  22. 1 0
      tstest/integration/tailscaled_deps_test_linux.go
  23. 1 0
      tstest/integration/tailscaled_deps_test_openbsd.go
  24. 1 0
      tstest/integration/tailscaled_deps_test_windows.go
  25. 5 4
      util/syspolicy/handler.go
  26. 4 3
      util/syspolicy/internal/metrics/metrics.go
  27. 2 1
      util/syspolicy/internal/metrics/metrics_test.go
  28. 177 0
      util/syspolicy/pkey/pkey.go
  29. 44 209
      util/syspolicy/policy_keys.go
  30. 6 1
      util/syspolicy/policy_keys_test.go
  31. 4 3
      util/syspolicy/rsop/change_callbacks.go
  32. 30 29
      util/syspolicy/rsop/resultant_policy_test.go
  33. 0 13
      util/syspolicy/setting/key.go
  34. 2 1
      util/syspolicy/setting/raw_item.go
  35. 7 6
      util/syspolicy/setting/setting.go
  36. 4 3
      util/syspolicy/setting/setting_test.go
  37. 13 12
      util/syspolicy/setting/snapshot.go
  38. 73 72
      util/syspolicy/setting/snapshot_test.go
  39. 8 7
      util/syspolicy/source/env_policy_store.go
  40. 3 2
      util/syspolicy/source/env_policy_store_test.go
  41. 3 2
      util/syspolicy/source/policy_reader.go
  42. 5 4
      util/syspolicy/source/policy_reader_test.go
  43. 5 4
      util/syspolicy/source/policy_source.go
  44. 15 14
      util/syspolicy/source/policy_store_windows.go
  45. 4 3
      util/syspolicy/source/policy_store_windows_test.go
  46. 15 14
      util/syspolicy/source/test_store.go
  47. 11 10
      util/syspolicy/syspolicy.go
  48. 39 38
      util/syspolicy/syspolicy_test.go

+ 1 - 0
cmd/derper/depaware.txt

@@ -174,6 +174,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
         tailscale.com/util/syspolicy/internal                        from tailscale.com/util/syspolicy/setting+
         tailscale.com/util/syspolicy/internal                        from tailscale.com/util/syspolicy/setting+
         tailscale.com/util/syspolicy/internal/loggerx                from tailscale.com/util/syspolicy/internal/metrics+
         tailscale.com/util/syspolicy/internal/loggerx                from tailscale.com/util/syspolicy/internal/metrics+
         tailscale.com/util/syspolicy/internal/metrics                from tailscale.com/util/syspolicy/source
         tailscale.com/util/syspolicy/internal/metrics                from tailscale.com/util/syspolicy/source
+        tailscale.com/util/syspolicy/pkey                            from tailscale.com/ipn+
         tailscale.com/util/syspolicy/rsop                            from tailscale.com/util/syspolicy
         tailscale.com/util/syspolicy/rsop                            from tailscale.com/util/syspolicy
         tailscale.com/util/syspolicy/setting                         from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/setting                         from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/source                          from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/source                          from tailscale.com/util/syspolicy+

+ 1 - 0
cmd/k8s-operator/depaware.txt

@@ -955,6 +955,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
         tailscale.com/util/syspolicy/internal                        from tailscale.com/util/syspolicy/setting+
         tailscale.com/util/syspolicy/internal                        from tailscale.com/util/syspolicy/setting+
         tailscale.com/util/syspolicy/internal/loggerx                from tailscale.com/util/syspolicy/internal/metrics+
         tailscale.com/util/syspolicy/internal/loggerx                from tailscale.com/util/syspolicy/internal/metrics+
         tailscale.com/util/syspolicy/internal/metrics                from tailscale.com/util/syspolicy/source
         tailscale.com/util/syspolicy/internal/metrics                from tailscale.com/util/syspolicy/source
+        tailscale.com/util/syspolicy/pkey                            from tailscale.com/control/controlclient+
         tailscale.com/util/syspolicy/rsop                            from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/rsop                            from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/setting                         from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/setting                         from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/source                          from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/source                          from tailscale.com/util/syspolicy+

+ 1 - 0
cmd/tailscale/depaware.txt

@@ -195,6 +195,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
         tailscale.com/util/syspolicy/internal                        from tailscale.com/util/syspolicy/setting+
         tailscale.com/util/syspolicy/internal                        from tailscale.com/util/syspolicy/setting+
         tailscale.com/util/syspolicy/internal/loggerx                from tailscale.com/util/syspolicy/internal/metrics+
         tailscale.com/util/syspolicy/internal/loggerx                from tailscale.com/util/syspolicy/internal/metrics+
         tailscale.com/util/syspolicy/internal/metrics                from tailscale.com/util/syspolicy/source
         tailscale.com/util/syspolicy/internal/metrics                from tailscale.com/util/syspolicy/source
+        tailscale.com/util/syspolicy/pkey                            from tailscale.com/ipn+
         tailscale.com/util/syspolicy/rsop                            from tailscale.com/util/syspolicy
         tailscale.com/util/syspolicy/rsop                            from tailscale.com/util/syspolicy
         tailscale.com/util/syspolicy/setting                         from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/setting                         from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/source                          from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/source                          from tailscale.com/util/syspolicy+

+ 1 - 0
cmd/tailscaled/depaware.txt

@@ -432,6 +432,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         tailscale.com/util/syspolicy/internal                        from tailscale.com/util/syspolicy/setting+
         tailscale.com/util/syspolicy/internal                        from tailscale.com/util/syspolicy/setting+
         tailscale.com/util/syspolicy/internal/loggerx                from tailscale.com/util/syspolicy/internal/metrics+
         tailscale.com/util/syspolicy/internal/loggerx                from tailscale.com/util/syspolicy/internal/metrics+
         tailscale.com/util/syspolicy/internal/metrics                from tailscale.com/util/syspolicy/source
         tailscale.com/util/syspolicy/internal/metrics                from tailscale.com/util/syspolicy/source
+        tailscale.com/util/syspolicy/pkey                            from tailscale.com/cmd/tailscaled+
         tailscale.com/util/syspolicy/rsop                            from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/rsop                            from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/setting                         from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/setting                         from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/source                          from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/source                          from tailscale.com/util/syspolicy+

+ 2 - 1
cmd/tailscaled/tailscaled.go

@@ -65,6 +65,7 @@ import (
 	"tailscale.com/util/multierr"
 	"tailscale.com/util/multierr"
 	"tailscale.com/util/osshare"
 	"tailscale.com/util/osshare"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/version"
 	"tailscale.com/version"
 	"tailscale.com/version/distro"
 	"tailscale.com/version/distro"
 	"tailscale.com/wgengine"
 	"tailscale.com/wgengine"
@@ -1011,6 +1012,6 @@ func defaultEncryptState() bool {
 		// (plan9/FreeBSD/etc).
 		// (plan9/FreeBSD/etc).
 		return false
 		return false
 	}
 	}
-	v, _ := syspolicy.GetBoolean(syspolicy.EncryptState, false)
+	v, _ := syspolicy.GetBoolean(pkey.EncryptState, false)
 	return v
 	return v
 }
 }

+ 3 - 3
cmd/tailscaled/tailscaled_windows.go

@@ -56,6 +56,7 @@ import (
 	"tailscale.com/types/logid"
 	"tailscale.com/types/logid"
 	"tailscale.com/util/osdiag"
 	"tailscale.com/util/osdiag"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/winutil"
 	"tailscale.com/util/winutil"
 	"tailscale.com/util/winutil/gp"
 	"tailscale.com/util/winutil/gp"
 	"tailscale.com/version"
 	"tailscale.com/version"
@@ -155,7 +156,7 @@ func runWindowsService(pol *logpolicy.Policy) error {
 
 
 	if syslog, err := eventlog.Open(serviceName); err == nil {
 	if syslog, err := eventlog.Open(serviceName); err == nil {
 		syslogf = func(format string, args ...any) {
 		syslogf = func(format string, args ...any) {
-			if logSCMInteractions, _ := syspolicy.GetBoolean(syspolicy.LogSCMInteractions, false); logSCMInteractions {
+			if logSCMInteractions, _ := syspolicy.GetBoolean(pkey.LogSCMInteractions, false); logSCMInteractions {
 				syslog.Info(0, fmt.Sprintf(format, args...))
 				syslog.Info(0, fmt.Sprintf(format, args...))
 			}
 			}
 		}
 		}
@@ -389,8 +390,7 @@ func handleSessionChange(chgRequest svc.ChangeRequest) {
 	if chgRequest.Cmd != svc.SessionChange || chgRequest.EventType != windows.WTS_SESSION_UNLOCK {
 	if chgRequest.Cmd != svc.SessionChange || chgRequest.EventType != windows.WTS_SESSION_UNLOCK {
 		return
 		return
 	}
 	}
-
-	if flushDNSOnSessionUnlock, _ := syspolicy.GetBoolean(syspolicy.FlushDNSOnSessionUnlock, false); flushDNSOnSessionUnlock {
+	if flushDNSOnSessionUnlock, _ := syspolicy.GetBoolean(pkey.FlushDNSOnSessionUnlock, false); flushDNSOnSessionUnlock {
 		log.Printf("Received WTS_SESSION_UNLOCK event, initiating DNS flush.")
 		log.Printf("Received WTS_SESSION_UNLOCK event, initiating DNS flush.")
 		go func() {
 		go func() {
 			err := dns.Flush()
 			err := dns.Flush()

+ 1 - 0
cmd/tsidp/depaware.txt

@@ -384,6 +384,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
         tailscale.com/util/syspolicy/internal                        from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/internal                        from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/internal/loggerx                from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/internal/loggerx                from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/internal/metrics                from tailscale.com/util/syspolicy/source
         tailscale.com/util/syspolicy/internal/metrics                from tailscale.com/util/syspolicy/source
+        tailscale.com/util/syspolicy/pkey                            from tailscale.com/control/controlclient+
         tailscale.com/util/syspolicy/rsop                            from tailscale.com/ipn/ipnlocal+
         tailscale.com/util/syspolicy/rsop                            from tailscale.com/ipn/ipnlocal+
         tailscale.com/util/syspolicy/setting                         from tailscale.com/client/local+
         tailscale.com/util/syspolicy/setting                         from tailscale.com/client/local+
         tailscale.com/util/syspolicy/source                          from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/source                          from tailscale.com/util/syspolicy+

+ 2 - 1
control/controlclient/direct.go

@@ -54,6 +54,7 @@ import (
 	"tailscale.com/util/multierr"
 	"tailscale.com/util/multierr"
 	"tailscale.com/util/singleflight"
 	"tailscale.com/util/singleflight"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/systemd"
 	"tailscale.com/util/systemd"
 	"tailscale.com/util/testenv"
 	"tailscale.com/util/testenv"
 	"tailscale.com/util/zstdframe"
 	"tailscale.com/util/zstdframe"
@@ -616,7 +617,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
 		return regen, opt.URL, nil, err
 		return regen, opt.URL, nil, err
 	}
 	}
 
 
-	tailnet, err := syspolicy.GetString(syspolicy.Tailnet, "")
+	tailnet, err := syspolicy.GetString(pkey.Tailnet, "")
 	if err != nil {
 	if err != nil {
 		c.logf("unable to provide Tailnet field in register request. err: %v", err)
 		c.logf("unable to provide Tailnet field in register request. err: %v", err)
 	}
 	}

+ 2 - 1
control/controlclient/sign_supported.go

@@ -19,6 +19,7 @@ import (
 	"tailscale.com/tailcfg"
 	"tailscale.com/tailcfg"
 	"tailscale.com/types/key"
 	"tailscale.com/types/key"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 )
 )
 
 
 // getMachineCertificateSubject returns the exact name of a Subject that needs
 // getMachineCertificateSubject returns the exact name of a Subject that needs
@@ -31,7 +32,7 @@ import (
 //
 //
 // Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
 // Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
 func getMachineCertificateSubject() string {
 func getMachineCertificateSubject() string {
-	machineCertSubject, _ := syspolicy.GetString(syspolicy.MachineCertificateSubject, "")
+	machineCertSubject, _ := syspolicy.GetString(pkey.MachineCertificateSubject, "")
 	return machineCertSubject
 	return machineCertSubject
 }
 }
 
 

+ 2 - 1
ipn/desktop/extension.go

@@ -19,6 +19,7 @@ import (
 	"tailscale.com/ipn/ipnext"
 	"tailscale.com/ipn/ipnext"
 	"tailscale.com/types/logger"
 	"tailscale.com/types/logger"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 )
 )
 
 
 // featureName is the name of the feature implemented by this package.
 // featureName is the name of the feature implemented by this package.
@@ -135,7 +136,7 @@ func (e *desktopSessionsExt) getBackgroundProfile(profiles ipnext.ProfileStore)
 	e.mu.Lock()
 	e.mu.Lock()
 	defer e.mu.Unlock()
 	defer e.mu.Unlock()
 
 
-	if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); !alwaysOn {
+	if alwaysOn, _ := syspolicy.GetBoolean(pkey.AlwaysOn, false); !alwaysOn {
 		// If the Always-On mode is disabled, there's no background profile
 		// If the Always-On mode is disabled, there's no background profile
 		// as far as the desktop session extension is concerned.
 		// as far as the desktop session extension is concerned.
 		return ipn.LoginProfileView{}
 		return ipn.LoginProfileView{}

+ 3 - 2
ipn/ipnauth/policy.go

@@ -11,6 +11,7 @@ import (
 	"tailscale.com/ipn"
 	"tailscale.com/ipn"
 	"tailscale.com/tailcfg"
 	"tailscale.com/tailcfg"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 )
 )
 
 
 type actorWithPolicyChecks struct{ Actor }
 type actorWithPolicyChecks struct{ Actor }
@@ -50,10 +51,10 @@ func (a actorWithPolicyChecks) CheckProfileAccess(profile ipn.LoginProfileView,
 // TODO(nickkhyl): unexport it when we move [ipn.Actor] implementations from [ipnserver]
 // TODO(nickkhyl): unexport it when we move [ipn.Actor] implementations from [ipnserver]
 // and corp to this package.
 // and corp to this package.
 func CheckDisconnectPolicy(actor Actor, profile ipn.LoginProfileView, reason string, auditFn AuditLogFunc) error {
 func CheckDisconnectPolicy(actor Actor, profile ipn.LoginProfileView, reason string, auditFn AuditLogFunc) error {
-	if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); !alwaysOn {
+	if alwaysOn, _ := syspolicy.GetBoolean(pkey.AlwaysOn, false); !alwaysOn {
 		return nil
 		return nil
 	}
 	}
-	if allowWithReason, _ := syspolicy.GetBoolean(syspolicy.AlwaysOnOverrideWithReason, false); !allowWithReason {
+	if allowWithReason, _ := syspolicy.GetBoolean(pkey.AlwaysOnOverrideWithReason, false); !allowWithReason {
 		return errors.New("disconnect not allowed: always-on mode is enabled")
 		return errors.New("disconnect not allowed: always-on mode is enabled")
 	}
 	}
 	if reason == "" {
 	if reason == "" {

+ 2 - 1
ipn/ipnlocal/c2n.go

@@ -30,6 +30,7 @@ import (
 	"tailscale.com/util/goroutines"
 	"tailscale.com/util/goroutines"
 	"tailscale.com/util/set"
 	"tailscale.com/util/set"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/version"
 	"tailscale.com/version"
 	"tailscale.com/version/distro"
 	"tailscale.com/version/distro"
 )
 )
@@ -342,7 +343,7 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
 	// this will first check syspolicy, MDM settings like Registry
 	// this will first check syspolicy, MDM settings like Registry
 	// on Windows or defaults on macOS. If they are not set, it falls
 	// on Windows or defaults on macOS. If they are not set, it falls
 	// back to the cli-flag, `--posture-checking`.
 	// back to the cli-flag, `--posture-checking`.
-	choice, err := syspolicy.GetPreferenceOption(syspolicy.PostureChecking)
+	choice, err := syspolicy.GetPreferenceOption(pkey.PostureChecking)
 	if err != nil {
 	if err != nil {
 		b.logf(
 		b.logf(
 			"c2n: failed to read PostureChecking from syspolicy, returning default from CLI: %s; got error: %s",
 			"c2n: failed to read PostureChecking from syspolicy, returning default from CLI: %s; got error: %s",

+ 27 - 26
ipn/ipnlocal/local.go

@@ -108,6 +108,7 @@ import (
 	"tailscale.com/util/set"
 	"tailscale.com/util/set"
 	"tailscale.com/util/slicesx"
 	"tailscale.com/util/slicesx"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/rsop"
 	"tailscale.com/util/syspolicy/rsop"
 	"tailscale.com/util/systemd"
 	"tailscale.com/util/systemd"
 	"tailscale.com/util/testenv"
 	"tailscale.com/util/testenv"
@@ -1762,51 +1763,51 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
 }
 }
 
 
 type preferencePolicyInfo struct {
 type preferencePolicyInfo struct {
-	key syspolicy.Key
+	key pkey.Key
 	get func(ipn.PrefsView) bool
 	get func(ipn.PrefsView) bool
 	set func(*ipn.Prefs, bool)
 	set func(*ipn.Prefs, bool)
 }
 }
 
 
 var preferencePolicies = []preferencePolicyInfo{
 var preferencePolicies = []preferencePolicyInfo{
 	{
 	{
-		key: syspolicy.EnableIncomingConnections,
+		key: pkey.EnableIncomingConnections,
 		// Allow Incoming (used by the UI) is the negation of ShieldsUp (used by the
 		// Allow Incoming (used by the UI) is the negation of ShieldsUp (used by the
 		// backend), so this has to convert between the two conventions.
 		// backend), so this has to convert between the two conventions.
 		get: func(p ipn.PrefsView) bool { return !p.ShieldsUp() },
 		get: func(p ipn.PrefsView) bool { return !p.ShieldsUp() },
 		set: func(p *ipn.Prefs, v bool) { p.ShieldsUp = !v },
 		set: func(p *ipn.Prefs, v bool) { p.ShieldsUp = !v },
 	},
 	},
 	{
 	{
-		key: syspolicy.EnableServerMode,
+		key: pkey.EnableServerMode,
 		get: func(p ipn.PrefsView) bool { return p.ForceDaemon() },
 		get: func(p ipn.PrefsView) bool { return p.ForceDaemon() },
 		set: func(p *ipn.Prefs, v bool) { p.ForceDaemon = v },
 		set: func(p *ipn.Prefs, v bool) { p.ForceDaemon = v },
 	},
 	},
 	{
 	{
-		key: syspolicy.ExitNodeAllowLANAccess,
+		key: pkey.ExitNodeAllowLANAccess,
 		get: func(p ipn.PrefsView) bool { return p.ExitNodeAllowLANAccess() },
 		get: func(p ipn.PrefsView) bool { return p.ExitNodeAllowLANAccess() },
 		set: func(p *ipn.Prefs, v bool) { p.ExitNodeAllowLANAccess = v },
 		set: func(p *ipn.Prefs, v bool) { p.ExitNodeAllowLANAccess = v },
 	},
 	},
 	{
 	{
-		key: syspolicy.EnableTailscaleDNS,
+		key: pkey.EnableTailscaleDNS,
 		get: func(p ipn.PrefsView) bool { return p.CorpDNS() },
 		get: func(p ipn.PrefsView) bool { return p.CorpDNS() },
 		set: func(p *ipn.Prefs, v bool) { p.CorpDNS = v },
 		set: func(p *ipn.Prefs, v bool) { p.CorpDNS = v },
 	},
 	},
 	{
 	{
-		key: syspolicy.EnableTailscaleSubnets,
+		key: pkey.EnableTailscaleSubnets,
 		get: func(p ipn.PrefsView) bool { return p.RouteAll() },
 		get: func(p ipn.PrefsView) bool { return p.RouteAll() },
 		set: func(p *ipn.Prefs, v bool) { p.RouteAll = v },
 		set: func(p *ipn.Prefs, v bool) { p.RouteAll = v },
 	},
 	},
 	{
 	{
-		key: syspolicy.CheckUpdates,
+		key: pkey.CheckUpdates,
 		get: func(p ipn.PrefsView) bool { return p.AutoUpdate().Check },
 		get: func(p ipn.PrefsView) bool { return p.AutoUpdate().Check },
 		set: func(p *ipn.Prefs, v bool) { p.AutoUpdate.Check = v },
 		set: func(p *ipn.Prefs, v bool) { p.AutoUpdate.Check = v },
 	},
 	},
 	{
 	{
-		key: syspolicy.ApplyUpdates,
+		key: pkey.ApplyUpdates,
 		get: func(p ipn.PrefsView) bool { v, _ := p.AutoUpdate().Apply.Get(); return v },
 		get: func(p ipn.PrefsView) bool { v, _ := p.AutoUpdate().Apply.Get(); return v },
 		set: func(p *ipn.Prefs, v bool) { p.AutoUpdate.Apply.Set(v) },
 		set: func(p *ipn.Prefs, v bool) { p.AutoUpdate.Apply.Set(v) },
 	},
 	},
 	{
 	{
-		key: syspolicy.EnableRunExitNode,
+		key: pkey.EnableRunExitNode,
 		get: func(p ipn.PrefsView) bool { return p.AdvertisesExitNode() },
 		get: func(p ipn.PrefsView) bool { return p.AdvertisesExitNode() },
 		set: func(p *ipn.Prefs, v bool) { p.SetAdvertiseExitNode(v) },
 		set: func(p *ipn.Prefs, v bool) { p.SetAdvertiseExitNode(v) },
 	},
 	},
@@ -1817,13 +1818,13 @@ var preferencePolicies = []preferencePolicyInfo{
 //
 //
 // b.mu must be held.
 // b.mu must be held.
 func (b *LocalBackend) applySysPolicyLocked(prefs *ipn.Prefs) (anyChange bool) {
 func (b *LocalBackend) applySysPolicyLocked(prefs *ipn.Prefs) (anyChange bool) {
-	if controlURL, err := syspolicy.GetString(syspolicy.ControlURL, prefs.ControlURL); err == nil && prefs.ControlURL != controlURL {
+	if controlURL, err := syspolicy.GetString(pkey.ControlURL, prefs.ControlURL); err == nil && prefs.ControlURL != controlURL {
 		prefs.ControlURL = controlURL
 		prefs.ControlURL = controlURL
 		anyChange = true
 		anyChange = true
 	}
 	}
 
 
 	const sentinel = "HostnameDefaultValue"
 	const sentinel = "HostnameDefaultValue"
-	hostnameFromPolicy, _ := syspolicy.GetString(syspolicy.Hostname, sentinel)
+	hostnameFromPolicy, _ := syspolicy.GetString(pkey.Hostname, sentinel)
 	switch hostnameFromPolicy {
 	switch hostnameFromPolicy {
 	case sentinel:
 	case sentinel:
 		// An empty string for this policy value means that the admin wants to delete
 		// An empty string for this policy value means that the admin wants to delete
@@ -1858,7 +1859,7 @@ func (b *LocalBackend) applySysPolicyLocked(prefs *ipn.Prefs) (anyChange bool) {
 		anyChange = true
 		anyChange = true
 	}
 	}
 
 
-	if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); alwaysOn && !b.overrideAlwaysOn && !prefs.WantRunning {
+	if alwaysOn, _ := syspolicy.GetBoolean(pkey.AlwaysOn, false); alwaysOn && !b.overrideAlwaysOn && !prefs.WantRunning {
 		prefs.WantRunning = true
 		prefs.WantRunning = true
 		anyChange = true
 		anyChange = true
 	}
 	}
@@ -1882,7 +1883,7 @@ func (b *LocalBackend) applySysPolicyLocked(prefs *ipn.Prefs) (anyChange bool) {
 //
 //
 // b.mu must be held.
 // b.mu must be held.
 func (b *LocalBackend) applyExitNodeSysPolicyLocked(prefs *ipn.Prefs) (anyChange bool) {
 func (b *LocalBackend) applyExitNodeSysPolicyLocked(prefs *ipn.Prefs) (anyChange bool) {
-	if exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, ""); exitNodeIDStr != "" {
+	if exitNodeIDStr, _ := syspolicy.GetString(pkey.ExitNodeID, ""); exitNodeIDStr != "" {
 		exitNodeID := tailcfg.StableNodeID(exitNodeIDStr)
 		exitNodeID := tailcfg.StableNodeID(exitNodeIDStr)
 
 
 		// Try to parse the policy setting value as an "auto:"-prefixed [ipn.ExitNodeExpression],
 		// Try to parse the policy setting value as an "auto:"-prefixed [ipn.ExitNodeExpression],
@@ -1923,7 +1924,7 @@ func (b *LocalBackend) applyExitNodeSysPolicyLocked(prefs *ipn.Prefs) (anyChange
 			prefs.ExitNodeIP = netip.Addr{}
 			prefs.ExitNodeIP = netip.Addr{}
 			anyChange = true
 			anyChange = true
 		}
 		}
-	} else if exitNodeIPStr, _ := syspolicy.GetString(syspolicy.ExitNodeIP, ""); exitNodeIPStr != "" {
+	} else if exitNodeIPStr, _ := syspolicy.GetString(pkey.ExitNodeIP, ""); exitNodeIPStr != "" {
 		if prefs.AutoExitNode != "" {
 		if prefs.AutoExitNode != "" {
 			prefs.AutoExitNode = "" // mutually exclusive with ExitNodeIP
 			prefs.AutoExitNode = "" // mutually exclusive with ExitNodeIP
 			anyChange = true
 			anyChange = true
@@ -1970,7 +1971,7 @@ func (b *LocalBackend) reconcilePrefs() (_ ipn.PrefsView, anyChange bool) {
 // sysPolicyChanged is a callback triggered by syspolicy when it detects
 // sysPolicyChanged is a callback triggered by syspolicy when it detects
 // a change in one or more syspolicy settings.
 // a change in one or more syspolicy settings.
 func (b *LocalBackend) sysPolicyChanged(policy *rsop.PolicyChange) {
 func (b *LocalBackend) sysPolicyChanged(policy *rsop.PolicyChange) {
-	if policy.HasChangedAnyOf(syspolicy.AlwaysOn, syspolicy.AlwaysOnOverrideWithReason) {
+	if policy.HasChangedAnyOf(pkey.AlwaysOn, pkey.AlwaysOnOverrideWithReason) {
 		// If the AlwaysOn or the AlwaysOnOverrideWithReason policy has changed,
 		// If the AlwaysOn or the AlwaysOnOverrideWithReason policy has changed,
 		// we should reset the overrideAlwaysOn flag, as the override might
 		// we should reset the overrideAlwaysOn flag, as the override might
 		// no longer be valid.
 		// no longer be valid.
@@ -1979,7 +1980,7 @@ func (b *LocalBackend) sysPolicyChanged(policy *rsop.PolicyChange) {
 		b.mu.Unlock()
 		b.mu.Unlock()
 	}
 	}
 
 
-	if policy.HasChangedAnyOf(syspolicy.ExitNodeID, syspolicy.ExitNodeIP, syspolicy.AllowExitNodeOverride) {
+	if policy.HasChangedAnyOf(pkey.ExitNodeID, pkey.ExitNodeIP, pkey.AllowExitNodeOverride) {
 		// Reset the exit node override if a policy that enforces exit node usage
 		// Reset the exit node override if a policy that enforces exit node usage
 		// or allows the user to override automatic exit node selection has changed.
 		// or allows the user to override automatic exit node selection has changed.
 		b.mu.Lock()
 		b.mu.Lock()
@@ -1987,7 +1988,7 @@ func (b *LocalBackend) sysPolicyChanged(policy *rsop.PolicyChange) {
 		b.mu.Unlock()
 		b.mu.Unlock()
 	}
 	}
 
 
-	if policy.HasChanged(syspolicy.AllowedSuggestedExitNodes) {
+	if policy.HasChanged(pkey.AllowedSuggestedExitNodes) {
 		b.refreshAllowedSuggestions()
 		b.refreshAllowedSuggestions()
 		// Re-evaluate exit node suggestion now that the policy setting has changed.
 		// Re-evaluate exit node suggestion now that the policy setting has changed.
 		if _, err := b.SuggestExitNode(); err != nil && !errors.Is(err, ErrNoPreferredDERP) {
 		if _, err := b.SuggestExitNode(); err != nil && !errors.Is(err, ErrNoPreferredDERP) {
@@ -2348,7 +2349,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
 	}
 	}
 
 
 	if b.state != ipn.Running && b.conf == nil && opts.AuthKey == "" {
 	if b.state != ipn.Running && b.conf == nil && opts.AuthKey == "" {
-		sysak, _ := syspolicy.GetString(syspolicy.AuthKey, "")
+		sysak, _ := syspolicy.GetString(pkey.AuthKey, "")
 		if sysak != "" {
 		if sysak != "" {
 			b.logf("Start: setting opts.AuthKey by syspolicy, len=%v", len(sysak))
 			b.logf("Start: setting opts.AuthKey by syspolicy, len=%v", len(sysak))
 			opts.AuthKey = strings.TrimSpace(sysak)
 			opts.AuthKey = strings.TrimSpace(sysak)
@@ -4407,7 +4408,7 @@ func (b *LocalBackend) checkEditPrefsAccessLocked(actor ipnauth.Actor, prefs ipn
 	// Prevent users from changing exit node preferences
 	// Prevent users from changing exit node preferences
 	// when exit node usage is managed by policy.
 	// when exit node usage is managed by policy.
 	if mp.ExitNodeIDSet || mp.ExitNodeIPSet || mp.AutoExitNodeSet {
 	if mp.ExitNodeIDSet || mp.ExitNodeIPSet || mp.AutoExitNodeSet {
-		isManaged, err := syspolicy.HasAnyOf(syspolicy.ExitNodeID, syspolicy.ExitNodeIP)
+		isManaged, err := syspolicy.HasAnyOf(pkey.ExitNodeID, pkey.ExitNodeIP)
 		if err != nil {
 		if err != nil {
 			err = fmt.Errorf("policy check failed: %w", err)
 			err = fmt.Errorf("policy check failed: %w", err)
 		} else if isManaged {
 		} else if isManaged {
@@ -4415,7 +4416,7 @@ func (b *LocalBackend) checkEditPrefsAccessLocked(actor ipnauth.Actor, prefs ipn
 			// if permitted by [syspolicy.AllowExitNodeOverride].
 			// if permitted by [syspolicy.AllowExitNodeOverride].
 			//
 			//
 			// Disabling exit node usage entirely is not allowed.
 			// Disabling exit node usage entirely is not allowed.
-			allowExitNodeOverride, _ := syspolicy.GetBoolean(syspolicy.AllowExitNodeOverride, false)
+			allowExitNodeOverride, _ := syspolicy.GetBoolean(pkey.AllowExitNodeOverride, false)
 			if !allowExitNodeOverride || b.changeDisablesExitNodeLocked(prefs, mp) {
 			if !allowExitNodeOverride || b.changeDisablesExitNodeLocked(prefs, mp) {
 				err = errManagedByPolicy
 				err = errManagedByPolicy
 			}
 			}
@@ -4519,7 +4520,7 @@ func (b *LocalBackend) onEditPrefsLocked(_ ipnauth.Actor, mp *ipn.MaskedPrefs, o
 		// mode on them until the policy changes, they switch to a different profile, etc.
 		// mode on them until the policy changes, they switch to a different profile, etc.
 		b.overrideAlwaysOn = true
 		b.overrideAlwaysOn = true
 
 
-		if reconnectAfter, _ := syspolicy.GetDuration(syspolicy.ReconnectAfter, 0); reconnectAfter > 0 {
+		if reconnectAfter, _ := syspolicy.GetDuration(pkey.ReconnectAfter, 0); reconnectAfter > 0 {
 			b.startReconnectTimerLocked(reconnectAfter)
 			b.startReconnectTimerLocked(reconnectAfter)
 		}
 		}
 	}
 	}
@@ -4530,7 +4531,7 @@ func (b *LocalBackend) onEditPrefsLocked(_ ipnauth.Actor, mp *ipn.MaskedPrefs, o
 		b.overrideExitNodePolicy = false
 		b.overrideExitNodePolicy = false
 	}
 	}
 	if mp.AutoExitNodeSet || mp.ExitNodeIDSet || mp.ExitNodeIPSet {
 	if mp.AutoExitNodeSet || mp.ExitNodeIDSet || mp.ExitNodeIPSet {
-		if allowExitNodeOverride, _ := syspolicy.GetBoolean(syspolicy.AllowExitNodeOverride, false); allowExitNodeOverride {
+		if allowExitNodeOverride, _ := syspolicy.GetBoolean(pkey.AllowExitNodeOverride, false); allowExitNodeOverride {
 			// If applying exit node policy settings to the new prefs results in no change,
 			// If applying exit node policy settings to the new prefs results in no change,
 			// the user is not overriding the policy. Otherwise, it is an override.
 			// the user is not overriding the policy. Otherwise, it is an override.
 			b.overrideExitNodePolicy = b.applyExitNodeSysPolicyLocked(newPrefs.AsStruct())
 			b.overrideExitNodePolicy = b.applyExitNodeSysPolicyLocked(newPrefs.AsStruct())
@@ -7807,9 +7808,9 @@ type selectRegionFunc func(views.Slice[int]) int
 type selectNodeFunc func(nodes views.Slice[tailcfg.NodeView], last tailcfg.StableNodeID) tailcfg.NodeView
 type selectNodeFunc func(nodes views.Slice[tailcfg.NodeView], last tailcfg.StableNodeID) tailcfg.NodeView
 
 
 func fillAllowedSuggestions() set.Set[tailcfg.StableNodeID] {
 func fillAllowedSuggestions() set.Set[tailcfg.StableNodeID] {
-	nodes, err := syspolicy.GetStringArray(syspolicy.AllowedSuggestedExitNodes, nil)
+	nodes, err := syspolicy.GetStringArray(pkey.AllowedSuggestedExitNodes, nil)
 	if err != nil {
 	if err != nil {
-		log.Printf("fillAllowedSuggestions: unable to look up %q policy: %v", syspolicy.AllowedSuggestedExitNodes, err)
+		log.Printf("fillAllowedSuggestions: unable to look up %q policy: %v", pkey.AllowedSuggestedExitNodes, err)
 		return nil
 		return nil
 	}
 	}
 	if nodes == nil {
 	if nodes == nil {
@@ -8176,7 +8177,7 @@ func isAllowedAutoExitNodeID(exitNodeID tailcfg.StableNodeID) bool {
 	if exitNodeID == "" {
 	if exitNodeID == "" {
 		return false // an exit node is required
 		return false // an exit node is required
 	}
 	}
-	if nodes, _ := syspolicy.GetStringArray(syspolicy.AllowedSuggestedExitNodes, nil); nodes != nil {
+	if nodes, _ := syspolicy.GetStringArray(pkey.AllowedSuggestedExitNodes, nil); nodes != nil {
 		return slices.Contains(nodes, string(exitNodeID))
 		return slices.Contains(nodes, string(exitNodeID))
 
 
 	}
 	}
@@ -8339,7 +8340,7 @@ func (b *LocalBackend) stateEncrypted() opt.Bool {
 			// the Keychain. A future release will clean up the on-disk state
 			// the Keychain. A future release will clean up the on-disk state
 			// files.
 			// files.
 			// TODO(#15830): always return true here once MacSys is fully migrated.
 			// TODO(#15830): always return true here once MacSys is fully migrated.
-			sp, _ := syspolicy.GetBoolean(syspolicy.EncryptState, false)
+			sp, _ := syspolicy.GetBoolean(pkey.EncryptState, false)
 			return opt.NewBool(sp)
 			return opt.NewBool(sp)
 		default:
 		default:
 			// Probably self-compiled tailscaled, we don't use the Keychain
 			// Probably self-compiled tailscaled, we don't use the Keychain

+ 59 - 58
ipn/ipnlocal/local_test.go

@@ -62,6 +62,7 @@ import (
 	"tailscale.com/util/must"
 	"tailscale.com/util/must"
 	"tailscale.com/util/set"
 	"tailscale.com/util/set"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/source"
 	"tailscale.com/util/syspolicy/source"
 	"tailscale.com/wgengine"
 	"tailscale.com/wgengine"
@@ -1182,16 +1183,16 @@ func TestConfigureExitNode(t *testing.T) {
 			// Configure policy settings, if any.
 			// Configure policy settings, if any.
 			store := source.NewTestStore(t)
 			store := source.NewTestStore(t)
 			if tt.exitNodeIDPolicy != nil {
 			if tt.exitNodeIDPolicy != nil {
-				store.SetStrings(source.TestSettingOf(syspolicy.ExitNodeID, string(*tt.exitNodeIDPolicy)))
+				store.SetStrings(source.TestSettingOf(pkey.ExitNodeID, string(*tt.exitNodeIDPolicy)))
 			}
 			}
 			if tt.exitNodeIPPolicy != nil {
 			if tt.exitNodeIPPolicy != nil {
-				store.SetStrings(source.TestSettingOf(syspolicy.ExitNodeIP, tt.exitNodeIPPolicy.String()))
+				store.SetStrings(source.TestSettingOf(pkey.ExitNodeIP, tt.exitNodeIPPolicy.String()))
 			}
 			}
 			if tt.exitNodeAllowedIDs != nil {
 			if tt.exitNodeAllowedIDs != nil {
-				store.SetStringLists(source.TestSettingOf(syspolicy.AllowedSuggestedExitNodes, toStrings(tt.exitNodeAllowedIDs)))
+				store.SetStringLists(source.TestSettingOf(pkey.AllowedSuggestedExitNodes, toStrings(tt.exitNodeAllowedIDs)))
 			}
 			}
 			if tt.exitNodeAllowOverride {
 			if tt.exitNodeAllowOverride {
-				store.SetBooleans(source.TestSettingOf(syspolicy.AllowExitNodeOverride, true))
+				store.SetBooleans(source.TestSettingOf(pkey.AllowExitNodeOverride, true))
 			}
 			}
 			if store.IsEmpty() {
 			if store.IsEmpty() {
 				// No syspolicy settings, so don't register a store.
 				// No syspolicy settings, so don't register a store.
@@ -2890,10 +2891,10 @@ func TestSetExitNodeIDPolicy(t *testing.T) {
 
 
 			policyStore := source.NewTestStore(t)
 			policyStore := source.NewTestStore(t)
 			if test.exitNodeIDKey {
 			if test.exitNodeIDKey {
-				policyStore.SetStrings(source.TestSettingOf(syspolicy.ExitNodeID, test.exitNodeID))
+				policyStore.SetStrings(source.TestSettingOf(pkey.ExitNodeID, test.exitNodeID))
 			}
 			}
 			if test.exitNodeIPKey {
 			if test.exitNodeIPKey {
-				policyStore.SetStrings(source.TestSettingOf(syspolicy.ExitNodeIP, test.exitNodeIP))
+				policyStore.SetStrings(source.TestSettingOf(pkey.ExitNodeIP, test.exitNodeIP))
 			}
 			}
 			syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
 			syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
 
 
@@ -3029,7 +3030,7 @@ func TestUpdateNetmapDeltaAutoExitNode(t *testing.T) {
 
 
 	syspolicy.RegisterWellKnownSettingsForTest(t)
 	syspolicy.RegisterWellKnownSettingsForTest(t)
 	policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
 	policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
-		syspolicy.ExitNodeID, "auto:any",
+		pkey.ExitNodeID, "auto:any",
 	))
 	))
 	syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
 	syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
 
 
@@ -3114,7 +3115,7 @@ func TestAutoExitNodeSetNetInfoCallback(t *testing.T) {
 	b.cc = cc
 	b.cc = cc
 	syspolicy.RegisterWellKnownSettingsForTest(t)
 	syspolicy.RegisterWellKnownSettingsForTest(t)
 	policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
 	policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
-		syspolicy.ExitNodeID, "auto:any",
+		pkey.ExitNodeID, "auto:any",
 	))
 	))
 	syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
 	syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
 	peer1 := makePeer(1, withCap(26), withDERP(3), withSuggest(), withExitRoutes())
 	peer1 := makePeer(1, withCap(26), withDERP(3), withSuggest(), withExitRoutes())
@@ -3223,7 +3224,7 @@ func TestSetControlClientStatusAutoExitNode(t *testing.T) {
 	b := newTestLocalBackend(t)
 	b := newTestLocalBackend(t)
 	syspolicy.RegisterWellKnownSettingsForTest(t)
 	syspolicy.RegisterWellKnownSettingsForTest(t)
 	policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
 	policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
-		syspolicy.ExitNodeID, "auto:any",
+		pkey.ExitNodeID, "auto:any",
 	))
 	))
 	syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
 	syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
 	b.currentNode().SetNetMap(nm)
 	b.currentNode().SetNetMap(nm)
@@ -3255,7 +3256,7 @@ func TestApplySysPolicy(t *testing.T) {
 		prefs          ipn.Prefs
 		prefs          ipn.Prefs
 		wantPrefs      ipn.Prefs
 		wantPrefs      ipn.Prefs
 		wantAnyChange  bool
 		wantAnyChange  bool
-		stringPolicies map[syspolicy.Key]string
+		stringPolicies map[pkey.Key]string
 	}{
 	}{
 		{
 		{
 			name: "empty prefs without policies",
 			name: "empty prefs without policies",
@@ -3290,13 +3291,13 @@ func TestApplySysPolicy(t *testing.T) {
 				RouteAll:               true,
 				RouteAll:               true,
 			},
 			},
 			wantAnyChange: true,
 			wantAnyChange: true,
-			stringPolicies: map[syspolicy.Key]string{
-				syspolicy.ControlURL:                "1",
-				syspolicy.EnableIncomingConnections: "never",
-				syspolicy.EnableServerMode:          "always",
-				syspolicy.ExitNodeAllowLANAccess:    "always",
-				syspolicy.EnableTailscaleDNS:        "always",
-				syspolicy.EnableTailscaleSubnets:    "always",
+			stringPolicies: map[pkey.Key]string{
+				pkey.ControlURL:                "1",
+				pkey.EnableIncomingConnections: "never",
+				pkey.EnableServerMode:          "always",
+				pkey.ExitNodeAllowLANAccess:    "always",
+				pkey.EnableTailscaleDNS:        "always",
+				pkey.EnableTailscaleSubnets:    "always",
 			},
 			},
 		},
 		},
 		{
 		{
@@ -3311,13 +3312,13 @@ func TestApplySysPolicy(t *testing.T) {
 				ShieldsUp:   true,
 				ShieldsUp:   true,
 				ForceDaemon: true,
 				ForceDaemon: true,
 			},
 			},
-			stringPolicies: map[syspolicy.Key]string{
-				syspolicy.ControlURL:                "1",
-				syspolicy.EnableIncomingConnections: "never",
-				syspolicy.EnableServerMode:          "always",
-				syspolicy.ExitNodeAllowLANAccess:    "never",
-				syspolicy.EnableTailscaleDNS:        "never",
-				syspolicy.EnableTailscaleSubnets:    "never",
+			stringPolicies: map[pkey.Key]string{
+				pkey.ControlURL:                "1",
+				pkey.EnableIncomingConnections: "never",
+				pkey.EnableServerMode:          "always",
+				pkey.ExitNodeAllowLANAccess:    "never",
+				pkey.EnableTailscaleDNS:        "never",
+				pkey.EnableTailscaleSubnets:    "never",
 			},
 			},
 		},
 		},
 		{
 		{
@@ -3339,13 +3340,13 @@ func TestApplySysPolicy(t *testing.T) {
 				RouteAll:               true,
 				RouteAll:               true,
 			},
 			},
 			wantAnyChange: true,
 			wantAnyChange: true,
-			stringPolicies: map[syspolicy.Key]string{
-				syspolicy.ControlURL:                "2",
-				syspolicy.EnableIncomingConnections: "always",
-				syspolicy.EnableServerMode:          "never",
-				syspolicy.ExitNodeAllowLANAccess:    "always",
-				syspolicy.EnableTailscaleDNS:        "never",
-				syspolicy.EnableTailscaleSubnets:    "always",
+			stringPolicies: map[pkey.Key]string{
+				pkey.ControlURL:                "2",
+				pkey.EnableIncomingConnections: "always",
+				pkey.EnableServerMode:          "never",
+				pkey.ExitNodeAllowLANAccess:    "always",
+				pkey.EnableTailscaleDNS:        "never",
+				pkey.EnableTailscaleSubnets:    "always",
 			},
 			},
 		},
 		},
 		{
 		{
@@ -3366,12 +3367,12 @@ func TestApplySysPolicy(t *testing.T) {
 				CorpDNS:                true,
 				CorpDNS:                true,
 				RouteAll:               true,
 				RouteAll:               true,
 			},
 			},
-			stringPolicies: map[syspolicy.Key]string{
-				syspolicy.EnableIncomingConnections: "user-decides",
-				syspolicy.EnableServerMode:          "user-decides",
-				syspolicy.ExitNodeAllowLANAccess:    "user-decides",
-				syspolicy.EnableTailscaleDNS:        "user-decides",
-				syspolicy.EnableTailscaleSubnets:    "user-decides",
+			stringPolicies: map[pkey.Key]string{
+				pkey.EnableIncomingConnections: "user-decides",
+				pkey.EnableServerMode:          "user-decides",
+				pkey.ExitNodeAllowLANAccess:    "user-decides",
+				pkey.EnableTailscaleDNS:        "user-decides",
+				pkey.EnableTailscaleSubnets:    "user-decides",
 			},
 			},
 		},
 		},
 		{
 		{
@@ -3380,8 +3381,8 @@ func TestApplySysPolicy(t *testing.T) {
 				ControlURL: "set",
 				ControlURL: "set",
 			},
 			},
 			wantAnyChange: true,
 			wantAnyChange: true,
-			stringPolicies: map[syspolicy.Key]string{
-				syspolicy.ControlURL: "set",
+			stringPolicies: map[pkey.Key]string{
+				pkey.ControlURL: "set",
 			},
 			},
 		},
 		},
 		{
 		{
@@ -3399,8 +3400,8 @@ func TestApplySysPolicy(t *testing.T) {
 				},
 				},
 			},
 			},
 			wantAnyChange: true,
 			wantAnyChange: true,
-			stringPolicies: map[syspolicy.Key]string{
-				syspolicy.ApplyUpdates: "always",
+			stringPolicies: map[pkey.Key]string{
+				pkey.ApplyUpdates: "always",
 			},
 			},
 		},
 		},
 		{
 		{
@@ -3418,8 +3419,8 @@ func TestApplySysPolicy(t *testing.T) {
 				},
 				},
 			},
 			},
 			wantAnyChange: true,
 			wantAnyChange: true,
-			stringPolicies: map[syspolicy.Key]string{
-				syspolicy.ApplyUpdates: "never",
+			stringPolicies: map[pkey.Key]string{
+				pkey.ApplyUpdates: "never",
 			},
 			},
 		},
 		},
 		{
 		{
@@ -3437,8 +3438,8 @@ func TestApplySysPolicy(t *testing.T) {
 				},
 				},
 			},
 			},
 			wantAnyChange: true,
 			wantAnyChange: true,
-			stringPolicies: map[syspolicy.Key]string{
-				syspolicy.CheckUpdates: "always",
+			stringPolicies: map[pkey.Key]string{
+				pkey.CheckUpdates: "always",
 			},
 			},
 		},
 		},
 		{
 		{
@@ -3456,8 +3457,8 @@ func TestApplySysPolicy(t *testing.T) {
 				},
 				},
 			},
 			},
 			wantAnyChange: true,
 			wantAnyChange: true,
-			stringPolicies: map[syspolicy.Key]string{
-				syspolicy.CheckUpdates: "never",
+			stringPolicies: map[pkey.Key]string{
+				pkey.CheckUpdates: "never",
 			},
 			},
 		},
 		},
 	}
 	}
@@ -5574,7 +5575,7 @@ func TestFillAllowedSuggestions(t *testing.T) {
 	for _, tt := range tests {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
 			policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
-				syspolicy.AllowedSuggestedExitNodes, tt.allowPolicy,
+				pkey.AllowedSuggestedExitNodes, tt.allowPolicy,
 			))
 			))
 			syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
 			syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
 
 
@@ -6480,23 +6481,23 @@ func TestUpdatePrefsOnSysPolicyChange(t *testing.T) {
 	}{
 	}{
 		{
 		{
 			name:           "ShieldsUp/True",
 			name:           "ShieldsUp/True",
-			stringSettings: []source.TestSetting[string]{source.TestSettingOf(syspolicy.EnableIncomingConnections, "never")},
+			stringSettings: []source.TestSetting[string]{source.TestSettingOf(pkey.EnableIncomingConnections, "never")},
 			want:           wantPrefsChanges(fieldChange{"ShieldsUp", true}),
 			want:           wantPrefsChanges(fieldChange{"ShieldsUp", true}),
 		},
 		},
 		{
 		{
 			name:           "ShieldsUp/False",
 			name:           "ShieldsUp/False",
 			initialPrefs:   &ipn.Prefs{ShieldsUp: true},
 			initialPrefs:   &ipn.Prefs{ShieldsUp: true},
-			stringSettings: []source.TestSetting[string]{source.TestSettingOf(syspolicy.EnableIncomingConnections, "always")},
+			stringSettings: []source.TestSetting[string]{source.TestSettingOf(pkey.EnableIncomingConnections, "always")},
 			want:           wantPrefsChanges(fieldChange{"ShieldsUp", false}),
 			want:           wantPrefsChanges(fieldChange{"ShieldsUp", false}),
 		},
 		},
 		{
 		{
 			name:           "ExitNodeID",
 			name:           "ExitNodeID",
-			stringSettings: []source.TestSetting[string]{source.TestSettingOf(syspolicy.ExitNodeID, "foo")},
+			stringSettings: []source.TestSetting[string]{source.TestSettingOf(pkey.ExitNodeID, "foo")},
 			want:           wantPrefsChanges(fieldChange{"ExitNodeID", tailcfg.StableNodeID("foo")}),
 			want:           wantPrefsChanges(fieldChange{"ExitNodeID", tailcfg.StableNodeID("foo")}),
 		},
 		},
 		{
 		{
 			name:           "EnableRunExitNode",
 			name:           "EnableRunExitNode",
-			stringSettings: []source.TestSetting[string]{source.TestSettingOf(syspolicy.EnableRunExitNode, "always")},
+			stringSettings: []source.TestSetting[string]{source.TestSettingOf(pkey.EnableRunExitNode, "always")},
 			want:           wantPrefsChanges(fieldChange{"AdvertiseRoutes", []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()}}),
 			want:           wantPrefsChanges(fieldChange{"AdvertiseRoutes", []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()}}),
 		},
 		},
 		{
 		{
@@ -6505,9 +6506,9 @@ func TestUpdatePrefsOnSysPolicyChange(t *testing.T) {
 				ExitNodeAllowLANAccess: true,
 				ExitNodeAllowLANAccess: true,
 			},
 			},
 			stringSettings: []source.TestSetting[string]{
 			stringSettings: []source.TestSetting[string]{
-				source.TestSettingOf(syspolicy.EnableServerMode, "always"),
-				source.TestSettingOf(syspolicy.ExitNodeAllowLANAccess, "never"),
-				source.TestSettingOf(syspolicy.ExitNodeIP, "127.0.0.1"),
+				source.TestSettingOf(pkey.EnableServerMode, "always"),
+				source.TestSettingOf(pkey.ExitNodeAllowLANAccess, "never"),
+				source.TestSettingOf(pkey.ExitNodeIP, "127.0.0.1"),
 			},
 			},
 			want: wantPrefsChanges(
 			want: wantPrefsChanges(
 				fieldChange{"ForceDaemon", true},
 				fieldChange{"ForceDaemon", true},
@@ -6523,9 +6524,9 @@ func TestUpdatePrefsOnSysPolicyChange(t *testing.T) {
 				AdvertiseRoutes: []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()},
 				AdvertiseRoutes: []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()},
 			},
 			},
 			stringSettings: []source.TestSetting[string]{
 			stringSettings: []source.TestSetting[string]{
-				source.TestSettingOf(syspolicy.EnableTailscaleDNS, "always"),
-				source.TestSettingOf(syspolicy.ExitNodeID, "foo"),
-				source.TestSettingOf(syspolicy.EnableRunExitNode, "always"),
+				source.TestSettingOf(pkey.EnableTailscaleDNS, "always"),
+				source.TestSettingOf(pkey.ExitNodeID, "foo"),
+				source.TestSettingOf(pkey.EnableRunExitNode, "always"),
 			},
 			},
 			want: nil, // syspolicy settings match the preferences; no change notification is expected.
 			want: nil, // syspolicy settings match the preferences; no change notification is expected.
 		},
 		},

+ 2 - 1
ipn/prefs.go

@@ -29,6 +29,7 @@ import (
 	"tailscale.com/types/views"
 	"tailscale.com/types/views"
 	"tailscale.com/util/dnsname"
 	"tailscale.com/util/dnsname"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/version"
 	"tailscale.com/version"
 )
 )
 
 
@@ -726,7 +727,7 @@ func (p PrefsView) ControlURLOrDefault() string {
 // If not configured, or if the configured value is a legacy name equivalent to
 // If not configured, or if the configured value is a legacy name equivalent to
 // the default, then DefaultControlURL is returned instead.
 // the default, then DefaultControlURL is returned instead.
 func (p *Prefs) ControlURLOrDefault() string {
 func (p *Prefs) ControlURLOrDefault() string {
-	controlURL, err := syspolicy.GetString(syspolicy.ControlURL, p.ControlURL)
+	controlURL, err := syspolicy.GetString(pkey.ControlURL, p.ControlURL)
 	if err != nil {
 	if err != nil {
 		controlURL = p.ControlURL
 		controlURL = p.ControlURL
 	}
 	}

+ 2 - 1
logpolicy/logpolicy.go

@@ -52,6 +52,7 @@ import (
 	"tailscale.com/util/must"
 	"tailscale.com/util/must"
 	"tailscale.com/util/racebuild"
 	"tailscale.com/util/racebuild"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/testenv"
 	"tailscale.com/util/testenv"
 	"tailscale.com/version"
 	"tailscale.com/version"
 	"tailscale.com/version/distro"
 	"tailscale.com/version/distro"
@@ -65,7 +66,7 @@ var getLogTargetOnce struct {
 func getLogTarget() string {
 func getLogTarget() string {
 	getLogTargetOnce.Do(func() {
 	getLogTargetOnce.Do(func() {
 		envTarget, _ := os.LookupEnv("TS_LOG_TARGET")
 		envTarget, _ := os.LookupEnv("TS_LOG_TARGET")
-		getLogTargetOnce.v, _ = syspolicy.GetString(syspolicy.LogTarget, envTarget)
+		getLogTargetOnce.v, _ = syspolicy.GetString(pkey.LogTarget, envTarget)
 	})
 	})
 
 
 	return getLogTargetOnce.v
 	return getLogTargetOnce.v

+ 3 - 2
net/dns/manager_windows.go

@@ -30,6 +30,7 @@ import (
 	"tailscale.com/types/logger"
 	"tailscale.com/types/logger"
 	"tailscale.com/util/dnsname"
 	"tailscale.com/util/dnsname"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/rsop"
 	"tailscale.com/util/syspolicy/rsop"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/winutil"
 	"tailscale.com/util/winutil"
@@ -508,7 +509,7 @@ func (m *windowsManager) Close() error {
 // sysPolicyChanged is a callback triggered by [syspolicy] when it detects
 // sysPolicyChanged is a callback triggered by [syspolicy] when it detects
 // a change in one or more syspolicy settings.
 // a change in one or more syspolicy settings.
 func (m *windowsManager) sysPolicyChanged(policy *rsop.PolicyChange) {
 func (m *windowsManager) sysPolicyChanged(policy *rsop.PolicyChange) {
-	if policy.HasChanged(syspolicy.EnableDNSRegistration) {
+	if policy.HasChanged(pkey.EnableDNSRegistration) {
 		m.reconfigureDNSRegistration()
 		m.reconfigureDNSRegistration()
 	}
 	}
 }
 }
@@ -520,7 +521,7 @@ func (m *windowsManager) reconfigureDNSRegistration() {
 	// Disable DNS registration by default (if the policy setting is not configured).
 	// Disable DNS registration by default (if the policy setting is not configured).
 	// This is primarily for historical reasons and to avoid breaking existing
 	// This is primarily for historical reasons and to avoid breaking existing
 	// setups that rely on this behavior.
 	// setups that rely on this behavior.
-	enableDNSRegistration, err := syspolicy.GetPreferenceOptionOrDefault(syspolicy.EnableDNSRegistration, setting.NeverByPolicy)
+	enableDNSRegistration, err := syspolicy.GetPreferenceOptionOrDefault(pkey.EnableDNSRegistration, setting.NeverByPolicy)
 	if err != nil {
 	if err != nil {
 		m.logf("error getting DNSRegistration policy setting: %v", err) // non-fatal; we'll use the default
 		m.logf("error getting DNSRegistration policy setting: %v", err) // non-fatal; we'll use the default
 	}
 	}

+ 2 - 1
posture/serialnumber_syspolicy.go

@@ -10,13 +10,14 @@ import (
 
 
 	"tailscale.com/types/logger"
 	"tailscale.com/types/logger"
 	"tailscale.com/util/syspolicy"
 	"tailscale.com/util/syspolicy"
+	"tailscale.com/util/syspolicy/pkey"
 )
 )
 
 
 // GetSerialNumbers returns the serial number of the device as reported by an
 // GetSerialNumbers returns the serial number of the device as reported by an
 // MDM solution. It requires configuration via the DeviceSerialNumber system policy.
 // MDM solution. It requires configuration via the DeviceSerialNumber system policy.
 // This is the only way to gather serial numbers on iOS, tvOS and Android.
 // This is the only way to gather serial numbers on iOS, tvOS and Android.
 func GetSerialNumbers(_ logger.Logf) ([]string, error) {
 func GetSerialNumbers(_ logger.Logf) ([]string, error) {
-	s, err := syspolicy.GetString(syspolicy.DeviceSerialNumber, "")
+	s, err := syspolicy.GetString(pkey.DeviceSerialNumber, "")
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("failed to get serial number from MDM: %v", err)
 		return nil, fmt.Errorf("failed to get serial number from MDM: %v", err)
 	}
 	}

+ 1 - 0
tsnet/depaware.txt

@@ -379,6 +379,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
         tailscale.com/util/syspolicy/internal                        from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/internal                        from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/internal/loggerx                from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/internal/loggerx                from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/internal/metrics                from tailscale.com/util/syspolicy/source
         tailscale.com/util/syspolicy/internal/metrics                from tailscale.com/util/syspolicy/source
+        tailscale.com/util/syspolicy/pkey                            from tailscale.com/control/controlclient+
         tailscale.com/util/syspolicy/rsop                            from tailscale.com/ipn/ipnlocal+
         tailscale.com/util/syspolicy/rsop                            from tailscale.com/ipn/ipnlocal+
         tailscale.com/util/syspolicy/setting                         from tailscale.com/client/local+
         tailscale.com/util/syspolicy/setting                         from tailscale.com/client/local+
         tailscale.com/util/syspolicy/source                          from tailscale.com/util/syspolicy+
         tailscale.com/util/syspolicy/source                          from tailscale.com/util/syspolicy+

+ 1 - 0
tstest/integration/tailscaled_deps_test_darwin.go

@@ -52,6 +52,7 @@ import (
 	_ "tailscale.com/util/multierr"
 	_ "tailscale.com/util/multierr"
 	_ "tailscale.com/util/osshare"
 	_ "tailscale.com/util/osshare"
 	_ "tailscale.com/util/syspolicy"
 	_ "tailscale.com/util/syspolicy"
+	_ "tailscale.com/util/syspolicy/pkey"
 	_ "tailscale.com/version"
 	_ "tailscale.com/version"
 	_ "tailscale.com/version/distro"
 	_ "tailscale.com/version/distro"
 	_ "tailscale.com/wgengine"
 	_ "tailscale.com/wgengine"

+ 1 - 0
tstest/integration/tailscaled_deps_test_freebsd.go

@@ -52,6 +52,7 @@ import (
 	_ "tailscale.com/util/multierr"
 	_ "tailscale.com/util/multierr"
 	_ "tailscale.com/util/osshare"
 	_ "tailscale.com/util/osshare"
 	_ "tailscale.com/util/syspolicy"
 	_ "tailscale.com/util/syspolicy"
+	_ "tailscale.com/util/syspolicy/pkey"
 	_ "tailscale.com/version"
 	_ "tailscale.com/version"
 	_ "tailscale.com/version/distro"
 	_ "tailscale.com/version/distro"
 	_ "tailscale.com/wgengine"
 	_ "tailscale.com/wgengine"

+ 1 - 0
tstest/integration/tailscaled_deps_test_linux.go

@@ -52,6 +52,7 @@ import (
 	_ "tailscale.com/util/multierr"
 	_ "tailscale.com/util/multierr"
 	_ "tailscale.com/util/osshare"
 	_ "tailscale.com/util/osshare"
 	_ "tailscale.com/util/syspolicy"
 	_ "tailscale.com/util/syspolicy"
+	_ "tailscale.com/util/syspolicy/pkey"
 	_ "tailscale.com/version"
 	_ "tailscale.com/version"
 	_ "tailscale.com/version/distro"
 	_ "tailscale.com/version/distro"
 	_ "tailscale.com/wgengine"
 	_ "tailscale.com/wgengine"

+ 1 - 0
tstest/integration/tailscaled_deps_test_openbsd.go

@@ -52,6 +52,7 @@ import (
 	_ "tailscale.com/util/multierr"
 	_ "tailscale.com/util/multierr"
 	_ "tailscale.com/util/osshare"
 	_ "tailscale.com/util/osshare"
 	_ "tailscale.com/util/syspolicy"
 	_ "tailscale.com/util/syspolicy"
+	_ "tailscale.com/util/syspolicy/pkey"
 	_ "tailscale.com/version"
 	_ "tailscale.com/version"
 	_ "tailscale.com/version/distro"
 	_ "tailscale.com/version/distro"
 	_ "tailscale.com/wgengine"
 	_ "tailscale.com/wgengine"

+ 1 - 0
tstest/integration/tailscaled_deps_test_windows.go

@@ -63,6 +63,7 @@ import (
 	_ "tailscale.com/util/osdiag"
 	_ "tailscale.com/util/osdiag"
 	_ "tailscale.com/util/osshare"
 	_ "tailscale.com/util/osshare"
 	_ "tailscale.com/util/syspolicy"
 	_ "tailscale.com/util/syspolicy"
+	_ "tailscale.com/util/syspolicy/pkey"
 	_ "tailscale.com/util/winutil"
 	_ "tailscale.com/util/winutil"
 	_ "tailscale.com/util/winutil/gp"
 	_ "tailscale.com/util/winutil/gp"
 	_ "tailscale.com/version"
 	_ "tailscale.com/version"

+ 5 - 4
util/syspolicy/handler.go

@@ -4,6 +4,7 @@
 package syspolicy
 package syspolicy
 
 
 import (
 import (
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/rsop"
 	"tailscale.com/util/syspolicy/rsop"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/source"
 	"tailscale.com/util/syspolicy/source"
@@ -85,22 +86,22 @@ func (s handlerStore) RegisterChangeCallback(callback func()) (unregister func()
 }
 }
 
 
 // ReadString implements [source.Store].
 // ReadString implements [source.Store].
-func (s handlerStore) ReadString(key setting.Key) (string, error) {
+func (s handlerStore) ReadString(key pkey.Key) (string, error) {
 	return s.h.ReadString(string(key))
 	return s.h.ReadString(string(key))
 }
 }
 
 
 // ReadUInt64 implements [source.Store].
 // ReadUInt64 implements [source.Store].
-func (s handlerStore) ReadUInt64(key setting.Key) (uint64, error) {
+func (s handlerStore) ReadUInt64(key pkey.Key) (uint64, error) {
 	return s.h.ReadUInt64(string(key))
 	return s.h.ReadUInt64(string(key))
 }
 }
 
 
 // ReadBoolean implements [source.Store].
 // ReadBoolean implements [source.Store].
-func (s handlerStore) ReadBoolean(key setting.Key) (bool, error) {
+func (s handlerStore) ReadBoolean(key pkey.Key) (bool, error) {
 	return s.h.ReadBoolean(string(key))
 	return s.h.ReadBoolean(string(key))
 }
 }
 
 
 // ReadStringArray implements [source.Store].
 // ReadStringArray implements [source.Store].
-func (s handlerStore) ReadStringArray(key setting.Key) ([]string, error) {
+func (s handlerStore) ReadStringArray(key pkey.Key) ([]string, error) {
 	return s.h.ReadStringArray(string(key))
 	return s.h.ReadStringArray(string(key))
 }
 }
 
 

+ 4 - 3
util/syspolicy/internal/metrics/metrics.go

@@ -17,6 +17,7 @@ import (
 	"tailscale.com/util/slicesx"
 	"tailscale.com/util/slicesx"
 	"tailscale.com/util/syspolicy/internal"
 	"tailscale.com/util/syspolicy/internal"
 	"tailscale.com/util/syspolicy/internal/loggerx"
 	"tailscale.com/util/syspolicy/internal/loggerx"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/testenv"
 	"tailscale.com/util/testenv"
 )
 )
@@ -209,7 +210,7 @@ func scopeMetrics(origin *setting.Origin) *policyScopeMetrics {
 
 
 var (
 var (
 	settingMetricsMu  sync.RWMutex
 	settingMetricsMu  sync.RWMutex
-	settingMetricsMap map[setting.Key]*settingMetrics
+	settingMetricsMap map[pkey.Key]*settingMetrics
 )
 )
 
 
 func settingMetricsFor(setting *setting.Definition) *settingMetrics {
 func settingMetricsFor(setting *setting.Definition) *settingMetrics {
@@ -283,8 +284,8 @@ func SetHooksForTest(tb testenv.TB, addMetric, setMetric metricFn) {
 	lazyUserMetrics.SetForTest(tb, newScopeMetrics(setting.UserSetting), nil)
 	lazyUserMetrics.SetForTest(tb, newScopeMetrics(setting.UserSetting), nil)
 }
 }
 
 
-func newSettingMetric(key setting.Key, scope setting.Scope, suffix string, typ clientmetric.Type) metric {
-	name := strings.ReplaceAll(string(key), string(setting.KeyPathSeparator), "_")
+func newSettingMetric(key pkey.Key, scope setting.Scope, suffix string, typ clientmetric.Type) metric {
+	name := strings.ReplaceAll(string(key), string(pkey.KeyPathSeparator), "_")
 	name = strings.ReplaceAll(name, ".", "_") // dots are not allowed in metric names
 	name = strings.ReplaceAll(name, ".", "_") // dots are not allowed in metric names
 	return newMetric([]string{name, metricScopeName(scope), suffix}, typ)
 	return newMetric([]string{name, metricScopeName(scope), suffix}, typ)
 }
 }

+ 2 - 1
util/syspolicy/internal/metrics/metrics_test.go

@@ -10,13 +10,14 @@ import (
 	"tailscale.com/types/lazy"
 	"tailscale.com/types/lazy"
 	"tailscale.com/util/clientmetric"
 	"tailscale.com/util/clientmetric"
 	"tailscale.com/util/syspolicy/internal"
 	"tailscale.com/util/syspolicy/internal"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 )
 )
 
 
 func TestSettingMetricNames(t *testing.T) {
 func TestSettingMetricNames(t *testing.T) {
 	tests := []struct {
 	tests := []struct {
 		name           string
 		name           string
-		key            setting.Key
+		key            pkey.Key
 		scope          setting.Scope
 		scope          setting.Scope
 		suffix         string
 		suffix         string
 		typ            clientmetric.Type
 		typ            clientmetric.Type

+ 177 - 0
util/syspolicy/pkey/pkey.go

@@ -0,0 +1,177 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package pkey defines the keys used to store system policies in the registry.
+//
+// This is a leaf package meant to only contain string constants, not code.
+package pkey
+
+// Key is a string that uniquely identifies a policy and must remain unchanged
+// once established and documented for a given policy setting. It may contain
+// alphanumeric characters and zero or more [KeyPathSeparator]s to group
+// individual policy settings into categories.
+type Key string
+
+// KeyPathSeparator allows logical grouping of policy settings into categories.
+const KeyPathSeparator = '/'
+
+// The const block below lists known policy keys.
+// When adding a key to this list, remember to add a corresponding
+// [setting.Definition] to [implicitDefinitions] in util/syspolicy/policy_keys.go.
+// Otherwise, the [TestKnownKeysRegistered] test will fail as a reminder.
+
+const (
+	// Keys with a string value
+	ControlURL Key = "LoginURL"  // default ""; if blank, ipn uses ipn.DefaultControlURL.
+	LogTarget  Key = "LogTarget" // default ""; if blank logging uses logtail.DefaultHost.
+	Tailnet    Key = "Tailnet"   // default ""; if blank, no tailnet name is sent to the server.
+
+	// AlwaysOn is a boolean key that controls whether Tailscale
+	// should always remain in a connected state, and the user should
+	// not be able to disconnect at their discretion.
+	//
+	// Warning: This policy setting is experimental and may change or be removed in the future.
+	// It may also not be fully supported by all Tailscale clients until it is out of experimental status.
+	// See tailscale/corp#26247, tailscale/corp#26248 and tailscale/corp#26249 for more information.
+	AlwaysOn Key = "AlwaysOn.Enabled"
+
+	// AlwaysOnOverrideWithReason is a boolean key that alters the behavior
+	// of [AlwaysOn]. When true, the user is allowed to disconnect Tailscale
+	// by providing a reason. The reason is logged and sent to the control
+	// for auditing purposes. It has no effect when [AlwaysOn] is false.
+	AlwaysOnOverrideWithReason Key = "AlwaysOn.OverrideWithReason"
+
+	// ReconnectAfter is a string value formatted for use with time.ParseDuration()
+	// that defines the duration after which the client should automatically reconnect
+	// to the Tailscale network following a user-initiated disconnect.
+	// An empty string or a zero duration disables automatic reconnection.
+	ReconnectAfter Key = "ReconnectAfter"
+
+	// ExitNodeID is the exit node's node id. default ""; if blank, no exit node is forced.
+	// Exit node ID takes precedence over exit node IP.
+	// To find the node ID, go to /api.md#device.
+	ExitNodeID Key = "ExitNodeID"
+	ExitNodeIP Key = "ExitNodeIP" // default ""; if blank, no exit node is forced. Value is exit node IP.
+
+	// AllowExitNodeOverride is a boolean key that allows the user to override exit node policy settings
+	// and manually select an exit node. It does not allow disabling exit node usage entirely.
+	// It is typically used in conjunction with [ExitNodeID] set to "auto:any".
+	//
+	// Warning: This policy setting is experimental and may change, be renamed or removed in the future.
+	// It may also not be fully supported by all Tailscale clients until it is out of experimental status.
+	// See tailscale/corp#29969.
+	AllowExitNodeOverride Key = "ExitNode.AllowOverride"
+
+	// Keys with a string value that specifies an option: "always", "never", "user-decides".
+	// The default is "user-decides" unless otherwise stated. Enforcement of
+	// these policies is typically performed in ipnlocal.applySysPolicy(). GUIs
+	// typically hide menu items related to policies that are enforced.
+	EnableIncomingConnections Key = "AllowIncomingConnections"
+	EnableServerMode          Key = "UnattendedMode"
+	ExitNodeAllowLANAccess    Key = "ExitNodeAllowLANAccess"
+	EnableTailscaleDNS        Key = "UseTailscaleDNSSettings"
+	EnableTailscaleSubnets    Key = "UseTailscaleSubnets"
+
+	// EnableDNSRegistration is a string value that can be set to "always", "never"
+	// or "user-decides". It controls whether DNS registration and dynamic DNS
+	// updates are enabled for the Tailscale interface. For historical reasons
+	// and to maintain compatibility with existing setups, the default is "never".
+	// It is only used on Windows.
+	EnableDNSRegistration Key = "EnableDNSRegistration"
+
+	// CheckUpdates is the key to signal if the updater should periodically
+	// check for updates.
+	CheckUpdates Key = "CheckUpdates"
+	// ApplyUpdates is the key to signal if updates should be automatically
+	// installed. Its value is "InstallUpdates" because of an awkwardly-named
+	// visibility option "ApplyUpdates" on MacOS.
+	ApplyUpdates Key = "InstallUpdates"
+	// EnableRunExitNode controls if the device acts as an exit node. Even when
+	// running as an exit node, the device must be approved by a tailnet
+	// administrator. Its name is slightly awkward because RunExitNodeVisibility
+	// predates this option but is preserved for backwards compatibility.
+	EnableRunExitNode Key = "AdvertiseExitNode"
+
+	// Keys with a string value that controls visibility: "show", "hide".
+	// The default is "show" unless otherwise stated. Enforcement of these
+	// policies is typically performed by the UI code for the relevant operating
+	// system.
+	AdminConsoleVisibility    Key = "AdminConsole"
+	NetworkDevicesVisibility  Key = "NetworkDevices"
+	TestMenuVisibility        Key = "TestMenu"
+	UpdateMenuVisibility      Key = "UpdateMenu"
+	ResetToDefaultsVisibility Key = "ResetToDefaults"
+	// RunExitNodeVisibility controls if the "run as exit node" menu item is
+	// visible, without controlling the setting itself. This is preserved for
+	// backwards compatibility but prefer EnableRunExitNode in new deployments.
+	RunExitNodeVisibility     Key = "RunExitNode"
+	PreferencesMenuVisibility Key = "PreferencesMenu"
+	ExitNodeMenuVisibility    Key = "ExitNodesPicker"
+	// AutoUpdateVisibility is the key to signal if the menu item for automatic
+	// installation of updates should be visible. It is only used by macsys
+	// installations and uses the Sparkle naming convention, even though it does
+	// not actually control updates, merely the UI for that setting.
+	AutoUpdateVisibility Key = "ApplyUpdates"
+	// SuggestedExitNodeVisibility controls the visibility of suggested exit nodes in the client GUI.
+	// When this system policy is set to 'hide', an exit node suggestion won't be presented to the user as part of the exit nodes picker.
+	SuggestedExitNodeVisibility Key = "SuggestedExitNode"
+	// OnboardingFlowVisibility controls the visibility of the onboarding flow in the client GUI.
+	// When this system policy is set to 'hide', the onboarding flow is never shown to the user.
+	OnboardingFlowVisibility Key = "OnboardingFlow"
+
+	// Keys with a string value formatted for use with time.ParseDuration().
+	KeyExpirationNoticeTime Key = "KeyExpirationNotice" // default 24 hours
+
+	// Boolean Keys that are only applicable on Windows. Booleans are stored in the registry as
+	// DWORD or QWORD (either is acceptable). 0 means false, and anything else means true.
+	// The default is 0 unless otherwise stated.
+	LogSCMInteractions      Key = "LogSCMInteractions"
+	FlushDNSOnSessionUnlock Key = "FlushDNSOnSessionUnlock"
+
+	// EncryptState is a boolean setting that specifies whether to encrypt the
+	// tailscaled state file with a TPM device.
+	EncryptState Key = "EncryptState"
+
+	// PostureChecking indicates if posture checking is enabled and the client shall gather
+	// posture data.
+	// Key is a string value that specifies an option: "always", "never", "user-decides".
+	// The default is "user-decides" unless otherwise stated.
+	PostureChecking Key = "PostureChecking"
+	// DeviceSerialNumber is the serial number of the device that is running Tailscale.
+	// This is used on Android, iOS and tvOS to allow IT administrators to manually give us a serial number via MDM.
+	// We are unable to programmatically get the serial number on mobile due to sandboxing restrictions.
+	DeviceSerialNumber Key = "DeviceSerialNumber"
+
+	// ManagedByOrganizationName indicates the name of the organization managing the Tailscale
+	// install. It is displayed inside the client UI in a prominent location.
+	ManagedByOrganizationName Key = "ManagedByOrganizationName"
+	// ManagedByCaption is an info message displayed inside the client UI as a caption when
+	// ManagedByOrganizationName is set. It can be used to provide a pointer to support resources
+	// for Tailscale within the organization.
+	ManagedByCaption Key = "ManagedByCaption"
+	// ManagedByURL is a valid URL pointing to a support help desk for Tailscale within the
+	// organization. A button in the client UI provides easy access to this URL.
+	ManagedByURL Key = "ManagedByURL"
+
+	// AuthKey is an auth key that will be used to login whenever the backend starts. This can be used to
+	// automatically authenticate managed devices, without requiring user interaction.
+	AuthKey Key = "AuthKey"
+
+	// MachineCertificateSubject is the exact name of a Subject that needs
+	// to be present in an identity's certificate chain to sign a RegisterRequest,
+	// formatted as per pkix.Name.String(). The Subject may be that of the identity
+	// itself, an intermediate CA or the root CA.
+	//
+	// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
+	MachineCertificateSubject Key = "MachineCertificateSubject"
+
+	// Hostname is the hostname of the device that is running Tailscale.
+	// When this policy is set, it overrides the hostname that the client
+	// would otherwise obtain from the OS, e.g. by calling os.Hostname().
+	Hostname Key = "Hostname"
+
+	// Keys with a string array value.
+
+	// AllowedSuggestedExitNodes's string array value is a list of exit node IDs that restricts which exit nodes are considered when generating suggestions for exit nodes.
+	AllowedSuggestedExitNodes Key = "AllowedSuggestedExitNodes"
+)

+ 44 - 209
util/syspolicy/policy_keys.go

@@ -6,225 +6,60 @@ package syspolicy
 import (
 import (
 	"tailscale.com/types/lazy"
 	"tailscale.com/types/lazy"
 	"tailscale.com/util/syspolicy/internal"
 	"tailscale.com/util/syspolicy/internal"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/testenv"
 	"tailscale.com/util/testenv"
 )
 )
 
 
-// Key is a string that uniquely identifies a policy and must remain unchanged
-// once established and documented for a given policy setting. It may contain
-// alphanumeric characters and zero or more [KeyPathSeparator]s to group
-// individual policy settings into categories.
-type Key = setting.Key
-
-// The const block below lists known policy keys.
-// When adding a key to this list, remember to add a corresponding
-// [setting.Definition] to [implicitDefinitions] below.
-// Otherwise, the [TestKnownKeysRegistered] test will fail as a reminder.
-
-const (
-	// Keys with a string value
-	ControlURL Key = "LoginURL"  // default ""; if blank, ipn uses ipn.DefaultControlURL.
-	LogTarget  Key = "LogTarget" // default ""; if blank logging uses logtail.DefaultHost.
-	Tailnet    Key = "Tailnet"   // default ""; if blank, no tailnet name is sent to the server.
-
-	// AlwaysOn is a boolean key that controls whether Tailscale
-	// should always remain in a connected state, and the user should
-	// not be able to disconnect at their discretion.
-	//
-	// Warning: This policy setting is experimental and may change or be removed in the future.
-	// It may also not be fully supported by all Tailscale clients until it is out of experimental status.
-	// See tailscale/corp#26247, tailscale/corp#26248 and tailscale/corp#26249 for more information.
-	AlwaysOn Key = "AlwaysOn.Enabled"
-
-	// AlwaysOnOverrideWithReason is a boolean key that alters the behavior
-	// of [AlwaysOn]. When true, the user is allowed to disconnect Tailscale
-	// by providing a reason. The reason is logged and sent to the control
-	// for auditing purposes. It has no effect when [AlwaysOn] is false.
-	AlwaysOnOverrideWithReason Key = "AlwaysOn.OverrideWithReason"
-
-	// ReconnectAfter is a string value formatted for use with time.ParseDuration()
-	// that defines the duration after which the client should automatically reconnect
-	// to the Tailscale network following a user-initiated disconnect.
-	// An empty string or a zero duration disables automatic reconnection.
-	ReconnectAfter Key = "ReconnectAfter"
-
-	// ExitNodeID is the exit node's node id. default ""; if blank, no exit node is forced.
-	// Exit node ID takes precedence over exit node IP.
-	// To find the node ID, go to /api.md#device.
-	ExitNodeID Key = "ExitNodeID"
-	ExitNodeIP Key = "ExitNodeIP" // default ""; if blank, no exit node is forced. Value is exit node IP.
-
-	// AllowExitNodeOverride is a boolean key that allows the user to override exit node policy settings
-	// and manually select an exit node. It does not allow disabling exit node usage entirely.
-	// It is typically used in conjunction with [ExitNodeID] set to "auto:any".
-	//
-	// Warning: This policy setting is experimental and may change, be renamed or removed in the future.
-	// It may also not be fully supported by all Tailscale clients until it is out of experimental status.
-	// See tailscale/corp#29969.
-	AllowExitNodeOverride Key = "ExitNode.AllowOverride"
-
-	// Keys with a string value that specifies an option: "always", "never", "user-decides".
-	// The default is "user-decides" unless otherwise stated. Enforcement of
-	// these policies is typically performed in ipnlocal.applySysPolicy(). GUIs
-	// typically hide menu items related to policies that are enforced.
-	EnableIncomingConnections Key = "AllowIncomingConnections"
-	EnableServerMode          Key = "UnattendedMode"
-	ExitNodeAllowLANAccess    Key = "ExitNodeAllowLANAccess"
-	EnableTailscaleDNS        Key = "UseTailscaleDNSSettings"
-	EnableTailscaleSubnets    Key = "UseTailscaleSubnets"
-
-	// EnableDNSRegistration is a string value that can be set to "always", "never"
-	// or "user-decides". It controls whether DNS registration and dynamic DNS
-	// updates are enabled for the Tailscale interface. For historical reasons
-	// and to maintain compatibility with existing setups, the default is "never".
-	// It is only used on Windows.
-	EnableDNSRegistration Key = "EnableDNSRegistration"
-
-	// CheckUpdates is the key to signal if the updater should periodically
-	// check for updates.
-	CheckUpdates Key = "CheckUpdates"
-	// ApplyUpdates is the key to signal if updates should be automatically
-	// installed. Its value is "InstallUpdates" because of an awkwardly-named
-	// visibility option "ApplyUpdates" on MacOS.
-	ApplyUpdates Key = "InstallUpdates"
-	// EnableRunExitNode controls if the device acts as an exit node. Even when
-	// running as an exit node, the device must be approved by a tailnet
-	// administrator. Its name is slightly awkward because RunExitNodeVisibility
-	// predates this option but is preserved for backwards compatibility.
-	EnableRunExitNode Key = "AdvertiseExitNode"
-
-	// Keys with a string value that controls visibility: "show", "hide".
-	// The default is "show" unless otherwise stated. Enforcement of these
-	// policies is typically performed by the UI code for the relevant operating
-	// system.
-	AdminConsoleVisibility    Key = "AdminConsole"
-	NetworkDevicesVisibility  Key = "NetworkDevices"
-	TestMenuVisibility        Key = "TestMenu"
-	UpdateMenuVisibility      Key = "UpdateMenu"
-	ResetToDefaultsVisibility Key = "ResetToDefaults"
-	// RunExitNodeVisibility controls if the "run as exit node" menu item is
-	// visible, without controlling the setting itself. This is preserved for
-	// backwards compatibility but prefer EnableRunExitNode in new deployments.
-	RunExitNodeVisibility     Key = "RunExitNode"
-	PreferencesMenuVisibility Key = "PreferencesMenu"
-	ExitNodeMenuVisibility    Key = "ExitNodesPicker"
-	// AutoUpdateVisibility is the key to signal if the menu item for automatic
-	// installation of updates should be visible. It is only used by macsys
-	// installations and uses the Sparkle naming convention, even though it does
-	// not actually control updates, merely the UI for that setting.
-	AutoUpdateVisibility Key = "ApplyUpdates"
-	// SuggestedExitNodeVisibility controls the visibility of suggested exit nodes in the client GUI.
-	// When this system policy is set to 'hide', an exit node suggestion won't be presented to the user as part of the exit nodes picker.
-	SuggestedExitNodeVisibility Key = "SuggestedExitNode"
-	// OnboardingFlowVisibility controls the visibility of the onboarding flow in the client GUI.
-	// When this system policy is set to 'hide', the onboarding flow is never shown to the user.
-	OnboardingFlowVisibility Key = "OnboardingFlow"
-
-	// Keys with a string value formatted for use with time.ParseDuration().
-	KeyExpirationNoticeTime Key = "KeyExpirationNotice" // default 24 hours
-
-	// Boolean Keys that are only applicable on Windows. Booleans are stored in the registry as
-	// DWORD or QWORD (either is acceptable). 0 means false, and anything else means true.
-	// The default is 0 unless otherwise stated.
-	LogSCMInteractions      Key = "LogSCMInteractions"
-	FlushDNSOnSessionUnlock Key = "FlushDNSOnSessionUnlock"
-
-	// EncryptState is a boolean setting that specifies whether to encrypt the
-	// tailscaled state file with a TPM device.
-	EncryptState Key = "EncryptState"
-
-	// PostureChecking indicates if posture checking is enabled and the client shall gather
-	// posture data.
-	// Key is a string value that specifies an option: "always", "never", "user-decides".
-	// The default is "user-decides" unless otherwise stated.
-	PostureChecking Key = "PostureChecking"
-	// DeviceSerialNumber is the serial number of the device that is running Tailscale.
-	// This is used on Android, iOS and tvOS to allow IT administrators to manually give us a serial number via MDM.
-	// We are unable to programmatically get the serial number on mobile due to sandboxing restrictions.
-	DeviceSerialNumber Key = "DeviceSerialNumber"
-
-	// ManagedByOrganizationName indicates the name of the organization managing the Tailscale
-	// install. It is displayed inside the client UI in a prominent location.
-	ManagedByOrganizationName Key = "ManagedByOrganizationName"
-	// ManagedByCaption is an info message displayed inside the client UI as a caption when
-	// ManagedByOrganizationName is set. It can be used to provide a pointer to support resources
-	// for Tailscale within the organization.
-	ManagedByCaption Key = "ManagedByCaption"
-	// ManagedByURL is a valid URL pointing to a support help desk for Tailscale within the
-	// organization. A button in the client UI provides easy access to this URL.
-	ManagedByURL Key = "ManagedByURL"
-
-	// AuthKey is an auth key that will be used to login whenever the backend starts. This can be used to
-	// automatically authenticate managed devices, without requiring user interaction.
-	AuthKey Key = "AuthKey"
-
-	// MachineCertificateSubject is the exact name of a Subject that needs
-	// to be present in an identity's certificate chain to sign a RegisterRequest,
-	// formatted as per pkix.Name.String(). The Subject may be that of the identity
-	// itself, an intermediate CA or the root CA.
-	//
-	// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
-	MachineCertificateSubject Key = "MachineCertificateSubject"
-
-	// Hostname is the hostname of the device that is running Tailscale.
-	// When this policy is set, it overrides the hostname that the client
-	// would otherwise obtain from the OS, e.g. by calling os.Hostname().
-	Hostname Key = "Hostname"
-
-	// Keys with a string array value.
-	// AllowedSuggestedExitNodes's string array value is a list of exit node IDs that restricts which exit nodes are considered when generating suggestions for exit nodes.
-	AllowedSuggestedExitNodes Key = "AllowedSuggestedExitNodes"
-)
-
 // implicitDefinitions is a list of [setting.Definition] that will be registered
 // implicitDefinitions is a list of [setting.Definition] that will be registered
 // automatically when the policy setting definitions are first used by the syspolicy package hierarchy.
 // automatically when the policy setting definitions are first used by the syspolicy package hierarchy.
 // This includes the first time a policy needs to be read from any source.
 // This includes the first time a policy needs to be read from any source.
 var implicitDefinitions = []*setting.Definition{
 var implicitDefinitions = []*setting.Definition{
 	// Device policy settings (can only be configured on a per-device basis):
 	// Device policy settings (can only be configured on a per-device basis):
-	setting.NewDefinition(AllowedSuggestedExitNodes, setting.DeviceSetting, setting.StringListValue),
-	setting.NewDefinition(AllowExitNodeOverride, setting.DeviceSetting, setting.BooleanValue),
-	setting.NewDefinition(AlwaysOn, setting.DeviceSetting, setting.BooleanValue),
-	setting.NewDefinition(AlwaysOnOverrideWithReason, setting.DeviceSetting, setting.BooleanValue),
-	setting.NewDefinition(ApplyUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
-	setting.NewDefinition(AuthKey, setting.DeviceSetting, setting.StringValue),
-	setting.NewDefinition(CheckUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
-	setting.NewDefinition(ControlURL, setting.DeviceSetting, setting.StringValue),
-	setting.NewDefinition(DeviceSerialNumber, setting.DeviceSetting, setting.StringValue),
-	setting.NewDefinition(EnableDNSRegistration, setting.DeviceSetting, setting.PreferenceOptionValue),
-	setting.NewDefinition(EnableIncomingConnections, setting.DeviceSetting, setting.PreferenceOptionValue),
-	setting.NewDefinition(EnableRunExitNode, setting.DeviceSetting, setting.PreferenceOptionValue),
-	setting.NewDefinition(EnableServerMode, setting.DeviceSetting, setting.PreferenceOptionValue),
-	setting.NewDefinition(EnableTailscaleDNS, setting.DeviceSetting, setting.PreferenceOptionValue),
-	setting.NewDefinition(EnableTailscaleSubnets, setting.DeviceSetting, setting.PreferenceOptionValue),
-	setting.NewDefinition(ExitNodeAllowLANAccess, setting.DeviceSetting, setting.PreferenceOptionValue),
-	setting.NewDefinition(ExitNodeID, setting.DeviceSetting, setting.StringValue),
-	setting.NewDefinition(ExitNodeIP, setting.DeviceSetting, setting.StringValue),
-	setting.NewDefinition(FlushDNSOnSessionUnlock, setting.DeviceSetting, setting.BooleanValue),
-	setting.NewDefinition(EncryptState, setting.DeviceSetting, setting.BooleanValue),
-	setting.NewDefinition(Hostname, setting.DeviceSetting, setting.StringValue),
-	setting.NewDefinition(LogSCMInteractions, setting.DeviceSetting, setting.BooleanValue),
-	setting.NewDefinition(LogTarget, setting.DeviceSetting, setting.StringValue),
-	setting.NewDefinition(MachineCertificateSubject, setting.DeviceSetting, setting.StringValue),
-	setting.NewDefinition(PostureChecking, setting.DeviceSetting, setting.PreferenceOptionValue),
-	setting.NewDefinition(ReconnectAfter, setting.DeviceSetting, setting.DurationValue),
-	setting.NewDefinition(Tailnet, setting.DeviceSetting, setting.StringValue),
+	setting.NewDefinition(pkey.AllowedSuggestedExitNodes, setting.DeviceSetting, setting.StringListValue),
+	setting.NewDefinition(pkey.AllowExitNodeOverride, setting.DeviceSetting, setting.BooleanValue),
+	setting.NewDefinition(pkey.AlwaysOn, setting.DeviceSetting, setting.BooleanValue),
+	setting.NewDefinition(pkey.AlwaysOnOverrideWithReason, setting.DeviceSetting, setting.BooleanValue),
+	setting.NewDefinition(pkey.ApplyUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
+	setting.NewDefinition(pkey.AuthKey, setting.DeviceSetting, setting.StringValue),
+	setting.NewDefinition(pkey.CheckUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
+	setting.NewDefinition(pkey.ControlURL, setting.DeviceSetting, setting.StringValue),
+	setting.NewDefinition(pkey.DeviceSerialNumber, setting.DeviceSetting, setting.StringValue),
+	setting.NewDefinition(pkey.EnableDNSRegistration, setting.DeviceSetting, setting.PreferenceOptionValue),
+	setting.NewDefinition(pkey.EnableIncomingConnections, setting.DeviceSetting, setting.PreferenceOptionValue),
+	setting.NewDefinition(pkey.EnableRunExitNode, setting.DeviceSetting, setting.PreferenceOptionValue),
+	setting.NewDefinition(pkey.EnableServerMode, setting.DeviceSetting, setting.PreferenceOptionValue),
+	setting.NewDefinition(pkey.EnableTailscaleDNS, setting.DeviceSetting, setting.PreferenceOptionValue),
+	setting.NewDefinition(pkey.EnableTailscaleSubnets, setting.DeviceSetting, setting.PreferenceOptionValue),
+	setting.NewDefinition(pkey.ExitNodeAllowLANAccess, setting.DeviceSetting, setting.PreferenceOptionValue),
+	setting.NewDefinition(pkey.ExitNodeID, setting.DeviceSetting, setting.StringValue),
+	setting.NewDefinition(pkey.ExitNodeIP, setting.DeviceSetting, setting.StringValue),
+	setting.NewDefinition(pkey.FlushDNSOnSessionUnlock, setting.DeviceSetting, setting.BooleanValue),
+	setting.NewDefinition(pkey.EncryptState, setting.DeviceSetting, setting.BooleanValue),
+	setting.NewDefinition(pkey.Hostname, setting.DeviceSetting, setting.StringValue),
+	setting.NewDefinition(pkey.LogSCMInteractions, setting.DeviceSetting, setting.BooleanValue),
+	setting.NewDefinition(pkey.LogTarget, setting.DeviceSetting, setting.StringValue),
+	setting.NewDefinition(pkey.MachineCertificateSubject, setting.DeviceSetting, setting.StringValue),
+	setting.NewDefinition(pkey.PostureChecking, setting.DeviceSetting, setting.PreferenceOptionValue),
+	setting.NewDefinition(pkey.ReconnectAfter, setting.DeviceSetting, setting.DurationValue),
+	setting.NewDefinition(pkey.Tailnet, setting.DeviceSetting, setting.StringValue),
 
 
 	// User policy settings (can be configured on a user- or device-basis):
 	// User policy settings (can be configured on a user- or device-basis):
-	setting.NewDefinition(AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
-	setting.NewDefinition(AutoUpdateVisibility, setting.UserSetting, setting.VisibilityValue),
-	setting.NewDefinition(ExitNodeMenuVisibility, setting.UserSetting, setting.VisibilityValue),
-	setting.NewDefinition(KeyExpirationNoticeTime, setting.UserSetting, setting.DurationValue),
-	setting.NewDefinition(ManagedByCaption, setting.UserSetting, setting.StringValue),
-	setting.NewDefinition(ManagedByOrganizationName, setting.UserSetting, setting.StringValue),
-	setting.NewDefinition(ManagedByURL, setting.UserSetting, setting.StringValue),
-	setting.NewDefinition(NetworkDevicesVisibility, setting.UserSetting, setting.VisibilityValue),
-	setting.NewDefinition(PreferencesMenuVisibility, setting.UserSetting, setting.VisibilityValue),
-	setting.NewDefinition(ResetToDefaultsVisibility, setting.UserSetting, setting.VisibilityValue),
-	setting.NewDefinition(RunExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
-	setting.NewDefinition(SuggestedExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
-	setting.NewDefinition(TestMenuVisibility, setting.UserSetting, setting.VisibilityValue),
-	setting.NewDefinition(UpdateMenuVisibility, setting.UserSetting, setting.VisibilityValue),
-	setting.NewDefinition(OnboardingFlowVisibility, setting.UserSetting, setting.VisibilityValue),
+	setting.NewDefinition(pkey.AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
+	setting.NewDefinition(pkey.AutoUpdateVisibility, setting.UserSetting, setting.VisibilityValue),
+	setting.NewDefinition(pkey.ExitNodeMenuVisibility, setting.UserSetting, setting.VisibilityValue),
+	setting.NewDefinition(pkey.KeyExpirationNoticeTime, setting.UserSetting, setting.DurationValue),
+	setting.NewDefinition(pkey.ManagedByCaption, setting.UserSetting, setting.StringValue),
+	setting.NewDefinition(pkey.ManagedByOrganizationName, setting.UserSetting, setting.StringValue),
+	setting.NewDefinition(pkey.ManagedByURL, setting.UserSetting, setting.StringValue),
+	setting.NewDefinition(pkey.NetworkDevicesVisibility, setting.UserSetting, setting.VisibilityValue),
+	setting.NewDefinition(pkey.PreferencesMenuVisibility, setting.UserSetting, setting.VisibilityValue),
+	setting.NewDefinition(pkey.ResetToDefaultsVisibility, setting.UserSetting, setting.VisibilityValue),
+	setting.NewDefinition(pkey.RunExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
+	setting.NewDefinition(pkey.SuggestedExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
+	setting.NewDefinition(pkey.TestMenuVisibility, setting.UserSetting, setting.VisibilityValue),
+	setting.NewDefinition(pkey.UpdateMenuVisibility, setting.UserSetting, setting.VisibilityValue),
+	setting.NewDefinition(pkey.OnboardingFlowVisibility, setting.UserSetting, setting.VisibilityValue),
 }
 }
 
 
 func init() {
 func init() {
@@ -248,7 +83,7 @@ var implicitDefinitionMap lazy.SyncValue[setting.DefinitionMap]
 // WellKnownSettingDefinition returns a well-known, implicit setting definition by its key,
 // WellKnownSettingDefinition returns a well-known, implicit setting definition by its key,
 // or an [ErrNoSuchKey] if a policy setting with the specified key does not exist
 // or an [ErrNoSuchKey] if a policy setting with the specified key does not exist
 // among implicit policy definitions.
 // among implicit policy definitions.
-func WellKnownSettingDefinition(k Key) (*setting.Definition, error) {
+func WellKnownSettingDefinition(k pkey.Key) (*setting.Definition, error) {
 	m, err := implicitDefinitionMap.GetErr(func() (setting.DefinitionMap, error) {
 	m, err := implicitDefinitionMap.GetErr(func() (setting.DefinitionMap, error) {
 		return setting.DefinitionMapOf(implicitDefinitions)
 		return setting.DefinitionMapOf(implicitDefinitions)
 	})
 	})

+ 6 - 1
util/syspolicy/policy_keys_test.go

@@ -14,14 +14,19 @@ import (
 	"strconv"
 	"strconv"
 	"testing"
 	"testing"
 
 
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 )
 )
 
 
 func TestKnownKeysRegistered(t *testing.T) {
 func TestKnownKeysRegistered(t *testing.T) {
-	keyConsts, err := listStringConsts[Key]("policy_keys.go")
+	const file = "pkey/pkey.go"
+	keyConsts, err := listStringConsts[pkey.Key](file)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("listStringConsts failed: %v", err)
 		t.Fatalf("listStringConsts failed: %v", err)
 	}
 	}
+	if len(keyConsts) == 0 {
+		t.Fatalf("no key constants found in %s", file)
+	}
 
 
 	m, err := setting.DefinitionMapOf(implicitDefinitions)
 	m, err := setting.DefinitionMapOf(implicitDefinitions)
 	if err != nil {
 	if err != nil {

+ 4 - 3
util/syspolicy/rsop/change_callbacks.go

@@ -11,6 +11,7 @@ import (
 
 
 	"tailscale.com/util/set"
 	"tailscale.com/util/set"
 	"tailscale.com/util/syspolicy/internal/loggerx"
 	"tailscale.com/util/syspolicy/internal/loggerx"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 )
 )
 
 
@@ -37,8 +38,8 @@ func (c PolicyChange) Old() *setting.Snapshot {
 	return c.snapshots.Old
 	return c.snapshots.Old
 }
 }
 
 
-// HasChanged reports whether a policy setting with the specified [setting.Key], has changed.
-func (c PolicyChange) HasChanged(key setting.Key) bool {
+// HasChanged reports whether a policy setting with the specified [pkey.Key], has changed.
+func (c PolicyChange) HasChanged(key pkey.Key) bool {
 	new, newErr := c.snapshots.New.GetErr(key)
 	new, newErr := c.snapshots.New.GetErr(key)
 	old, oldErr := c.snapshots.Old.GetErr(key)
 	old, oldErr := c.snapshots.Old.GetErr(key)
 	if newErr != nil && oldErr != nil {
 	if newErr != nil && oldErr != nil {
@@ -60,7 +61,7 @@ func (c PolicyChange) HasChanged(key setting.Key) bool {
 }
 }
 
 
 // HasChangedAnyOf reports whether any of the specified policy settings has changed.
 // HasChangedAnyOf reports whether any of the specified policy settings has changed.
-func (c PolicyChange) HasChangedAnyOf(keys ...setting.Key) bool {
+func (c PolicyChange) HasChangedAnyOf(keys ...pkey.Key) bool {
 	return slices.ContainsFunc(keys, c.HasChanged)
 	return slices.ContainsFunc(keys, c.HasChanged)
 }
 }
 
 

+ 30 - 29
util/syspolicy/rsop/resultant_policy_test.go

@@ -15,6 +15,7 @@ import (
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"tailscale.com/tstest"
 	"tailscale.com/tstest"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 
 
 	"tailscale.com/util/syspolicy/source"
 	"tailscale.com/util/syspolicy/source"
@@ -80,7 +81,7 @@ func TestRegisterSourceAndGetEffectivePolicy(t *testing.T) {
 	type sourceConfig struct {
 	type sourceConfig struct {
 		name          string
 		name          string
 		scope         setting.PolicyScope
 		scope         setting.PolicyScope
-		settingKey    setting.Key
+		settingKey    pkey.Key
 		settingValue  string
 		settingValue  string
 		wantEffective bool
 		wantEffective bool
 	}
 	}
@@ -113,7 +114,7 @@ func TestRegisterSourceAndGetEffectivePolicy(t *testing.T) {
 					wantEffective: true,
 					wantEffective: true,
 				},
 				},
 			},
 			},
-			wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{
+			wantSnapshot: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 				"TestKeyA": setting.RawItemWith("TestValueA", nil, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)),
 				"TestKeyA": setting.RawItemWith("TestValueA", nil, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)),
 			}, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)),
 			}, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)),
 		},
 		},
@@ -129,7 +130,7 @@ func TestRegisterSourceAndGetEffectivePolicy(t *testing.T) {
 					wantEffective: true,
 					wantEffective: true,
 				},
 				},
 			},
 			},
-			wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{
+			wantSnapshot: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 				"TestKeyA": setting.RawItemWith("TestValueA", nil, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)),
 				"TestKeyA": setting.RawItemWith("TestValueA", nil, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)),
 			}, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)),
 			}, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)),
 		},
 		},
@@ -159,7 +160,7 @@ func TestRegisterSourceAndGetEffectivePolicy(t *testing.T) {
 					wantEffective: true,
 					wantEffective: true,
 				},
 				},
 			},
 			},
-			wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{
+			wantSnapshot: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 				"TestKeyA": setting.RawItemWith("TestValueA", nil, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)),
 				"TestKeyA": setting.RawItemWith("TestValueA", nil, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)),
 				"TestKeyB": setting.RawItemWith("TestValueB", nil, setting.NewNamedOrigin("TestSourceB", setting.DeviceScope)),
 				"TestKeyB": setting.RawItemWith("TestValueB", nil, setting.NewNamedOrigin("TestSourceB", setting.DeviceScope)),
 				"TestKeyC": setting.RawItemWith("TestValueC", nil, setting.NewNamedOrigin("TestSourceC", setting.DeviceScope)),
 				"TestKeyC": setting.RawItemWith("TestValueC", nil, setting.NewNamedOrigin("TestSourceC", setting.DeviceScope)),
@@ -191,7 +192,7 @@ func TestRegisterSourceAndGetEffectivePolicy(t *testing.T) {
 					wantEffective: true,
 					wantEffective: true,
 				},
 				},
 			},
 			},
-			wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{
+			wantSnapshot: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 				"TestKeyA": setting.RawItemWith("TestValueC", nil, setting.NewNamedOrigin("TestSourceC", setting.DeviceScope)),
 				"TestKeyA": setting.RawItemWith("TestValueC", nil, setting.NewNamedOrigin("TestSourceC", setting.DeviceScope)),
 				"TestKeyB": setting.RawItemWith("TestValueB", nil, setting.NewNamedOrigin("TestSourceB", setting.DeviceScope)),
 				"TestKeyB": setting.RawItemWith("TestValueB", nil, setting.NewNamedOrigin("TestSourceB", setting.DeviceScope)),
 			}, setting.DeviceScope),
 			}, setting.DeviceScope),
@@ -245,7 +246,7 @@ func TestRegisterSourceAndGetEffectivePolicy(t *testing.T) {
 					wantEffective: true,
 					wantEffective: true,
 				},
 				},
 			},
 			},
-			wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{
+			wantSnapshot: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 				"TestKeyA": setting.RawItemWith("TestValueF", nil, setting.NewNamedOrigin("TestSourceF", setting.DeviceScope)),
 				"TestKeyA": setting.RawItemWith("TestValueF", nil, setting.NewNamedOrigin("TestSourceF", setting.DeviceScope)),
 				"TestKeyB": setting.RawItemWith("TestValueB", nil, setting.NewNamedOrigin("TestSourceB", setting.DeviceScope)),
 				"TestKeyB": setting.RawItemWith("TestValueB", nil, setting.NewNamedOrigin("TestSourceB", setting.DeviceScope)),
 				"TestKeyC": setting.RawItemWith("TestValueE", nil, setting.NewNamedOrigin("TestSourceE", setting.DeviceScope)),
 				"TestKeyC": setting.RawItemWith("TestValueE", nil, setting.NewNamedOrigin("TestSourceE", setting.DeviceScope)),
@@ -263,7 +264,7 @@ func TestRegisterSourceAndGetEffectivePolicy(t *testing.T) {
 					wantEffective: true,
 					wantEffective: true,
 				},
 				},
 			},
 			},
-			wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{
+			wantSnapshot: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 				"TestKeyA": setting.RawItemWith("DeviceValue", nil, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)),
 				"TestKeyA": setting.RawItemWith("DeviceValue", nil, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)),
 			}, setting.CurrentUserScope, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)),
 			}, setting.CurrentUserScope, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)),
 		},
 		},
@@ -288,7 +289,7 @@ func TestRegisterSourceAndGetEffectivePolicy(t *testing.T) {
 					wantEffective: true,
 					wantEffective: true,
 				},
 				},
 			},
 			},
-			wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{
+			wantSnapshot: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 				"TestKeyA": setting.RawItemWith("DeviceValue", nil, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)),
 				"TestKeyA": setting.RawItemWith("DeviceValue", nil, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)),
 				"TestKeyB": setting.RawItemWith("UserValue", nil, setting.NewNamedOrigin("TestSourceUser", setting.CurrentUserScope)),
 				"TestKeyB": setting.RawItemWith("UserValue", nil, setting.NewNamedOrigin("TestSourceUser", setting.CurrentUserScope)),
 			}, setting.CurrentUserScope),
 			}, setting.CurrentUserScope),
@@ -321,7 +322,7 @@ func TestRegisterSourceAndGetEffectivePolicy(t *testing.T) {
 					wantEffective: true,
 					wantEffective: true,
 				},
 				},
 			},
 			},
-			wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{
+			wantSnapshot: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 				"TestKeyA": setting.RawItemWith("DeviceValue", nil, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)),
 				"TestKeyA": setting.RawItemWith("DeviceValue", nil, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)),
 				"TestKeyB": setting.RawItemWith("ProfileValue", nil, setting.NewNamedOrigin("TestSourceProfile", setting.CurrentProfileScope)),
 				"TestKeyB": setting.RawItemWith("ProfileValue", nil, setting.NewNamedOrigin("TestSourceProfile", setting.CurrentProfileScope)),
 			}, setting.CurrentUserScope),
 			}, setting.CurrentUserScope),
@@ -347,7 +348,7 @@ func TestRegisterSourceAndGetEffectivePolicy(t *testing.T) {
 					wantEffective: false, // Registering a user source should have no impact on the device policy.
 					wantEffective: false, // Registering a user source should have no impact on the device policy.
 				},
 				},
 			},
 			},
-			wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{
+			wantSnapshot: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 				"TestKeyA": setting.RawItemWith("DeviceValue", nil, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)),
 				"TestKeyA": setting.RawItemWith("DeviceValue", nil, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)),
 			}, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)),
 			}, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)),
 		},
 		},
@@ -497,61 +498,61 @@ func TestPolicyFor(t *testing.T) {
 func TestPolicyChangeHasChanged(t *testing.T) {
 func TestPolicyChangeHasChanged(t *testing.T) {
 	tests := []struct {
 	tests := []struct {
 		name          string
 		name          string
-		old, new      map[setting.Key]setting.RawItem
-		wantChanged   []setting.Key
-		wantUnchanged []setting.Key
+		old, new      map[pkey.Key]setting.RawItem
+		wantChanged   []pkey.Key
+		wantUnchanged []pkey.Key
 	}{
 	}{
 		{
 		{
 			name: "String-Settings",
 			name: "String-Settings",
-			old: map[setting.Key]setting.RawItem{
+			old: map[pkey.Key]setting.RawItem{
 				"ChangedSetting":   setting.RawItemOf("Old"),
 				"ChangedSetting":   setting.RawItemOf("Old"),
 				"UnchangedSetting": setting.RawItemOf("Value"),
 				"UnchangedSetting": setting.RawItemOf("Value"),
 			},
 			},
-			new: map[setting.Key]setting.RawItem{
+			new: map[pkey.Key]setting.RawItem{
 				"ChangedSetting":   setting.RawItemOf("New"),
 				"ChangedSetting":   setting.RawItemOf("New"),
 				"UnchangedSetting": setting.RawItemOf("Value"),
 				"UnchangedSetting": setting.RawItemOf("Value"),
 			},
 			},
-			wantChanged:   []setting.Key{"ChangedSetting"},
-			wantUnchanged: []setting.Key{"UnchangedSetting"},
+			wantChanged:   []pkey.Key{"ChangedSetting"},
+			wantUnchanged: []pkey.Key{"UnchangedSetting"},
 		},
 		},
 		{
 		{
 			name: "UInt64-Settings",
 			name: "UInt64-Settings",
-			old: map[setting.Key]setting.RawItem{
+			old: map[pkey.Key]setting.RawItem{
 				"ChangedSetting":   setting.RawItemOf(uint64(0)),
 				"ChangedSetting":   setting.RawItemOf(uint64(0)),
 				"UnchangedSetting": setting.RawItemOf(uint64(42)),
 				"UnchangedSetting": setting.RawItemOf(uint64(42)),
 			},
 			},
-			new: map[setting.Key]setting.RawItem{
+			new: map[pkey.Key]setting.RawItem{
 				"ChangedSetting":   setting.RawItemOf(uint64(1)),
 				"ChangedSetting":   setting.RawItemOf(uint64(1)),
 				"UnchangedSetting": setting.RawItemOf(uint64(42)),
 				"UnchangedSetting": setting.RawItemOf(uint64(42)),
 			},
 			},
-			wantChanged:   []setting.Key{"ChangedSetting"},
-			wantUnchanged: []setting.Key{"UnchangedSetting"},
+			wantChanged:   []pkey.Key{"ChangedSetting"},
+			wantUnchanged: []pkey.Key{"UnchangedSetting"},
 		},
 		},
 		{
 		{
 			name: "StringSlice-Settings",
 			name: "StringSlice-Settings",
-			old: map[setting.Key]setting.RawItem{
+			old: map[pkey.Key]setting.RawItem{
 				"ChangedSetting":   setting.RawItemOf([]string{"Chicago"}),
 				"ChangedSetting":   setting.RawItemOf([]string{"Chicago"}),
 				"UnchangedSetting": setting.RawItemOf([]string{"String1", "String2"}),
 				"UnchangedSetting": setting.RawItemOf([]string{"String1", "String2"}),
 			},
 			},
-			new: map[setting.Key]setting.RawItem{
+			new: map[pkey.Key]setting.RawItem{
 				"ChangedSetting":   setting.RawItemOf([]string{"New York"}),
 				"ChangedSetting":   setting.RawItemOf([]string{"New York"}),
 				"UnchangedSetting": setting.RawItemOf([]string{"String1", "String2"}),
 				"UnchangedSetting": setting.RawItemOf([]string{"String1", "String2"}),
 			},
 			},
-			wantChanged:   []setting.Key{"ChangedSetting"},
-			wantUnchanged: []setting.Key{"UnchangedSetting"},
+			wantChanged:   []pkey.Key{"ChangedSetting"},
+			wantUnchanged: []pkey.Key{"UnchangedSetting"},
 		},
 		},
 		{
 		{
 			name: "Int8-Settings", // We don't have actual int8 settings, but this should still work.
 			name: "Int8-Settings", // We don't have actual int8 settings, but this should still work.
-			old: map[setting.Key]setting.RawItem{
+			old: map[pkey.Key]setting.RawItem{
 				"ChangedSetting":   setting.RawItemOf(int8(0)),
 				"ChangedSetting":   setting.RawItemOf(int8(0)),
 				"UnchangedSetting": setting.RawItemOf(int8(42)),
 				"UnchangedSetting": setting.RawItemOf(int8(42)),
 			},
 			},
-			new: map[setting.Key]setting.RawItem{
+			new: map[pkey.Key]setting.RawItem{
 				"ChangedSetting":   setting.RawItemOf(int8(1)),
 				"ChangedSetting":   setting.RawItemOf(int8(1)),
 				"UnchangedSetting": setting.RawItemOf(int8(42)),
 				"UnchangedSetting": setting.RawItemOf(int8(42)),
 			},
 			},
-			wantChanged:   []setting.Key{"ChangedSetting"},
-			wantUnchanged: []setting.Key{"UnchangedSetting"},
+			wantChanged:   []pkey.Key{"ChangedSetting"},
+			wantUnchanged: []pkey.Key{"UnchangedSetting"},
 		},
 		},
 	}
 	}
 	for _, tt := range tests {
 	for _, tt := range tests {

+ 0 - 13
util/syspolicy/setting/key.go

@@ -1,13 +0,0 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package setting
-
-// Key is a string that uniquely identifies a policy and must remain unchanged
-// once established and documented for a given policy setting. It may contain
-// alphanumeric characters and zero or more [KeyPathSeparator]s to group
-// individual policy settings into categories.
-type Key string
-
-// KeyPathSeparator allows logical grouping of policy settings into categories.
-const KeyPathSeparator = '/'

+ 2 - 1
util/syspolicy/setting/raw_item.go

@@ -11,6 +11,7 @@ import (
 	"github.com/go-json-experiment/json/jsontext"
 	"github.com/go-json-experiment/json/jsontext"
 	"tailscale.com/types/opt"
 	"tailscale.com/types/opt"
 	"tailscale.com/types/structs"
 	"tailscale.com/types/structs"
+	"tailscale.com/util/syspolicy/pkey"
 )
 )
 
 
 // RawItem contains a raw policy setting value as read from a policy store, or an
 // RawItem contains a raw policy setting value as read from a policy store, or an
@@ -169,4 +170,4 @@ func (v *RawValue) UnmarshalJSON(b []byte) error {
 }
 }
 
 
 // RawValues is a map of keyed setting values that can be read from a JSON.
 // RawValues is a map of keyed setting values that can be read from a JSON.
-type RawValues map[Key]RawValue
+type RawValues map[pkey.Key]RawValue

+ 7 - 6
util/syspolicy/setting/setting.go

@@ -16,6 +16,7 @@ import (
 
 
 	"tailscale.com/types/lazy"
 	"tailscale.com/types/lazy"
 	"tailscale.com/util/syspolicy/internal"
 	"tailscale.com/util/syspolicy/internal"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/testenv"
 	"tailscale.com/util/testenv"
 )
 )
 
 
@@ -134,7 +135,7 @@ type ValueType interface {
 
 
 // Definition defines policy key, scope and value type.
 // Definition defines policy key, scope and value type.
 type Definition struct {
 type Definition struct {
-	key       Key
+	key       pkey.Key
 	scope     Scope
 	scope     Scope
 	typ       Type
 	typ       Type
 	platforms PlatformList
 	platforms PlatformList
@@ -142,12 +143,12 @@ type Definition struct {
 
 
 // NewDefinition returns a new [Definition] with the specified
 // NewDefinition returns a new [Definition] with the specified
 // key, scope, type and supported platforms (see [PlatformList]).
 // key, scope, type and supported platforms (see [PlatformList]).
-func NewDefinition(k Key, s Scope, t Type, platforms ...string) *Definition {
+func NewDefinition(k pkey.Key, s Scope, t Type, platforms ...string) *Definition {
 	return &Definition{key: k, scope: s, typ: t, platforms: platforms}
 	return &Definition{key: k, scope: s, typ: t, platforms: platforms}
 }
 }
 
 
 // Key returns a policy setting's identifier.
 // Key returns a policy setting's identifier.
-func (d *Definition) Key() Key {
+func (d *Definition) Key() pkey.Key {
 	if d == nil {
 	if d == nil {
 		return ""
 		return ""
 	}
 	}
@@ -208,7 +209,7 @@ func (d *Definition) Equal(d2 *Definition) bool {
 }
 }
 
 
 // DefinitionMap is a map of setting [Definition] by [Key].
 // DefinitionMap is a map of setting [Definition] by [Key].
-type DefinitionMap map[Key]*Definition
+type DefinitionMap map[pkey.Key]*Definition
 
 
 var (
 var (
 	definitions lazy.SyncValue[DefinitionMap]
 	definitions lazy.SyncValue[DefinitionMap]
@@ -224,7 +225,7 @@ var (
 // invoking any functions that use the registered policy definitions. This
 // invoking any functions that use the registered policy definitions. This
 // includes calling [Definitions] or [DefinitionOf] directly, or reading any
 // includes calling [Definitions] or [DefinitionOf] directly, or reading any
 // policy settings via syspolicy.
 // policy settings via syspolicy.
-func Register(k Key, s Scope, t Type, platforms ...string) {
+func Register(k pkey.Key, s Scope, t Type, platforms ...string) {
 	RegisterDefinition(NewDefinition(k, s, t, platforms...))
 	RegisterDefinition(NewDefinition(k, s, t, platforms...))
 }
 }
 
 
@@ -290,7 +291,7 @@ func SetDefinitionsForTest(tb testenv.TB, ds ...*Definition) error {
 // DefinitionOf returns a setting definition by key,
 // DefinitionOf returns a setting definition by key,
 // or [ErrNoSuchKey] if the specified key does not exist,
 // or [ErrNoSuchKey] if the specified key does not exist,
 // or an error if there are conflicting policy definitions.
 // or an error if there are conflicting policy definitions.
-func DefinitionOf(k Key) (*Definition, error) {
+func DefinitionOf(k pkey.Key) (*Definition, error) {
 	ds, err := settingDefinitions()
 	ds, err := settingDefinitions()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err

+ 4 - 3
util/syspolicy/setting/setting_test.go

@@ -11,6 +11,7 @@ import (
 	"tailscale.com/types/lazy"
 	"tailscale.com/types/lazy"
 	"tailscale.com/types/ptr"
 	"tailscale.com/types/ptr"
 	"tailscale.com/util/syspolicy/internal"
 	"tailscale.com/util/syspolicy/internal"
+	"tailscale.com/util/syspolicy/pkey"
 )
 )
 
 
 func TestSettingDefinition(t *testing.T) {
 func TestSettingDefinition(t *testing.T) {
@@ -18,7 +19,7 @@ func TestSettingDefinition(t *testing.T) {
 		name                   string
 		name                   string
 		setting                *Definition
 		setting                *Definition
 		osOverride             string
 		osOverride             string
-		wantKey                Key
+		wantKey                pkey.Key
 		wantScope              Scope
 		wantScope              Scope
 		wantType               Type
 		wantType               Type
 		wantIsSupported        bool
 		wantIsSupported        bool
@@ -163,10 +164,10 @@ func TestSettingDefinition(t *testing.T) {
 }
 }
 
 
 func TestRegisterSettingDefinition(t *testing.T) {
 func TestRegisterSettingDefinition(t *testing.T) {
-	const testPolicySettingKey Key = "TestPolicySetting"
+	const testPolicySettingKey pkey.Key = "TestPolicySetting"
 	tests := []struct {
 	tests := []struct {
 		name    string
 		name    string
-		key     Key
+		key     pkey.Key
 		wantEq  *Definition
 		wantEq  *Definition
 		wantErr error
 		wantErr error
 	}{
 	}{

+ 13 - 12
util/syspolicy/setting/snapshot.go

@@ -15,34 +15,35 @@ import (
 	"github.com/go-json-experiment/json/jsontext"
 	"github.com/go-json-experiment/json/jsontext"
 	xmaps "golang.org/x/exp/maps"
 	xmaps "golang.org/x/exp/maps"
 	"tailscale.com/util/deephash"
 	"tailscale.com/util/deephash"
+	"tailscale.com/util/syspolicy/pkey"
 )
 )
 
 
 // Snapshot is an immutable collection of ([Key], [RawItem]) pairs, representing
 // Snapshot is an immutable collection of ([Key], [RawItem]) pairs, representing
 // a set of policy settings applied at a specific moment in time.
 // a set of policy settings applied at a specific moment in time.
 // A nil pointer to [Snapshot] is valid.
 // A nil pointer to [Snapshot] is valid.
 type Snapshot struct {
 type Snapshot struct {
-	m       map[Key]RawItem
+	m       map[pkey.Key]RawItem
 	sig     deephash.Sum // of m
 	sig     deephash.Sum // of m
 	summary Summary
 	summary Summary
 }
 }
 
 
 // NewSnapshot returns a new [Snapshot] with the specified items and options.
 // NewSnapshot returns a new [Snapshot] with the specified items and options.
-func NewSnapshot(items map[Key]RawItem, opts ...SummaryOption) *Snapshot {
+func NewSnapshot(items map[pkey.Key]RawItem, opts ...SummaryOption) *Snapshot {
 	return &Snapshot{m: xmaps.Clone(items), sig: deephash.Hash(&items), summary: SummaryWith(opts...)}
 	return &Snapshot{m: xmaps.Clone(items), sig: deephash.Hash(&items), summary: SummaryWith(opts...)}
 }
 }
 
 
 // All returns an iterator over policy settings in s. The iteration order is not
 // All returns an iterator over policy settings in s. The iteration order is not
 // specified and is not guaranteed to be the same from one call to the next.
 // specified and is not guaranteed to be the same from one call to the next.
-func (s *Snapshot) All() iter.Seq2[Key, RawItem] {
+func (s *Snapshot) All() iter.Seq2[pkey.Key, RawItem] {
 	if s == nil {
 	if s == nil {
-		return func(yield func(Key, RawItem) bool) {}
+		return func(yield func(pkey.Key, RawItem) bool) {}
 	}
 	}
 	return maps.All(s.m)
 	return maps.All(s.m)
 }
 }
 
 
 // Get returns the value of the policy setting with the specified key
 // Get returns the value of the policy setting with the specified key
 // or nil if it is not configured or has an error.
 // or nil if it is not configured or has an error.
-func (s *Snapshot) Get(k Key) any {
+func (s *Snapshot) Get(k pkey.Key) any {
 	v, _ := s.GetErr(k)
 	v, _ := s.GetErr(k)
 	return v
 	return v
 }
 }
@@ -50,7 +51,7 @@ func (s *Snapshot) Get(k Key) any {
 // GetErr returns the value of the policy setting with the specified key,
 // GetErr returns the value of the policy setting with the specified key,
 // [ErrNotConfigured] if it is not configured, or an error returned by
 // [ErrNotConfigured] if it is not configured, or an error returned by
 // the policy Store if the policy setting could not be read.
 // the policy Store if the policy setting could not be read.
-func (s *Snapshot) GetErr(k Key) (any, error) {
+func (s *Snapshot) GetErr(k pkey.Key) (any, error) {
 	if s != nil {
 	if s != nil {
 		if s, ok := s.m[k]; ok {
 		if s, ok := s.m[k]; ok {
 			return s.Value(), s.Error()
 			return s.Value(), s.Error()
@@ -62,7 +63,7 @@ func (s *Snapshot) GetErr(k Key) (any, error) {
 // GetSetting returns the untyped policy setting with the specified key and true
 // GetSetting returns the untyped policy setting with the specified key and true
 // if a policy setting with such key has been configured;
 // if a policy setting with such key has been configured;
 // otherwise, it returns zero, false.
 // otherwise, it returns zero, false.
-func (s *Snapshot) GetSetting(k Key) (setting RawItem, ok bool) {
+func (s *Snapshot) GetSetting(k pkey.Key) (setting RawItem, ok bool) {
 	setting, ok = s.m[k]
 	setting, ok = s.m[k]
 	return setting, ok
 	return setting, ok
 }
 }
@@ -94,9 +95,9 @@ func (s *Snapshot) EqualItems(s2 *Snapshot) bool {
 
 
 // Keys return an iterator over keys in s. The iteration order is not specified
 // Keys return an iterator over keys in s. The iteration order is not specified
 // and is not guaranteed to be the same from one call to the next.
 // and is not guaranteed to be the same from one call to the next.
-func (s *Snapshot) Keys() iter.Seq[Key] {
+func (s *Snapshot) Keys() iter.Seq[pkey.Key] {
 	if s.m == nil {
 	if s.m == nil {
-		return func(yield func(Key) bool) {}
+		return func(yield func(pkey.Key) bool) {}
 	}
 	}
 	return maps.Keys(s.m)
 	return maps.Keys(s.m)
 }
 }
@@ -144,8 +145,8 @@ func (s *Snapshot) String() string {
 
 
 // snapshotJSON holds JSON-marshallable data for [Snapshot].
 // snapshotJSON holds JSON-marshallable data for [Snapshot].
 type snapshotJSON struct {
 type snapshotJSON struct {
-	Summary  Summary         `json:",omitzero"`
-	Settings map[Key]RawItem `json:",omitempty"`
+	Summary  Summary              `json:",omitzero"`
+	Settings map[pkey.Key]RawItem `json:",omitempty"`
 }
 }
 
 
 var (
 var (
@@ -232,7 +233,7 @@ func MergeSnapshots(snapshot1, snapshot2 *Snapshot) *Snapshot {
 		}
 		}
 		return &Snapshot{snapshot2.m, snapshot2.sig, SummaryWith(summaryOpts...)}
 		return &Snapshot{snapshot2.m, snapshot2.sig, SummaryWith(summaryOpts...)}
 	}
 	}
-	m := make(map[Key]RawItem, snapshot1.Len()+snapshot2.Len())
+	m := make(map[pkey.Key]RawItem, snapshot1.Len()+snapshot2.Len())
 	xmaps.Copy(m, snapshot1.m)
 	xmaps.Copy(m, snapshot1.m)
 	xmaps.Copy(m, snapshot2.m) // snapshot2 has higher precedence
 	xmaps.Copy(m, snapshot2.m) // snapshot2 has higher precedence
 	return &Snapshot{m, deephash.Hash(&m), SummaryWith(summaryOpts...)}
 	return &Snapshot{m, deephash.Hash(&m), SummaryWith(summaryOpts...)}

+ 73 - 72
util/syspolicy/setting/snapshot_test.go

@@ -11,6 +11,7 @@ import (
 
 
 	jsonv2 "github.com/go-json-experiment/json"
 	jsonv2 "github.com/go-json-experiment/json"
 	"tailscale.com/util/syspolicy/internal"
 	"tailscale.com/util/syspolicy/internal"
+	"tailscale.com/util/syspolicy/pkey"
 )
 )
 
 
 func TestMergeSnapshots(t *testing.T) {
 func TestMergeSnapshots(t *testing.T) {
@@ -23,23 +24,23 @@ func TestMergeSnapshots(t *testing.T) {
 			name: "both-nil",
 			name: "both-nil",
 			s1:   nil,
 			s1:   nil,
 			s2:   nil,
 			s2:   nil,
-			want: NewSnapshot(map[Key]RawItem{}),
+			want: NewSnapshot(map[pkey.Key]RawItem{}),
 		},
 		},
 		{
 		{
 			name: "both-empty",
 			name: "both-empty",
-			s1:   NewSnapshot(map[Key]RawItem{}),
-			s2:   NewSnapshot(map[Key]RawItem{}),
-			want: NewSnapshot(map[Key]RawItem{}),
+			s1:   NewSnapshot(map[pkey.Key]RawItem{}),
+			s2:   NewSnapshot(map[pkey.Key]RawItem{}),
+			want: NewSnapshot(map[pkey.Key]RawItem{}),
 		},
 		},
 		{
 		{
 			name: "first-nil",
 			name: "first-nil",
 			s1:   nil,
 			s1:   nil,
-			s2: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true),
 				"Setting3": RawItemOf(true),
 			}),
 			}),
-			want: NewSnapshot(map[Key]RawItem{
+			want: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true),
 				"Setting3": RawItemOf(true),
@@ -47,13 +48,13 @@ func TestMergeSnapshots(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "first-empty",
 			name: "first-empty",
-			s1:   NewSnapshot(map[Key]RawItem{}),
-			s2: NewSnapshot(map[Key]RawItem{
+			s1:   NewSnapshot(map[pkey.Key]RawItem{}),
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 			}),
 			}),
-			want: NewSnapshot(map[Key]RawItem{
+			want: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
@@ -61,13 +62,13 @@ func TestMergeSnapshots(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "second-nil",
 			name: "second-nil",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true),
 				"Setting3": RawItemOf(true),
 			}),
 			}),
 			s2: nil,
 			s2: nil,
-			want: NewSnapshot(map[Key]RawItem{
+			want: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true),
 				"Setting3": RawItemOf(true),
@@ -75,13 +76,13 @@ func TestMergeSnapshots(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "second-empty",
 			name: "second-empty",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 			}),
 			}),
-			s2: NewSnapshot(map[Key]RawItem{}),
-			want: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{}),
+			want: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
@@ -89,17 +90,17 @@ func TestMergeSnapshots(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "no-conflicts",
 			name: "no-conflicts",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 			}),
 			}),
-			s2: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting4": RawItemOf(2 * time.Hour),
 				"Setting4": RawItemOf(2 * time.Hour),
 				"Setting5": RawItemOf(VisibleByPolicy),
 				"Setting5": RawItemOf(VisibleByPolicy),
 				"Setting6": RawItemOf(ShowChoiceByPolicy),
 				"Setting6": RawItemOf(ShowChoiceByPolicy),
 			}),
 			}),
-			want: NewSnapshot(map[Key]RawItem{
+			want: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
@@ -110,17 +111,17 @@ func TestMergeSnapshots(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "with-conflicts",
 			name: "with-conflicts",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true),
 				"Setting3": RawItemOf(true),
 			}),
 			}),
-			s2: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(456),
 				"Setting1": RawItemOf(456),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 				"Setting4": RawItemOf(2 * time.Hour),
 				"Setting4": RawItemOf(2 * time.Hour),
 			}),
 			}),
-			want: NewSnapshot(map[Key]RawItem{
+			want: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(456),
 				"Setting1": RawItemOf(456),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
@@ -129,17 +130,17 @@ func TestMergeSnapshots(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "with-scope-first-wins",
 			name: "with-scope-first-wins",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true),
 				"Setting3": RawItemOf(true),
 			}, DeviceScope),
 			}, DeviceScope),
-			s2: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(456),
 				"Setting1": RawItemOf(456),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 				"Setting4": RawItemOf(2 * time.Hour),
 				"Setting4": RawItemOf(2 * time.Hour),
 			}, CurrentUserScope),
 			}, CurrentUserScope),
-			want: NewSnapshot(map[Key]RawItem{
+			want: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true),
 				"Setting3": RawItemOf(true),
@@ -148,17 +149,17 @@ func TestMergeSnapshots(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "with-scope-second-wins",
 			name: "with-scope-second-wins",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true),
 				"Setting3": RawItemOf(true),
 			}, CurrentUserScope),
 			}, CurrentUserScope),
-			s2: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(456),
 				"Setting1": RawItemOf(456),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 				"Setting4": RawItemOf(2 * time.Hour),
 				"Setting4": RawItemOf(2 * time.Hour),
 			}, DeviceScope),
 			}, DeviceScope),
-			want: NewSnapshot(map[Key]RawItem{
+			want: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(456),
 				"Setting1": RawItemOf(456),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
@@ -167,18 +168,18 @@ func TestMergeSnapshots(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "with-scope-both-empty",
 			name: "with-scope-both-empty",
-			s1:   NewSnapshot(map[Key]RawItem{}, CurrentUserScope),
-			s2:   NewSnapshot(map[Key]RawItem{}, DeviceScope),
-			want: NewSnapshot(map[Key]RawItem{}, CurrentUserScope),
+			s1:   NewSnapshot(map[pkey.Key]RawItem{}, CurrentUserScope),
+			s2:   NewSnapshot(map[pkey.Key]RawItem{}, DeviceScope),
+			want: NewSnapshot(map[pkey.Key]RawItem{}, CurrentUserScope),
 		},
 		},
 		{
 		{
 			name: "with-scope-first-empty",
 			name: "with-scope-first-empty",
-			s1:   NewSnapshot(map[Key]RawItem{}, CurrentUserScope),
-			s2: NewSnapshot(map[Key]RawItem{
+			s1:   NewSnapshot(map[pkey.Key]RawItem{}, CurrentUserScope),
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true)}, DeviceScope, NewNamedOrigin("TestPolicy", DeviceScope)),
 				"Setting3": RawItemOf(true)}, DeviceScope, NewNamedOrigin("TestPolicy", DeviceScope)),
-			want: NewSnapshot(map[Key]RawItem{
+			want: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true),
 				"Setting3": RawItemOf(true),
@@ -186,13 +187,13 @@ func TestMergeSnapshots(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "with-scope-second-empty",
 			name: "with-scope-second-empty",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true),
 				"Setting3": RawItemOf(true),
 			}, CurrentUserScope),
 			}, CurrentUserScope),
-			s2: NewSnapshot(map[Key]RawItem{}),
-			want: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{}),
+			want: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true),
 				"Setting3": RawItemOf(true),
@@ -226,28 +227,28 @@ func TestSnapshotEqual(t *testing.T) {
 		{
 		{
 			name:           "nil-empty",
 			name:           "nil-empty",
 			s1:             nil,
 			s1:             nil,
-			s2:             NewSnapshot(map[Key]RawItem{}),
+			s2:             NewSnapshot(map[pkey.Key]RawItem{}),
 			wantEqual:      true,
 			wantEqual:      true,
 			wantEqualItems: true,
 			wantEqualItems: true,
 		},
 		},
 		{
 		{
 			name:           "empty-nil",
 			name:           "empty-nil",
-			s1:             NewSnapshot(map[Key]RawItem{}),
+			s1:             NewSnapshot(map[pkey.Key]RawItem{}),
 			s2:             nil,
 			s2:             nil,
 			wantEqual:      true,
 			wantEqual:      true,
 			wantEqualItems: true,
 			wantEqualItems: true,
 		},
 		},
 		{
 		{
 			name:           "empty-empty",
 			name:           "empty-empty",
-			s1:             NewSnapshot(map[Key]RawItem{}),
-			s2:             NewSnapshot(map[Key]RawItem{}),
+			s1:             NewSnapshot(map[pkey.Key]RawItem{}),
+			s2:             NewSnapshot(map[pkey.Key]RawItem{}),
 			wantEqual:      true,
 			wantEqual:      true,
 			wantEqualItems: true,
 			wantEqualItems: true,
 		},
 		},
 		{
 		{
 			name: "first-nil",
 			name: "first-nil",
 			s1:   nil,
 			s1:   nil,
-			s2: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
@@ -257,8 +258,8 @@ func TestSnapshotEqual(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "first-empty",
 			name: "first-empty",
-			s1:   NewSnapshot(map[Key]RawItem{}),
-			s2: NewSnapshot(map[Key]RawItem{
+			s1:   NewSnapshot(map[pkey.Key]RawItem{}),
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
@@ -268,7 +269,7 @@ func TestSnapshotEqual(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "second-nil",
 			name: "second-nil",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(true),
 				"Setting3": RawItemOf(true),
@@ -279,23 +280,23 @@ func TestSnapshotEqual(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "second-empty",
 			name: "second-empty",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 			}),
 			}),
-			s2:             NewSnapshot(map[Key]RawItem{}),
+			s2:             NewSnapshot(map[pkey.Key]RawItem{}),
 			wantEqual:      false,
 			wantEqual:      false,
 			wantEqualItems: false,
 			wantEqualItems: false,
 		},
 		},
 		{
 		{
 			name: "same-items-same-order-no-scope",
 			name: "same-items-same-order-no-scope",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 			}),
 			}),
-			s2: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
@@ -305,12 +306,12 @@ func TestSnapshotEqual(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "same-items-same-order-same-scope",
 			name: "same-items-same-order-same-scope",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 			}, DeviceScope),
 			}, DeviceScope),
-			s2: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
@@ -320,12 +321,12 @@ func TestSnapshotEqual(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "same-items-different-order-same-scope",
 			name: "same-items-different-order-same-scope",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 			}, DeviceScope),
 			}, DeviceScope),
-			s2: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
@@ -335,12 +336,12 @@ func TestSnapshotEqual(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "same-items-same-order-different-scope",
 			name: "same-items-same-order-different-scope",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 			}, DeviceScope),
 			}, DeviceScope),
-			s2: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
@@ -350,12 +351,12 @@ func TestSnapshotEqual(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "different-items-same-scope",
 			name: "different-items-same-scope",
-			s1: NewSnapshot(map[Key]RawItem{
+			s1: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(123),
 				"Setting1": RawItemOf(123),
 				"Setting2": RawItemOf("String"),
 				"Setting2": RawItemOf("String"),
 				"Setting3": RawItemOf(false),
 				"Setting3": RawItemOf(false),
 			}, DeviceScope),
 			}, DeviceScope),
-			s2: NewSnapshot(map[Key]RawItem{
+			s2: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting4": RawItemOf(2 * time.Hour),
 				"Setting4": RawItemOf(2 * time.Hour),
 				"Setting5": RawItemOf(VisibleByPolicy),
 				"Setting5": RawItemOf(VisibleByPolicy),
 				"Setting6": RawItemOf(ShowChoiceByPolicy),
 				"Setting6": RawItemOf(ShowChoiceByPolicy),
@@ -404,7 +405,7 @@ func TestSnapshotString(t *testing.T) {
 		},
 		},
 		{
 		{
 			name: "non-empty",
 			name: "non-empty",
-			snapshot: NewSnapshot(map[Key]RawItem{
+			snapshot: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemOf(2 * time.Hour),
 				"Setting1": RawItemOf(2 * time.Hour),
 				"Setting2": RawItemOf(VisibleByPolicy),
 				"Setting2": RawItemOf(VisibleByPolicy),
 				"Setting3": RawItemOf(ShowChoiceByPolicy),
 				"Setting3": RawItemOf(ShowChoiceByPolicy),
@@ -416,14 +417,14 @@ Setting3 = user-decides`,
 		},
 		},
 		{
 		{
 			name: "non-empty-with-item-origin",
 			name: "non-empty-with-item-origin",
-			snapshot: NewSnapshot(map[Key]RawItem{
+			snapshot: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemWith(42, nil, NewNamedOrigin("Test Policy", DeviceScope)),
 				"Setting1": RawItemWith(42, nil, NewNamedOrigin("Test Policy", DeviceScope)),
 			}),
 			}),
 			wantString: `Setting1 = 42 - {Test Policy (Device)}`,
 			wantString: `Setting1 = 42 - {Test Policy (Device)}`,
 		},
 		},
 		{
 		{
 			name: "non-empty-with-item-error",
 			name: "non-empty-with-item-error",
-			snapshot: NewSnapshot(map[Key]RawItem{
+			snapshot: NewSnapshot(map[pkey.Key]RawItem{
 				"Setting1": RawItemWith(nil, NewErrorText("bang!"), nil),
 				"Setting1": RawItemWith(nil, NewErrorText("bang!"), nil),
 			}),
 			}),
 			wantString: `Setting1 = Error{"bang!"}`,
 			wantString: `Setting1 = Error{"bang!"}`,
@@ -458,55 +459,55 @@ func TestMarshalUnmarshalSnapshot(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:     "Bool/True",
 			name:     "Bool/True",
-			snapshot: NewSnapshot(map[Key]RawItem{"BoolPolicy": RawItemOf(true)}),
+			snapshot: NewSnapshot(map[pkey.Key]RawItem{"BoolPolicy": RawItemOf(true)}),
 			wantJSON: `{"Settings": {"BoolPolicy": {"Value": true}}}`,
 			wantJSON: `{"Settings": {"BoolPolicy": {"Value": true}}}`,
 		},
 		},
 		{
 		{
 			name:     "Bool/False",
 			name:     "Bool/False",
-			snapshot: NewSnapshot(map[Key]RawItem{"BoolPolicy": RawItemOf(false)}),
+			snapshot: NewSnapshot(map[pkey.Key]RawItem{"BoolPolicy": RawItemOf(false)}),
 			wantJSON: `{"Settings": {"BoolPolicy": {"Value": false}}}`,
 			wantJSON: `{"Settings": {"BoolPolicy": {"Value": false}}}`,
 		},
 		},
 		{
 		{
 			name:     "String/Non-Empty",
 			name:     "String/Non-Empty",
-			snapshot: NewSnapshot(map[Key]RawItem{"StringPolicy": RawItemOf("StringValue")}),
+			snapshot: NewSnapshot(map[pkey.Key]RawItem{"StringPolicy": RawItemOf("StringValue")}),
 			wantJSON: `{"Settings": {"StringPolicy": {"Value": "StringValue"}}}`,
 			wantJSON: `{"Settings": {"StringPolicy": {"Value": "StringValue"}}}`,
 		},
 		},
 		{
 		{
 			name:     "String/Empty",
 			name:     "String/Empty",
-			snapshot: NewSnapshot(map[Key]RawItem{"StringPolicy": RawItemOf("")}),
+			snapshot: NewSnapshot(map[pkey.Key]RawItem{"StringPolicy": RawItemOf("")}),
 			wantJSON: `{"Settings": {"StringPolicy": {"Value": ""}}}`,
 			wantJSON: `{"Settings": {"StringPolicy": {"Value": ""}}}`,
 		},
 		},
 		{
 		{
 			name:     "Integer/NonZero",
 			name:     "Integer/NonZero",
-			snapshot: NewSnapshot(map[Key]RawItem{"IntPolicy": RawItemOf(uint64(42))}),
+			snapshot: NewSnapshot(map[pkey.Key]RawItem{"IntPolicy": RawItemOf(uint64(42))}),
 			wantJSON: `{"Settings": {"IntPolicy": {"Value": 42}}}`,
 			wantJSON: `{"Settings": {"IntPolicy": {"Value": 42}}}`,
 		},
 		},
 		{
 		{
 			name:     "Integer/Zero",
 			name:     "Integer/Zero",
-			snapshot: NewSnapshot(map[Key]RawItem{"IntPolicy": RawItemOf(uint64(0))}),
+			snapshot: NewSnapshot(map[pkey.Key]RawItem{"IntPolicy": RawItemOf(uint64(0))}),
 			wantJSON: `{"Settings": {"IntPolicy": {"Value": 0}}}`,
 			wantJSON: `{"Settings": {"IntPolicy": {"Value": 0}}}`,
 		},
 		},
 		{
 		{
 			name:     "String-List",
 			name:     "String-List",
-			snapshot: NewSnapshot(map[Key]RawItem{"ListPolicy": RawItemOf([]string{"Value1", "Value2"})}),
+			snapshot: NewSnapshot(map[pkey.Key]RawItem{"ListPolicy": RawItemOf([]string{"Value1", "Value2"})}),
 			wantJSON: `{"Settings": {"ListPolicy": {"Value": ["Value1", "Value2"]}}}`,
 			wantJSON: `{"Settings": {"ListPolicy": {"Value": ["Value1", "Value2"]}}}`,
 		},
 		},
 		{
 		{
 			name:     "Duration/Zero",
 			name:     "Duration/Zero",
-			snapshot: NewSnapshot(map[Key]RawItem{"DurationPolicy": RawItemOf(time.Duration(0))}),
+			snapshot: NewSnapshot(map[pkey.Key]RawItem{"DurationPolicy": RawItemOf(time.Duration(0))}),
 			wantJSON: `{"Settings": {"DurationPolicy": {"Value": "0s"}}}`,
 			wantJSON: `{"Settings": {"DurationPolicy": {"Value": "0s"}}}`,
-			wantBack: NewSnapshot(map[Key]RawItem{"DurationPolicy": RawItemOf("0s")}),
+			wantBack: NewSnapshot(map[pkey.Key]RawItem{"DurationPolicy": RawItemOf("0s")}),
 		},
 		},
 		{
 		{
 			name:     "Duration/NonZero",
 			name:     "Duration/NonZero",
-			snapshot: NewSnapshot(map[Key]RawItem{"DurationPolicy": RawItemOf(2 * time.Hour)}),
+			snapshot: NewSnapshot(map[pkey.Key]RawItem{"DurationPolicy": RawItemOf(2 * time.Hour)}),
 			wantJSON: `{"Settings": {"DurationPolicy": {"Value": "2h0m0s"}}}`,
 			wantJSON: `{"Settings": {"DurationPolicy": {"Value": "2h0m0s"}}}`,
-			wantBack: NewSnapshot(map[Key]RawItem{"DurationPolicy": RawItemOf("2h0m0s")}),
+			wantBack: NewSnapshot(map[pkey.Key]RawItem{"DurationPolicy": RawItemOf("2h0m0s")}),
 		},
 		},
 		{
 		{
 			name: "Empty/With-Summary",
 			name: "Empty/With-Summary",
 			snapshot: NewSnapshot(
 			snapshot: NewSnapshot(
-				map[Key]RawItem{},
+				map[pkey.Key]RawItem{},
 				SummaryWith(CurrentUserScope, NewNamedOrigin("TestSource", DeviceScope)),
 				SummaryWith(CurrentUserScope, NewNamedOrigin("TestSource", DeviceScope)),
 			),
 			),
 			wantJSON: `{"Summary": {"Origin": {"Name": "TestSource", "Scope": "Device"}, "Scope": "User"}}`,
 			wantJSON: `{"Summary": {"Origin": {"Name": "TestSource", "Scope": "Device"}, "Scope": "User"}}`,
@@ -514,7 +515,7 @@ func TestMarshalUnmarshalSnapshot(t *testing.T) {
 		{
 		{
 			name: "Setting/With-Summary",
 			name: "Setting/With-Summary",
 			snapshot: NewSnapshot(
 			snapshot: NewSnapshot(
-				map[Key]RawItem{"PolicySetting": RawItemOf(uint64(42))},
+				map[pkey.Key]RawItem{"PolicySetting": RawItemOf(uint64(42))},
 				SummaryWith(CurrentUserScope, NewNamedOrigin("TestSource", DeviceScope)),
 				SummaryWith(CurrentUserScope, NewNamedOrigin("TestSource", DeviceScope)),
 			),
 			),
 			wantJSON: `{
 			wantJSON: `{
@@ -525,7 +526,7 @@ func TestMarshalUnmarshalSnapshot(t *testing.T) {
 		{
 		{
 			name: "Settings/With-Origins",
 			name: "Settings/With-Origins",
 			snapshot: NewSnapshot(
 			snapshot: NewSnapshot(
-				map[Key]RawItem{
+				map[pkey.Key]RawItem{
 					"SettingA": RawItemWith(uint64(42), nil, NewNamedOrigin("SourceA", DeviceScope)),
 					"SettingA": RawItemWith(uint64(42), nil, NewNamedOrigin("SourceA", DeviceScope)),
 					"SettingB": RawItemWith("B", nil, NewNamedOrigin("SourceB", CurrentProfileScope)),
 					"SettingB": RawItemWith("B", nil, NewNamedOrigin("SourceB", CurrentProfileScope)),
 					"SettingC": RawItemWith(true, nil, NewNamedOrigin("SourceC", CurrentUserScope)),
 					"SettingC": RawItemWith(true, nil, NewNamedOrigin("SourceC", CurrentUserScope)),

+ 8 - 7
util/syspolicy/source/env_policy_store.go

@@ -11,6 +11,7 @@ import (
 	"strings"
 	"strings"
 	"unicode/utf8"
 	"unicode/utf8"
 
 
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 )
 )
 
 
@@ -22,7 +23,7 @@ var _ Store = (*EnvPolicyStore)(nil)
 type EnvPolicyStore struct{}
 type EnvPolicyStore struct{}
 
 
 // ReadString implements [Store].
 // ReadString implements [Store].
-func (s *EnvPolicyStore) ReadString(key setting.Key) (string, error) {
+func (s *EnvPolicyStore) ReadString(key pkey.Key) (string, error) {
 	_, str, err := s.lookupSettingVariable(key)
 	_, str, err := s.lookupSettingVariable(key)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
@@ -31,7 +32,7 @@ func (s *EnvPolicyStore) ReadString(key setting.Key) (string, error) {
 }
 }
 
 
 // ReadUInt64 implements [Store].
 // ReadUInt64 implements [Store].
-func (s *EnvPolicyStore) ReadUInt64(key setting.Key) (uint64, error) {
+func (s *EnvPolicyStore) ReadUInt64(key pkey.Key) (uint64, error) {
 	name, str, err := s.lookupSettingVariable(key)
 	name, str, err := s.lookupSettingVariable(key)
 	if err != nil {
 	if err != nil {
 		return 0, err
 		return 0, err
@@ -47,7 +48,7 @@ func (s *EnvPolicyStore) ReadUInt64(key setting.Key) (uint64, error) {
 }
 }
 
 
 // ReadBoolean implements [Store].
 // ReadBoolean implements [Store].
-func (s *EnvPolicyStore) ReadBoolean(key setting.Key) (bool, error) {
+func (s *EnvPolicyStore) ReadBoolean(key pkey.Key) (bool, error) {
 	name, str, err := s.lookupSettingVariable(key)
 	name, str, err := s.lookupSettingVariable(key)
 	if err != nil {
 	if err != nil {
 		return false, err
 		return false, err
@@ -63,7 +64,7 @@ func (s *EnvPolicyStore) ReadBoolean(key setting.Key) (bool, error) {
 }
 }
 
 
 // ReadStringArray implements [Store].
 // ReadStringArray implements [Store].
-func (s *EnvPolicyStore) ReadStringArray(key setting.Key) ([]string, error) {
+func (s *EnvPolicyStore) ReadStringArray(key pkey.Key) ([]string, error) {
 	_, str, err := s.lookupSettingVariable(key)
 	_, str, err := s.lookupSettingVariable(key)
 	if err != nil || str == "" {
 	if err != nil || str == "" {
 		return nil, err
 		return nil, err
@@ -79,7 +80,7 @@ func (s *EnvPolicyStore) ReadStringArray(key setting.Key) ([]string, error) {
 	return res[0:dst], nil
 	return res[0:dst], nil
 }
 }
 
 
-func (s *EnvPolicyStore) lookupSettingVariable(key setting.Key) (name, value string, err error) {
+func (s *EnvPolicyStore) lookupSettingVariable(key pkey.Key) (name, value string, err error) {
 	name, err = keyToEnvVarName(key)
 	name, err = keyToEnvVarName(key)
 	if err != nil {
 	if err != nil {
 		return "", "", err
 		return "", "", err
@@ -103,7 +104,7 @@ var (
 //
 //
 // It's fine to use this in [EnvPolicyStore] without caching variable names since it's not a hot path.
 // It's fine to use this in [EnvPolicyStore] without caching variable names since it's not a hot path.
 // [EnvPolicyStore] is not a [Changeable] policy store, so the conversion will only happen once.
 // [EnvPolicyStore] is not a [Changeable] policy store, so the conversion will only happen once.
-func keyToEnvVarName(key setting.Key) (string, error) {
+func keyToEnvVarName(key pkey.Key) (string, error) {
 	if len(key) == 0 {
 	if len(key) == 0 {
 		return "", errEmptyKey
 		return "", errEmptyKey
 	}
 	}
@@ -135,7 +136,7 @@ func keyToEnvVarName(key setting.Key) (string, error) {
 			}
 			}
 		case isDigit(c):
 		case isDigit(c):
 			split = currentWord.Len() > 0 && !isDigit(key[i-1])
 			split = currentWord.Len() > 0 && !isDigit(key[i-1])
-		case c == setting.KeyPathSeparator:
+		case c == pkey.KeyPathSeparator:
 			words = append(words, currentWord.String())
 			words = append(words, currentWord.String())
 			currentWord.Reset()
 			currentWord.Reset()
 			continue
 			continue

+ 3 - 2
util/syspolicy/source/env_policy_store_test.go

@@ -11,13 +11,14 @@ import (
 	"strconv"
 	"strconv"
 	"testing"
 	"testing"
 
 
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 )
 )
 
 
 func TestKeyToEnvVarName(t *testing.T) {
 func TestKeyToEnvVarName(t *testing.T) {
 	tests := []struct {
 	tests := []struct {
 		name    string
 		name    string
-		key     setting.Key
+		key     pkey.Key
 		want    string // suffix after "TS_DEBUGSYSPOLICY_"
 		want    string // suffix after "TS_DEBUGSYSPOLICY_"
 		wantErr error
 		wantErr error
 	}{
 	}{
@@ -166,7 +167,7 @@ func TestEnvPolicyStore(t *testing.T) {
 	}
 	}
 	tests := []struct {
 	tests := []struct {
 		name    string
 		name    string
-		key     setting.Key
+		key     pkey.Key
 		lookup  func(string) (string, bool)
 		lookup  func(string) (string, bool)
 		want    any
 		want    any
 		wantErr error
 		wantErr error

+ 3 - 2
util/syspolicy/source/policy_reader.go

@@ -16,6 +16,7 @@ import (
 	"tailscale.com/util/set"
 	"tailscale.com/util/set"
 	"tailscale.com/util/syspolicy/internal/loggerx"
 	"tailscale.com/util/syspolicy/internal/loggerx"
 	"tailscale.com/util/syspolicy/internal/metrics"
 	"tailscale.com/util/syspolicy/internal/metrics"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 )
 )
 
 
@@ -138,9 +139,9 @@ func (r *Reader) reload(force bool) (*setting.Snapshot, error) {
 
 
 	metrics.Reset(r.origin)
 	metrics.Reset(r.origin)
 
 
-	var m map[setting.Key]setting.RawItem
+	var m map[pkey.Key]setting.RawItem
 	if lastPolicyCount := r.lastPolicy.Len(); lastPolicyCount > 0 {
 	if lastPolicyCount := r.lastPolicy.Len(); lastPolicyCount > 0 {
-		m = make(map[setting.Key]setting.RawItem, lastPolicyCount)
+		m = make(map[pkey.Key]setting.RawItem, lastPolicyCount)
 	}
 	}
 	for _, s := range r.settings {
 	for _, s := range r.settings {
 		if !r.origin.Scope().IsConfigurableSetting(s) {
 		if !r.origin.Scope().IsConfigurableSetting(s) {

+ 5 - 4
util/syspolicy/source/policy_reader_test.go

@@ -9,6 +9,7 @@ import (
 	"time"
 	"time"
 
 
 	"tailscale.com/util/must"
 	"tailscale.com/util/must"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 )
 )
 
 
@@ -72,7 +73,7 @@ func TestReaderLifecycle(t *testing.T) {
 			initWant:       setting.NewSnapshot(nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
 			initWant:       setting.NewSnapshot(nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
 			addStrings:     []TestSetting[string]{TestSettingOf("StringValue", "S1")},
 			addStrings:     []TestSetting[string]{TestSettingOf("StringValue", "S1")},
 			addStringLists: []TestSetting[[]string]{TestSettingOf("StringListValue", []string{"S1", "S2", "S3"})},
 			addStringLists: []TestSetting[[]string]{TestSettingOf("StringListValue", []string{"S1", "S2", "S3"})},
-			newWant: setting.NewSnapshot(map[setting.Key]setting.RawItem{
+			newWant: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 				"StringValue":     setting.RawItemWith("S1", nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
 				"StringValue":     setting.RawItemWith("S1", nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
 				"StringListValue": setting.RawItemWith([]string{"S1", "S2", "S3"}, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
 				"StringListValue": setting.RawItemWith([]string{"S1", "S2", "S3"}, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
 			}, setting.NewNamedOrigin("Test", setting.DeviceScope)),
 			}, setting.NewNamedOrigin("Test", setting.DeviceScope)),
@@ -136,7 +137,7 @@ func TestReaderLifecycle(t *testing.T) {
 				TestSettingOf("PreferenceOptionValue", "always"),
 				TestSettingOf("PreferenceOptionValue", "always"),
 				TestSettingOf("VisibilityValue", "show"),
 				TestSettingOf("VisibilityValue", "show"),
 			},
 			},
-			initWant: setting.NewSnapshot(map[setting.Key]setting.RawItem{
+			initWant: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 				"DurationValue":         setting.RawItemWith(must.Get(time.ParseDuration("2h30m")), nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
 				"DurationValue":         setting.RawItemWith(must.Get(time.ParseDuration("2h30m")), nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
 				"PreferenceOptionValue": setting.RawItemWith(setting.AlwaysByPolicy, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
 				"PreferenceOptionValue": setting.RawItemWith(setting.AlwaysByPolicy, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
 				"VisibilityValue":       setting.RawItemWith(setting.VisibleByPolicy, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
 				"VisibilityValue":       setting.RawItemWith(setting.VisibleByPolicy, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
@@ -165,7 +166,7 @@ func TestReaderLifecycle(t *testing.T) {
 			initUInt64s: []TestSetting[uint64]{
 			initUInt64s: []TestSetting[uint64]{
 				TestSettingOf[uint64]("VisibilityValue", 42), // type mismatch
 				TestSettingOf[uint64]("VisibilityValue", 42), // type mismatch
 			},
 			},
-			initWant: setting.NewSnapshot(map[setting.Key]setting.RawItem{
+			initWant: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 				"DurationValue1":        setting.RawItemWith(nil, setting.NewErrorText("time: invalid duration \"soon\""), setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
 				"DurationValue1":        setting.RawItemWith(nil, setting.NewErrorText("time: invalid duration \"soon\""), setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
 				"DurationValue2":        setting.RawItemWith(nil, setting.NewErrorText("bang!"), setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
 				"DurationValue2":        setting.RawItemWith(nil, setting.NewErrorText("bang!"), setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
 				"PreferenceOptionValue": setting.RawItemWith(setting.ShowChoiceByPolicy, nil, setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
 				"PreferenceOptionValue": setting.RawItemWith(setting.ShowChoiceByPolicy, nil, setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
@@ -277,7 +278,7 @@ func TestReadingSession(t *testing.T) {
 		t.Fatalf("the session was closed prematurely")
 		t.Fatalf("the session was closed prematurely")
 	}
 	}
 
 
-	want := setting.NewSnapshot(map[setting.Key]setting.RawItem{
+	want := setting.NewSnapshot(map[pkey.Key]setting.RawItem{
 		"StringValue": setting.RawItemWith("S1", nil, origin),
 		"StringValue": setting.RawItemWith("S1", nil, origin),
 	}, origin)
 	}, origin)
 	if got := session.GetSettings(); !got.Equal(want) {
 	if got := session.GetSettings(); !got.Equal(want) {

+ 5 - 4
util/syspolicy/source/policy_source.go

@@ -13,6 +13,7 @@ import (
 	"io"
 	"io"
 
 
 	"tailscale.com/types/lazy"
 	"tailscale.com/types/lazy"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 )
 )
 
 
@@ -31,19 +32,19 @@ type Store interface {
 	// ReadString returns the value of a [setting.StringValue] with the specified key,
 	// ReadString returns the value of a [setting.StringValue] with the specified key,
 	// an [setting.ErrNotConfigured] if the policy setting is not configured, or
 	// an [setting.ErrNotConfigured] if the policy setting is not configured, or
 	// an error on failure.
 	// an error on failure.
-	ReadString(key setting.Key) (string, error)
+	ReadString(key pkey.Key) (string, error)
 	// ReadUInt64 returns the value of a [setting.IntegerValue] with the specified key,
 	// ReadUInt64 returns the value of a [setting.IntegerValue] with the specified key,
 	// an [setting.ErrNotConfigured] if the policy setting is not configured, or
 	// an [setting.ErrNotConfigured] if the policy setting is not configured, or
 	// an error on failure.
 	// an error on failure.
-	ReadUInt64(key setting.Key) (uint64, error)
+	ReadUInt64(key pkey.Key) (uint64, error)
 	// ReadBoolean returns the value of a [setting.BooleanValue] with the specified key,
 	// ReadBoolean returns the value of a [setting.BooleanValue] with the specified key,
 	// an [setting.ErrNotConfigured] if the policy setting is not configured, or
 	// an [setting.ErrNotConfigured] if the policy setting is not configured, or
 	// an error on failure.
 	// an error on failure.
-	ReadBoolean(key setting.Key) (bool, error)
+	ReadBoolean(key pkey.Key) (bool, error)
 	// ReadStringArray returns the value of a [setting.StringListValue] with the specified key,
 	// ReadStringArray returns the value of a [setting.StringListValue] with the specified key,
 	// an [setting.ErrNotConfigured] if the policy setting is not configured, or
 	// an [setting.ErrNotConfigured] if the policy setting is not configured, or
 	// an error on failure.
 	// an error on failure.
-	ReadStringArray(key setting.Key) ([]string, error)
+	ReadStringArray(key pkey.Key) ([]string, error)
 }
 }
 
 
 // Lockable is an optional interface that [Store] implementations may support.
 // Lockable is an optional interface that [Store] implementations may support.

+ 15 - 14
util/syspolicy/source/policy_store_windows.go

@@ -13,6 +13,7 @@ import (
 	"golang.org/x/sys/windows/registry"
 	"golang.org/x/sys/windows/registry"
 	"tailscale.com/util/set"
 	"tailscale.com/util/set"
 	"tailscale.com/util/syspolicy/internal/loggerx"
 	"tailscale.com/util/syspolicy/internal/loggerx"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/winutil/gp"
 	"tailscale.com/util/winutil/gp"
 )
 )
@@ -251,7 +252,7 @@ func (ps *PlatformPolicyStore) onChange() {
 
 
 // ReadString retrieves a string policy with the specified key.
 // ReadString retrieves a string policy with the specified key.
 // It returns [setting.ErrNotConfigured] if the policy setting does not exist.
 // It returns [setting.ErrNotConfigured] if the policy setting does not exist.
-func (ps *PlatformPolicyStore) ReadString(key setting.Key) (val string, err error) {
+func (ps *PlatformPolicyStore) ReadString(key pkey.Key) (val string, err error) {
 	return getPolicyValue(ps, key,
 	return getPolicyValue(ps, key,
 		func(key registry.Key, valueName string) (string, error) {
 		func(key registry.Key, valueName string) (string, error) {
 			val, _, err := key.GetStringValue(valueName)
 			val, _, err := key.GetStringValue(valueName)
@@ -261,7 +262,7 @@ func (ps *PlatformPolicyStore) ReadString(key setting.Key) (val string, err erro
 
 
 // ReadUInt64 retrieves an integer policy with the specified key.
 // ReadUInt64 retrieves an integer policy with the specified key.
 // It returns [setting.ErrNotConfigured] if the policy setting does not exist.
 // It returns [setting.ErrNotConfigured] if the policy setting does not exist.
-func (ps *PlatformPolicyStore) ReadUInt64(key setting.Key) (uint64, error) {
+func (ps *PlatformPolicyStore) ReadUInt64(key pkey.Key) (uint64, error) {
 	return getPolicyValue(ps, key,
 	return getPolicyValue(ps, key,
 		func(key registry.Key, valueName string) (uint64, error) {
 		func(key registry.Key, valueName string) (uint64, error) {
 			val, _, err := key.GetIntegerValue(valueName)
 			val, _, err := key.GetIntegerValue(valueName)
@@ -271,7 +272,7 @@ func (ps *PlatformPolicyStore) ReadUInt64(key setting.Key) (uint64, error) {
 
 
 // ReadBoolean retrieves a boolean policy with the specified key.
 // ReadBoolean retrieves a boolean policy with the specified key.
 // It returns [setting.ErrNotConfigured] if the policy setting does not exist.
 // It returns [setting.ErrNotConfigured] if the policy setting does not exist.
-func (ps *PlatformPolicyStore) ReadBoolean(key setting.Key) (bool, error) {
+func (ps *PlatformPolicyStore) ReadBoolean(key pkey.Key) (bool, error) {
 	return getPolicyValue(ps, key,
 	return getPolicyValue(ps, key,
 		func(key registry.Key, valueName string) (bool, error) {
 		func(key registry.Key, valueName string) (bool, error) {
 			val, _, err := key.GetIntegerValue(valueName)
 			val, _, err := key.GetIntegerValue(valueName)
@@ -283,8 +284,8 @@ func (ps *PlatformPolicyStore) ReadBoolean(key setting.Key) (bool, error) {
 }
 }
 
 
 // ReadString retrieves a multi-string policy with the specified key.
 // ReadString retrieves a multi-string policy with the specified key.
-// It returns [setting.ErrNotConfigured] if the policy setting does not exist.
-func (ps *PlatformPolicyStore) ReadStringArray(key setting.Key) ([]string, error) {
+// It returns [pkey.ErrNotConfigured] if the policy setting does not exist.
+func (ps *PlatformPolicyStore) ReadStringArray(key pkey.Key) ([]string, error) {
 	return getPolicyValue(ps, key,
 	return getPolicyValue(ps, key,
 		func(key registry.Key, valueName string) ([]string, error) {
 		func(key registry.Key, valueName string) ([]string, error) {
 			val, _, err := key.GetStringsValue(valueName)
 			val, _, err := key.GetStringsValue(valueName)
@@ -322,25 +323,25 @@ func (ps *PlatformPolicyStore) ReadStringArray(key setting.Key) ([]string, error
 		})
 		})
 }
 }
 
 
-// splitSettingKey extracts the registry key name and value name from a [setting.Key].
-// The [setting.Key] format allows grouping settings into nested categories using one
-// or more [setting.KeyPathSeparator]s in the path. How individual policy settings are
+// splitSettingKey extracts the registry key name and value name from a [pkey.Key].
+// The [pkey.Key] format allows grouping settings into nested categories using one
+// or more [pkey.KeyPathSeparator]s in the path. How individual policy settings are
 // stored is an implementation detail of each [Store]. In the [PlatformPolicyStore]
 // stored is an implementation detail of each [Store]. In the [PlatformPolicyStore]
 // for Windows, we map nested policy categories onto the Registry key hierarchy.
 // for Windows, we map nested policy categories onto the Registry key hierarchy.
-// The last component after a [setting.KeyPathSeparator] is treated as the value name,
+// The last component after a [pkey.KeyPathSeparator] is treated as the value name,
 // while everything preceding it is considered a subpath (relative to the {HKLM,HKCU}\Software\Policies\Tailscale key).
 // while everything preceding it is considered a subpath (relative to the {HKLM,HKCU}\Software\Policies\Tailscale key).
-// If there are no [setting.KeyPathSeparator]s in the key, the policy setting value
+// If there are no [pkey.KeyPathSeparator]s in the key, the policy setting value
 // is meant to be stored directly under {HKLM,HKCU}\Software\Policies\Tailscale.
 // is meant to be stored directly under {HKLM,HKCU}\Software\Policies\Tailscale.
-func splitSettingKey(key setting.Key) (path, valueName string) {
-	if idx := strings.LastIndexByte(string(key), setting.KeyPathSeparator); idx != -1 {
-		path = strings.ReplaceAll(string(key[:idx]), string(setting.KeyPathSeparator), `\`)
+func splitSettingKey(key pkey.Key) (path, valueName string) {
+	if idx := strings.LastIndexByte(string(key), pkey.KeyPathSeparator); idx != -1 {
+		path = strings.ReplaceAll(string(key[:idx]), string(pkey.KeyPathSeparator), `\`)
 		valueName = string(key[idx+1:])
 		valueName = string(key[idx+1:])
 		return path, valueName
 		return path, valueName
 	}
 	}
 	return "", string(key)
 	return "", string(key)
 }
 }
 
 
-func getPolicyValue[T any](ps *PlatformPolicyStore, key setting.Key, getter registryValueGetter[T]) (T, error) {
+func getPolicyValue[T any](ps *PlatformPolicyStore, key pkey.Key, getter registryValueGetter[T]) (T, error) {
 	var zero T
 	var zero T
 
 
 	ps.mu.Lock()
 	ps.mu.Lock()

+ 4 - 3
util/syspolicy/source/policy_store_windows_test.go

@@ -19,6 +19,7 @@ import (
 	"tailscale.com/tstest"
 	"tailscale.com/tstest"
 	"tailscale.com/util/cibuild"
 	"tailscale.com/util/cibuild"
 	"tailscale.com/util/mak"
 	"tailscale.com/util/mak"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/winutil"
 	"tailscale.com/util/winutil"
 	"tailscale.com/util/winutil/gp"
 	"tailscale.com/util/winutil/gp"
@@ -31,7 +32,7 @@ import (
 type subkeyStrings []string
 type subkeyStrings []string
 
 
 type testPolicyValue struct {
 type testPolicyValue struct {
-	name  setting.Key
+	name  pkey.Key
 	value any
 	value any
 }
 }
 
 
@@ -100,7 +101,7 @@ func TestReadPolicyStore(t *testing.T) {
 		t.Skipf("test requires running as elevated user")
 		t.Skipf("test requires running as elevated user")
 	}
 	}
 	tests := []struct {
 	tests := []struct {
-		name        setting.Key
+		name        pkey.Key
 		newValue    any
 		newValue    any
 		legacyValue any
 		legacyValue any
 		want        any
 		want        any
@@ -269,7 +270,7 @@ func TestPolicyStoreChangeNotifications(t *testing.T) {
 func TestSplitSettingKey(t *testing.T) {
 func TestSplitSettingKey(t *testing.T) {
 	tests := []struct {
 	tests := []struct {
 		name      string
 		name      string
-		key       setting.Key
+		key       pkey.Key
 		wantPath  string
 		wantPath  string
 		wantValue string
 		wantValue string
 	}{
 	}{

+ 15 - 14
util/syspolicy/source/test_store.go

@@ -12,6 +12,7 @@ import (
 	"tailscale.com/util/mak"
 	"tailscale.com/util/mak"
 	"tailscale.com/util/set"
 	"tailscale.com/util/set"
 	"tailscale.com/util/slicesx"
 	"tailscale.com/util/slicesx"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/testenv"
 	"tailscale.com/util/testenv"
 )
 )
@@ -31,7 +32,7 @@ type TestValueType interface {
 // TestSetting is a policy setting in a [TestStore].
 // TestSetting is a policy setting in a [TestStore].
 type TestSetting[T TestValueType] struct {
 type TestSetting[T TestValueType] struct {
 	// Key is the setting's unique identifier.
 	// Key is the setting's unique identifier.
-	Key setting.Key
+	Key pkey.Key
 	// Error is the error to be returned by the [TestStore] when reading
 	// Error is the error to be returned by the [TestStore] when reading
 	// a policy setting with the specified key.
 	// a policy setting with the specified key.
 	Error error
 	Error error
@@ -43,20 +44,20 @@ type TestSetting[T TestValueType] struct {
 
 
 // TestSettingOf returns a [TestSetting] representing a policy setting
 // TestSettingOf returns a [TestSetting] representing a policy setting
 // configured with the specified key and value.
 // configured with the specified key and value.
-func TestSettingOf[T TestValueType](key setting.Key, value T) TestSetting[T] {
+func TestSettingOf[T TestValueType](key pkey.Key, value T) TestSetting[T] {
 	return TestSetting[T]{Key: key, Value: value}
 	return TestSetting[T]{Key: key, Value: value}
 }
 }
 
 
 // TestSettingWithError returns a [TestSetting] representing a policy setting
 // TestSettingWithError returns a [TestSetting] representing a policy setting
 // with the specified key and error.
 // with the specified key and error.
-func TestSettingWithError[T TestValueType](key setting.Key, err error) TestSetting[T] {
+func TestSettingWithError[T TestValueType](key pkey.Key, err error) TestSetting[T] {
 	return TestSetting[T]{Key: key, Error: err}
 	return TestSetting[T]{Key: key, Error: err}
 }
 }
 
 
 // testReadOperation describes a single policy setting read operation.
 // testReadOperation describes a single policy setting read operation.
 type testReadOperation struct {
 type testReadOperation struct {
 	// Key is the setting's unique identifier.
 	// Key is the setting's unique identifier.
-	Key setting.Key
+	Key pkey.Key
 	// Type is a value type of a read operation.
 	// Type is a value type of a read operation.
 	// [setting.BooleanValue], [setting.IntegerValue], [setting.StringValue] or [setting.StringListValue]
 	// [setting.BooleanValue], [setting.IntegerValue], [setting.StringValue] or [setting.StringListValue]
 	Type setting.Type
 	Type setting.Type
@@ -65,7 +66,7 @@ type testReadOperation struct {
 // TestExpectedReads is the number of read operations with the specified details.
 // TestExpectedReads is the number of read operations with the specified details.
 type TestExpectedReads struct {
 type TestExpectedReads struct {
 	// Key is the setting's unique identifier.
 	// Key is the setting's unique identifier.
-	Key setting.Key
+	Key pkey.Key
 	// Type is a value type of a read operation.
 	// Type is a value type of a read operation.
 	// [setting.BooleanValue], [setting.IntegerValue], [setting.StringValue] or [setting.StringListValue]
 	// [setting.BooleanValue], [setting.IntegerValue], [setting.StringValue] or [setting.StringListValue]
 	Type setting.Type
 	Type setting.Type
@@ -87,8 +88,8 @@ type TestStore struct {
 	storeLockCount atomic.Int32
 	storeLockCount atomic.Int32
 
 
 	mu           sync.RWMutex
 	mu           sync.RWMutex
-	suspendCount int                 // change callback are suspended if > 0
-	mr, mw       map[setting.Key]any // maps for reading and writing; they're the same unless the store is suspended.
+	suspendCount int              // change callback are suspended if > 0
+	mr, mw       map[pkey.Key]any // maps for reading and writing; they're the same unless the store is suspended.
 	cbs          set.HandleSet[func()]
 	cbs          set.HandleSet[func()]
 	closed       bool
 	closed       bool
 
 
@@ -99,7 +100,7 @@ type TestStore struct {
 // NewTestStore returns a new [TestStore].
 // NewTestStore returns a new [TestStore].
 // The tb will be used to report coding errors detected by the [TestStore].
 // The tb will be used to report coding errors detected by the [TestStore].
 func NewTestStore(tb testenv.TB) *TestStore {
 func NewTestStore(tb testenv.TB) *TestStore {
-	m := make(map[setting.Key]any)
+	m := make(map[pkey.Key]any)
 	store := &TestStore{
 	store := &TestStore{
 		tb:   tb,
 		tb:   tb,
 		done: make(chan struct{}),
 		done: make(chan struct{}),
@@ -162,7 +163,7 @@ func (s *TestStore) IsEmpty() bool {
 }
 }
 
 
 // ReadString implements [Store].
 // ReadString implements [Store].
-func (s *TestStore) ReadString(key setting.Key) (string, error) {
+func (s *TestStore) ReadString(key pkey.Key) (string, error) {
 	defer s.recordRead(key, setting.StringValue)
 	defer s.recordRead(key, setting.StringValue)
 	s.mu.RLock()
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 	defer s.mu.RUnlock()
@@ -181,7 +182,7 @@ func (s *TestStore) ReadString(key setting.Key) (string, error) {
 }
 }
 
 
 // ReadUInt64 implements [Store].
 // ReadUInt64 implements [Store].
-func (s *TestStore) ReadUInt64(key setting.Key) (uint64, error) {
+func (s *TestStore) ReadUInt64(key pkey.Key) (uint64, error) {
 	defer s.recordRead(key, setting.IntegerValue)
 	defer s.recordRead(key, setting.IntegerValue)
 	s.mu.RLock()
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 	defer s.mu.RUnlock()
@@ -200,7 +201,7 @@ func (s *TestStore) ReadUInt64(key setting.Key) (uint64, error) {
 }
 }
 
 
 // ReadBoolean implements [Store].
 // ReadBoolean implements [Store].
-func (s *TestStore) ReadBoolean(key setting.Key) (bool, error) {
+func (s *TestStore) ReadBoolean(key pkey.Key) (bool, error) {
 	defer s.recordRead(key, setting.BooleanValue)
 	defer s.recordRead(key, setting.BooleanValue)
 	s.mu.RLock()
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 	defer s.mu.RUnlock()
@@ -219,7 +220,7 @@ func (s *TestStore) ReadBoolean(key setting.Key) (bool, error) {
 }
 }
 
 
 // ReadStringArray implements [Store].
 // ReadStringArray implements [Store].
-func (s *TestStore) ReadStringArray(key setting.Key) ([]string, error) {
+func (s *TestStore) ReadStringArray(key pkey.Key) ([]string, error) {
 	defer s.recordRead(key, setting.StringListValue)
 	defer s.recordRead(key, setting.StringListValue)
 	s.mu.RLock()
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 	defer s.mu.RUnlock()
@@ -237,7 +238,7 @@ func (s *TestStore) ReadStringArray(key setting.Key) ([]string, error) {
 	return slice, nil
 	return slice, nil
 }
 }
 
 
-func (s *TestStore) recordRead(key setting.Key, typ setting.Type) {
+func (s *TestStore) recordRead(key pkey.Key, typ setting.Type) {
 	s.readsMu.Lock()
 	s.readsMu.Lock()
 	op := testReadOperation{key, typ}
 	op := testReadOperation{key, typ}
 	num := s.reads[op]
 	num := s.reads[op]
@@ -399,7 +400,7 @@ func (s *TestStore) SetStringLists(settings ...TestSetting[[]string]) {
 }
 }
 
 
 // Delete deletes the specified settings from s.
 // Delete deletes the specified settings from s.
-func (s *TestStore) Delete(keys ...setting.Key) {
+func (s *TestStore) Delete(keys ...pkey.Key) {
 	s.storeLock.Lock()
 	s.storeLock.Lock()
 	for _, key := range keys {
 	for _, key := range keys {
 		s.mu.Lock()
 		s.mu.Lock()

+ 11 - 10
util/syspolicy/syspolicy.go

@@ -17,6 +17,7 @@ import (
 	"time"
 	"time"
 
 
 	"tailscale.com/util/syspolicy/internal/loggerx"
 	"tailscale.com/util/syspolicy/internal/loggerx"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/rsop"
 	"tailscale.com/util/syspolicy/rsop"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/source"
 	"tailscale.com/util/syspolicy/source"
@@ -58,7 +59,7 @@ func MustRegisterStoreForTest(tb testenv.TB, name string, scope setting.PolicySc
 
 
 // HasAnyOf returns whether at least one of the specified policy settings is configured,
 // HasAnyOf returns whether at least one of the specified policy settings is configured,
 // or an error if no keys are provided or the check fails.
 // or an error if no keys are provided or the check fails.
-func HasAnyOf(keys ...Key) (bool, error) {
+func HasAnyOf(keys ...pkey.Key) (bool, error) {
 	if len(keys) == 0 {
 	if len(keys) == 0 {
 		return false, errors.New("at least one key must be specified")
 		return false, errors.New("at least one key must be specified")
 	}
 	}
@@ -82,25 +83,25 @@ func HasAnyOf(keys ...Key) (bool, error) {
 
 
 // GetString returns a string policy setting with the specified key,
 // GetString returns a string policy setting with the specified key,
 // or defaultValue if it does not exist.
 // or defaultValue if it does not exist.
-func GetString(key Key, defaultValue string) (string, error) {
+func GetString(key pkey.Key, defaultValue string) (string, error) {
 	return getCurrentPolicySettingValue(key, defaultValue)
 	return getCurrentPolicySettingValue(key, defaultValue)
 }
 }
 
 
 // GetUint64 returns a numeric policy setting with the specified key,
 // GetUint64 returns a numeric policy setting with the specified key,
 // or defaultValue if it does not exist.
 // or defaultValue if it does not exist.
-func GetUint64(key Key, defaultValue uint64) (uint64, error) {
+func GetUint64(key pkey.Key, defaultValue uint64) (uint64, error) {
 	return getCurrentPolicySettingValue(key, defaultValue)
 	return getCurrentPolicySettingValue(key, defaultValue)
 }
 }
 
 
 // GetBoolean returns a boolean policy setting with the specified key,
 // GetBoolean returns a boolean policy setting with the specified key,
 // or defaultValue if it does not exist.
 // or defaultValue if it does not exist.
-func GetBoolean(key Key, defaultValue bool) (bool, error) {
+func GetBoolean(key pkey.Key, defaultValue bool) (bool, error) {
 	return getCurrentPolicySettingValue(key, defaultValue)
 	return getCurrentPolicySettingValue(key, defaultValue)
 }
 }
 
 
 // GetStringArray returns a multi-string policy setting with the specified key,
 // GetStringArray returns a multi-string policy setting with the specified key,
 // or defaultValue if it does not exist.
 // or defaultValue if it does not exist.
-func GetStringArray(key Key, defaultValue []string) ([]string, error) {
+func GetStringArray(key pkey.Key, defaultValue []string) ([]string, error) {
 	return getCurrentPolicySettingValue(key, defaultValue)
 	return getCurrentPolicySettingValue(key, defaultValue)
 }
 }
 
 
@@ -110,14 +111,14 @@ func GetStringArray(key Key, defaultValue []string) ([]string, error) {
 // the authority to set. It describes user-decides/always/never options, where
 // the authority to set. It describes user-decides/always/never options, where
 // "always" and "never" remove the user's ability to make a selection. If not
 // "always" and "never" remove the user's ability to make a selection. If not
 // present or set to a different value, "user-decides" is the default.
 // present or set to a different value, "user-decides" is the default.
-func GetPreferenceOption(name Key) (setting.PreferenceOption, error) {
+func GetPreferenceOption(name pkey.Key) (setting.PreferenceOption, error) {
 	return getCurrentPolicySettingValue(name, setting.ShowChoiceByPolicy)
 	return getCurrentPolicySettingValue(name, setting.ShowChoiceByPolicy)
 }
 }
 
 
 // GetPreferenceOptionOrDefault is like [GetPreferenceOption], but allows
 // GetPreferenceOptionOrDefault is like [GetPreferenceOption], but allows
 // specifying a default value to return if the policy setting is not configured.
 // specifying a default value to return if the policy setting is not configured.
 // It can be used in situations where "user-decides" is not the default.
 // It can be used in situations where "user-decides" is not the default.
-func GetPreferenceOptionOrDefault(name Key, defaultValue setting.PreferenceOption) (setting.PreferenceOption, error) {
+func GetPreferenceOptionOrDefault(name pkey.Key, defaultValue setting.PreferenceOption) (setting.PreferenceOption, error) {
 	return getCurrentPolicySettingValue(name, defaultValue)
 	return getCurrentPolicySettingValue(name, defaultValue)
 }
 }
 
 
@@ -126,7 +127,7 @@ func GetPreferenceOptionOrDefault(name Key, defaultValue setting.PreferenceOptio
 // for UI elements. The registry value should be a string set to "show" (return
 // for UI elements. The registry value should be a string set to "show" (return
 // true) or "hide" (return true). If not present or set to a different value,
 // true) or "hide" (return true). If not present or set to a different value,
 // "show" (return false) is the default.
 // "show" (return false) is the default.
-func GetVisibility(name Key) (setting.Visibility, error) {
+func GetVisibility(name pkey.Key) (setting.Visibility, error) {
 	return getCurrentPolicySettingValue(name, setting.VisibleByPolicy)
 	return getCurrentPolicySettingValue(name, setting.VisibleByPolicy)
 }
 }
 
 
@@ -135,7 +136,7 @@ func GetVisibility(name Key) (setting.Visibility, error) {
 // action. The registry value should be a string that time.ParseDuration
 // action. The registry value should be a string that time.ParseDuration
 // understands. If the registry value is "" or can not be processed,
 // understands. If the registry value is "" or can not be processed,
 // defaultValue is returned instead.
 // defaultValue is returned instead.
-func GetDuration(name Key, defaultValue time.Duration) (time.Duration, error) {
+func GetDuration(name pkey.Key, defaultValue time.Duration) (time.Duration, error) {
 	d, err := getCurrentPolicySettingValue(name, defaultValue)
 	d, err := getCurrentPolicySettingValue(name, defaultValue)
 	if err != nil {
 	if err != nil {
 		return d, err
 		return d, err
@@ -160,7 +161,7 @@ func RegisterChangeCallback(cb rsop.PolicyChangeCallback) (unregister func(), er
 // specified by its key from the [rsop.Policy] of the [setting.DefaultScope]. It
 // specified by its key from the [rsop.Policy] of the [setting.DefaultScope]. It
 // returns def if the policy setting is not configured, or an error if it has
 // returns def if the policy setting is not configured, or an error if it has
 // an error or could not be converted to the specified type T.
 // an error or could not be converted to the specified type T.
-func getCurrentPolicySettingValue[T setting.ValueType](key Key, def T) (T, error) {
+func getCurrentPolicySettingValue[T setting.ValueType](key pkey.Key, def T) (T, error) {
 	effective, err := rsop.PolicyFor(setting.DefaultScope())
 	effective, err := rsop.PolicyFor(setting.DefaultScope())
 	if err != nil {
 	if err != nil {
 		return def, err
 		return def, err

+ 39 - 38
util/syspolicy/syspolicy_test.go

@@ -12,6 +12,7 @@ import (
 	"tailscale.com/types/logger"
 	"tailscale.com/types/logger"
 	"tailscale.com/util/syspolicy/internal/loggerx"
 	"tailscale.com/util/syspolicy/internal/loggerx"
 	"tailscale.com/util/syspolicy/internal/metrics"
 	"tailscale.com/util/syspolicy/internal/metrics"
+	"tailscale.com/util/syspolicy/pkey"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/setting"
 	"tailscale.com/util/syspolicy/source"
 	"tailscale.com/util/syspolicy/source"
 	"tailscale.com/util/testenv"
 	"tailscale.com/util/testenv"
@@ -22,7 +23,7 @@ var someOtherError = errors.New("error other than not found")
 func TestGetString(t *testing.T) {
 func TestGetString(t *testing.T) {
 	tests := []struct {
 	tests := []struct {
 		name         string
 		name         string
-		key          Key
+		key          pkey.Key
 		handlerValue string
 		handlerValue string
 		handlerError error
 		handlerError error
 		defaultValue string
 		defaultValue string
@@ -32,7 +33,7 @@ func TestGetString(t *testing.T) {
 	}{
 	}{
 		{
 		{
 			name:         "read existing value",
 			name:         "read existing value",
-			key:          AdminConsoleVisibility,
+			key:          pkey.AdminConsoleVisibility,
 			handlerValue: "hide",
 			handlerValue: "hide",
 			wantValue:    "hide",
 			wantValue:    "hide",
 			wantMetrics: []metrics.TestState{
 			wantMetrics: []metrics.TestState{
@@ -42,13 +43,13 @@ func TestGetString(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:         "read non-existing value",
 			name:         "read non-existing value",
-			key:          EnableServerMode,
+			key:          pkey.EnableServerMode,
 			handlerError: ErrNotConfigured,
 			handlerError: ErrNotConfigured,
 			wantError:    nil,
 			wantError:    nil,
 		},
 		},
 		{
 		{
 			name:         "read non-existing value, non-blank default",
 			name:         "read non-existing value, non-blank default",
-			key:          EnableServerMode,
+			key:          pkey.EnableServerMode,
 			handlerError: ErrNotConfigured,
 			handlerError: ErrNotConfigured,
 			defaultValue: "test",
 			defaultValue: "test",
 			wantValue:    "test",
 			wantValue:    "test",
@@ -56,7 +57,7 @@ func TestGetString(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:         "reading value returns other error",
 			name:         "reading value returns other error",
-			key:          NetworkDevicesVisibility,
+			key:          pkey.NetworkDevicesVisibility,
 			handlerError: someOtherError,
 			handlerError: someOtherError,
 			wantError:    someOtherError,
 			wantError:    someOtherError,
 			wantMetrics: []metrics.TestState{
 			wantMetrics: []metrics.TestState{
@@ -103,7 +104,7 @@ func TestGetString(t *testing.T) {
 func TestGetUint64(t *testing.T) {
 func TestGetUint64(t *testing.T) {
 	tests := []struct {
 	tests := []struct {
 		name         string
 		name         string
-		key          Key
+		key          pkey.Key
 		handlerValue uint64
 		handlerValue uint64
 		handlerError error
 		handlerError error
 		defaultValue uint64
 		defaultValue uint64
@@ -112,27 +113,27 @@ func TestGetUint64(t *testing.T) {
 	}{
 	}{
 		{
 		{
 			name:         "read existing value",
 			name:         "read existing value",
-			key:          LogSCMInteractions,
+			key:          pkey.LogSCMInteractions,
 			handlerValue: 1,
 			handlerValue: 1,
 			wantValue:    1,
 			wantValue:    1,
 		},
 		},
 		{
 		{
 			name:         "read non-existing value",
 			name:         "read non-existing value",
-			key:          LogSCMInteractions,
+			key:          pkey.LogSCMInteractions,
 			handlerValue: 0,
 			handlerValue: 0,
 			handlerError: ErrNotConfigured,
 			handlerError: ErrNotConfigured,
 			wantValue:    0,
 			wantValue:    0,
 		},
 		},
 		{
 		{
 			name:         "read non-existing value, non-zero default",
 			name:         "read non-existing value, non-zero default",
-			key:          LogSCMInteractions,
+			key:          pkey.LogSCMInteractions,
 			defaultValue: 2,
 			defaultValue: 2,
 			handlerError: ErrNotConfigured,
 			handlerError: ErrNotConfigured,
 			wantValue:    2,
 			wantValue:    2,
 		},
 		},
 		{
 		{
 			name:         "reading value returns other error",
 			name:         "reading value returns other error",
-			key:          FlushDNSOnSessionUnlock,
+			key:          pkey.FlushDNSOnSessionUnlock,
 			handlerError: someOtherError,
 			handlerError: someOtherError,
 			wantError:    someOtherError,
 			wantError:    someOtherError,
 		},
 		},
@@ -169,7 +170,7 @@ func TestGetUint64(t *testing.T) {
 func TestGetBoolean(t *testing.T) {
 func TestGetBoolean(t *testing.T) {
 	tests := []struct {
 	tests := []struct {
 		name         string
 		name         string
-		key          Key
+		key          pkey.Key
 		handlerValue bool
 		handlerValue bool
 		handlerError error
 		handlerError error
 		defaultValue bool
 		defaultValue bool
@@ -179,7 +180,7 @@ func TestGetBoolean(t *testing.T) {
 	}{
 	}{
 		{
 		{
 			name:         "read existing value",
 			name:         "read existing value",
-			key:          FlushDNSOnSessionUnlock,
+			key:          pkey.FlushDNSOnSessionUnlock,
 			handlerValue: true,
 			handlerValue: true,
 			wantValue:    true,
 			wantValue:    true,
 			wantMetrics: []metrics.TestState{
 			wantMetrics: []metrics.TestState{
@@ -189,14 +190,14 @@ func TestGetBoolean(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:         "read non-existing value",
 			name:         "read non-existing value",
-			key:          LogSCMInteractions,
+			key:          pkey.LogSCMInteractions,
 			handlerValue: false,
 			handlerValue: false,
 			handlerError: ErrNotConfigured,
 			handlerError: ErrNotConfigured,
 			wantValue:    false,
 			wantValue:    false,
 		},
 		},
 		{
 		{
 			name:         "reading value returns other error",
 			name:         "reading value returns other error",
-			key:          FlushDNSOnSessionUnlock,
+			key:          pkey.FlushDNSOnSessionUnlock,
 			handlerError: someOtherError,
 			handlerError: someOtherError,
 			wantError:    someOtherError, // expect error...
 			wantError:    someOtherError, // expect error...
 			defaultValue: true,
 			defaultValue: true,
@@ -245,7 +246,7 @@ func TestGetBoolean(t *testing.T) {
 func TestGetPreferenceOption(t *testing.T) {
 func TestGetPreferenceOption(t *testing.T) {
 	tests := []struct {
 	tests := []struct {
 		name         string
 		name         string
-		key          Key
+		key          pkey.Key
 		handlerValue string
 		handlerValue string
 		handlerError error
 		handlerError error
 		wantValue    setting.PreferenceOption
 		wantValue    setting.PreferenceOption
@@ -254,7 +255,7 @@ func TestGetPreferenceOption(t *testing.T) {
 	}{
 	}{
 		{
 		{
 			name:         "always by policy",
 			name:         "always by policy",
-			key:          EnableIncomingConnections,
+			key:          pkey.EnableIncomingConnections,
 			handlerValue: "always",
 			handlerValue: "always",
 			wantValue:    setting.AlwaysByPolicy,
 			wantValue:    setting.AlwaysByPolicy,
 			wantMetrics: []metrics.TestState{
 			wantMetrics: []metrics.TestState{
@@ -264,7 +265,7 @@ func TestGetPreferenceOption(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:         "never by policy",
 			name:         "never by policy",
-			key:          EnableIncomingConnections,
+			key:          pkey.EnableIncomingConnections,
 			handlerValue: "never",
 			handlerValue: "never",
 			wantValue:    setting.NeverByPolicy,
 			wantValue:    setting.NeverByPolicy,
 			wantMetrics: []metrics.TestState{
 			wantMetrics: []metrics.TestState{
@@ -274,7 +275,7 @@ func TestGetPreferenceOption(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:         "use default",
 			name:         "use default",
-			key:          EnableIncomingConnections,
+			key:          pkey.EnableIncomingConnections,
 			handlerValue: "",
 			handlerValue: "",
 			wantValue:    setting.ShowChoiceByPolicy,
 			wantValue:    setting.ShowChoiceByPolicy,
 			wantMetrics: []metrics.TestState{
 			wantMetrics: []metrics.TestState{
@@ -284,13 +285,13 @@ func TestGetPreferenceOption(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:         "read non-existing value",
 			name:         "read non-existing value",
-			key:          EnableIncomingConnections,
+			key:          pkey.EnableIncomingConnections,
 			handlerError: ErrNotConfigured,
 			handlerError: ErrNotConfigured,
 			wantValue:    setting.ShowChoiceByPolicy,
 			wantValue:    setting.ShowChoiceByPolicy,
 		},
 		},
 		{
 		{
 			name:         "other error is returned",
 			name:         "other error is returned",
-			key:          EnableIncomingConnections,
+			key:          pkey.EnableIncomingConnections,
 			handlerError: someOtherError,
 			handlerError: someOtherError,
 			wantValue:    setting.ShowChoiceByPolicy,
 			wantValue:    setting.ShowChoiceByPolicy,
 			wantError:    someOtherError,
 			wantError:    someOtherError,
@@ -338,7 +339,7 @@ func TestGetPreferenceOption(t *testing.T) {
 func TestGetVisibility(t *testing.T) {
 func TestGetVisibility(t *testing.T) {
 	tests := []struct {
 	tests := []struct {
 		name         string
 		name         string
-		key          Key
+		key          pkey.Key
 		handlerValue string
 		handlerValue string
 		handlerError error
 		handlerError error
 		wantValue    setting.Visibility
 		wantValue    setting.Visibility
@@ -347,7 +348,7 @@ func TestGetVisibility(t *testing.T) {
 	}{
 	}{
 		{
 		{
 			name:         "hidden by policy",
 			name:         "hidden by policy",
-			key:          AdminConsoleVisibility,
+			key:          pkey.AdminConsoleVisibility,
 			handlerValue: "hide",
 			handlerValue: "hide",
 			wantValue:    setting.HiddenByPolicy,
 			wantValue:    setting.HiddenByPolicy,
 			wantMetrics: []metrics.TestState{
 			wantMetrics: []metrics.TestState{
@@ -357,7 +358,7 @@ func TestGetVisibility(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:         "visibility default",
 			name:         "visibility default",
-			key:          AdminConsoleVisibility,
+			key:          pkey.AdminConsoleVisibility,
 			handlerValue: "show",
 			handlerValue: "show",
 			wantValue:    setting.VisibleByPolicy,
 			wantValue:    setting.VisibleByPolicy,
 			wantMetrics: []metrics.TestState{
 			wantMetrics: []metrics.TestState{
@@ -367,14 +368,14 @@ func TestGetVisibility(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:         "read non-existing value",
 			name:         "read non-existing value",
-			key:          AdminConsoleVisibility,
+			key:          pkey.AdminConsoleVisibility,
 			handlerValue: "show",
 			handlerValue: "show",
 			handlerError: ErrNotConfigured,
 			handlerError: ErrNotConfigured,
 			wantValue:    setting.VisibleByPolicy,
 			wantValue:    setting.VisibleByPolicy,
 		},
 		},
 		{
 		{
 			name:         "other error is returned",
 			name:         "other error is returned",
-			key:          AdminConsoleVisibility,
+			key:          pkey.AdminConsoleVisibility,
 			handlerValue: "show",
 			handlerValue: "show",
 			handlerError: someOtherError,
 			handlerError: someOtherError,
 			wantValue:    setting.VisibleByPolicy,
 			wantValue:    setting.VisibleByPolicy,
@@ -423,7 +424,7 @@ func TestGetVisibility(t *testing.T) {
 func TestGetDuration(t *testing.T) {
 func TestGetDuration(t *testing.T) {
 	tests := []struct {
 	tests := []struct {
 		name         string
 		name         string
-		key          Key
+		key          pkey.Key
 		handlerValue string
 		handlerValue string
 		handlerError error
 		handlerError error
 		defaultValue time.Duration
 		defaultValue time.Duration
@@ -433,7 +434,7 @@ func TestGetDuration(t *testing.T) {
 	}{
 	}{
 		{
 		{
 			name:         "read existing value",
 			name:         "read existing value",
-			key:          KeyExpirationNoticeTime,
+			key:          pkey.KeyExpirationNoticeTime,
 			handlerValue: "2h",
 			handlerValue: "2h",
 			wantValue:    2 * time.Hour,
 			wantValue:    2 * time.Hour,
 			defaultValue: 24 * time.Hour,
 			defaultValue: 24 * time.Hour,
@@ -444,7 +445,7 @@ func TestGetDuration(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:         "invalid duration value",
 			name:         "invalid duration value",
-			key:          KeyExpirationNoticeTime,
+			key:          pkey.KeyExpirationNoticeTime,
 			handlerValue: "-20",
 			handlerValue: "-20",
 			wantValue:    24 * time.Hour,
 			wantValue:    24 * time.Hour,
 			wantError:    errors.New(`time: missing unit in duration "-20"`),
 			wantError:    errors.New(`time: missing unit in duration "-20"`),
@@ -456,21 +457,21 @@ func TestGetDuration(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:         "read non-existing value",
 			name:         "read non-existing value",
-			key:          KeyExpirationNoticeTime,
+			key:          pkey.KeyExpirationNoticeTime,
 			handlerError: ErrNotConfigured,
 			handlerError: ErrNotConfigured,
 			wantValue:    24 * time.Hour,
 			wantValue:    24 * time.Hour,
 			defaultValue: 24 * time.Hour,
 			defaultValue: 24 * time.Hour,
 		},
 		},
 		{
 		{
 			name:         "read non-existing value different default",
 			name:         "read non-existing value different default",
-			key:          KeyExpirationNoticeTime,
+			key:          pkey.KeyExpirationNoticeTime,
 			handlerError: ErrNotConfigured,
 			handlerError: ErrNotConfigured,
 			wantValue:    0 * time.Second,
 			wantValue:    0 * time.Second,
 			defaultValue: 0 * time.Second,
 			defaultValue: 0 * time.Second,
 		},
 		},
 		{
 		{
 			name:         "other error is returned",
 			name:         "other error is returned",
-			key:          KeyExpirationNoticeTime,
+			key:          pkey.KeyExpirationNoticeTime,
 			handlerError: someOtherError,
 			handlerError: someOtherError,
 			wantValue:    24 * time.Hour,
 			wantValue:    24 * time.Hour,
 			wantError:    someOtherError,
 			wantError:    someOtherError,
@@ -519,7 +520,7 @@ func TestGetDuration(t *testing.T) {
 func TestGetStringArray(t *testing.T) {
 func TestGetStringArray(t *testing.T) {
 	tests := []struct {
 	tests := []struct {
 		name         string
 		name         string
-		key          Key
+		key          pkey.Key
 		handlerValue []string
 		handlerValue []string
 		handlerError error
 		handlerError error
 		defaultValue []string
 		defaultValue []string
@@ -529,7 +530,7 @@ func TestGetStringArray(t *testing.T) {
 	}{
 	}{
 		{
 		{
 			name:         "read existing value",
 			name:         "read existing value",
-			key:          AllowedSuggestedExitNodes,
+			key:          pkey.AllowedSuggestedExitNodes,
 			handlerValue: []string{"foo", "bar"},
 			handlerValue: []string{"foo", "bar"},
 			wantValue:    []string{"foo", "bar"},
 			wantValue:    []string{"foo", "bar"},
 			wantMetrics: []metrics.TestState{
 			wantMetrics: []metrics.TestState{
@@ -539,13 +540,13 @@ func TestGetStringArray(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:         "read non-existing value",
 			name:         "read non-existing value",
-			key:          AllowedSuggestedExitNodes,
+			key:          pkey.AllowedSuggestedExitNodes,
 			handlerError: ErrNotConfigured,
 			handlerError: ErrNotConfigured,
 			wantError:    nil,
 			wantError:    nil,
 		},
 		},
 		{
 		{
 			name:         "read non-existing value, non nil default",
 			name:         "read non-existing value, non nil default",
-			key:          AllowedSuggestedExitNodes,
+			key:          pkey.AllowedSuggestedExitNodes,
 			handlerError: ErrNotConfigured,
 			handlerError: ErrNotConfigured,
 			defaultValue: []string{"foo", "bar"},
 			defaultValue: []string{"foo", "bar"},
 			wantValue:    []string{"foo", "bar"},
 			wantValue:    []string{"foo", "bar"},
@@ -553,7 +554,7 @@ func TestGetStringArray(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:         "reading value returns other error",
 			name:         "reading value returns other error",
-			key:          AllowedSuggestedExitNodes,
+			key:          pkey.AllowedSuggestedExitNodes,
 			handlerError: someOtherError,
 			handlerError: someOtherError,
 			wantError:    someOtherError,
 			wantError:    someOtherError,
 			wantMetrics: []metrics.TestState{
 			wantMetrics: []metrics.TestState{
@@ -607,11 +608,11 @@ func BenchmarkGetString(b *testing.B) {
 	RegisterWellKnownSettingsForTest(b)
 	RegisterWellKnownSettingsForTest(b)
 
 
 	wantControlURL := "https://login.tailscale.com"
 	wantControlURL := "https://login.tailscale.com"
-	registerSingleSettingStoreForTest(b, source.TestSettingOf(ControlURL, wantControlURL))
+	registerSingleSettingStoreForTest(b, source.TestSettingOf(pkey.ControlURL, wantControlURL))
 
 
 	b.ResetTimer()
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		gotControlURL, _ := GetString(ControlURL, "https://controlplane.tailscale.com")
+		gotControlURL, _ := GetString(pkey.ControlURL, "https://controlplane.tailscale.com")
 		if gotControlURL != wantControlURL {
 		if gotControlURL != wantControlURL {
 			b.Fatalf("got %v; want %v", gotControlURL, wantControlURL)
 			b.Fatalf("got %v; want %v", gotControlURL, wantControlURL)
 		}
 		}