diff --git a/cmd/tailscaled/flag.go b/cmd/tailscaled/flag.go new file mode 100644 index 000000000..f640aceed --- /dev/null +++ b/cmd/tailscaled/flag.go @@ -0,0 +1,31 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package main + +import "strconv" + +// boolFlag is a flag.Value that tracks whether it was ever set. +type boolFlag struct { + set bool + v bool +} + +func (b *boolFlag) String() string { + if b == nil || !b.set { + return "unset" + } + return strconv.FormatBool(b.v) +} + +func (b *boolFlag) Set(s string) error { + v, err := strconv.ParseBool(s) + if err != nil { + return err + } + b.v = v + b.set = true + return nil +} + +func (b *boolFlag) IsBoolFlag() bool { return true } diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index 27fec05a3..c3a4c8b05 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -120,7 +120,7 @@ var args struct { debug string port uint16 statepath string - encryptState bool + encryptState boolFlag statedir string socketpath string birdSocketPath string @@ -197,7 +197,7 @@ func main() { flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`) flag.Var(flagtype.PortValue(&args.port, defaultPort()), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select") flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an ephemeral node. If empty and --statedir is provided, the default is /tailscaled.state. Default: "+paths.DefaultTailscaledStateFile()) - flag.BoolVar(&args.encryptState, "encrypt-state", defaultEncryptState(), "encrypt the state file on disk; uses TPM on Linux and Windows, on all other platforms this flag is not supported") + flag.Var(&args.encryptState, "encrypt-state", `encrypt the state file on disk; when not set encryption will be enabled if supported on this platform; uses TPM on Linux and Windows, on all other platforms this flag is not supported`) flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.") flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket") flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket") @@ -275,7 +275,10 @@ func main() { } } - if args.encryptState { + if !args.encryptState.set { + args.encryptState.v = defaultEncryptState() + } + if args.encryptState.v { if runtime.GOOS != "linux" && runtime.GOOS != "windows" { log.SetFlags(0) log.Fatalf("--encrypt-state is not supported on %s", runtime.GOOS) @@ -351,7 +354,7 @@ func statePathOrDefault() string { if path == "" && args.statedir != "" { path = filepath.Join(args.statedir, "tailscaled.state") } - if path != "" && !store.HasKnownProviderPrefix(path) && args.encryptState { + if path != "" && !store.HasKnownProviderPrefix(path) && args.encryptState.v { path = store.TPMPrefix + path } return path @@ -909,6 +912,6 @@ func defaultEncryptState() bool { // (plan9/FreeBSD/etc). return false } - v, _ := policyclient.Get().GetBoolean(pkey.EncryptState, false) + v, _ := policyclient.Get().GetBoolean(pkey.EncryptState, feature.TPMAvailable()) return v } diff --git a/feature/hooks.go b/feature/hooks.go index bc42bd8d9..2eade1ead 100644 --- a/feature/hooks.go +++ b/feature/hooks.go @@ -40,3 +40,15 @@ var HookProxySetSelfProxy Hook[func(...string)] // HookProxySetTransportGetProxyConnectHeader is a hook for feature/useproxy to register // [tshttpproxy.SetTransportGetProxyConnectHeader]. var HookProxySetTransportGetProxyConnectHeader Hook[func(*http.Transport)] + +// HookTPMAvailable is a hook that reports whether a TPM device is supported +// and available. +var HookTPMAvailable Hook[func() bool] + +// TPMAvailable reports whether a TPM device is supported and available. +func TPMAvailable() bool { + if f, ok := HookTPMAvailable.GetOk(); ok { + return f() + } + return false +} diff --git a/feature/tpm/tpm.go b/feature/tpm/tpm.go index b700637e6..b67cb4e3b 100644 --- a/feature/tpm/tpm.go +++ b/feature/tpm/tpm.go @@ -39,6 +39,7 @@ var infoOnce = sync.OnceValue(info) func init() { feature.Register("tpm") + feature.HookTPMAvailable.Set(tpmSupported) hostinfo.RegisterHostinfoNewHook(func(hi *tailcfg.Hostinfo) { hi.TPM = infoOnce() }) @@ -51,6 +52,15 @@ func init() { } } +func tpmSupported() bool { + tpm, err := open() + if err != nil { + return false + } + tpm.Close() + return true +} + var verboseTPM = envknob.RegisterBool("TS_DEBUG_TPM") func info() *tailcfg.TPMInfo { diff --git a/feature/tpm/tpm_test.go b/feature/tpm/tpm_test.go index f4497f8c7..5401fd5c3 100644 --- a/feature/tpm/tpm_test.go +++ b/feature/tpm/tpm_test.go @@ -277,15 +277,6 @@ func TestMigrateStateToTPM(t *testing.T) { } } -func tpmSupported() bool { - tpm, err := open() - if err != nil { - return false - } - tpm.Close() - return true -} - type mockTPMSealProvider struct { path string data map[ipn.StateKey][]byte diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index e8952216b..965768660 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -7559,11 +7559,7 @@ func (b *LocalBackend) stateEncrypted() opt.Bool { case version.IsMacAppStore(): return opt.NewBool(true) case version.IsMacSysExt(): - // MacSys still stores its state in plaintext on disk in addition to - // the Keychain. A future release will clean up the on-disk state - // files. - // TODO(#15830): always return true here once MacSys is fully migrated. - sp, _ := b.polc.GetBoolean(pkey.EncryptState, false) + sp, _ := b.polc.GetBoolean(pkey.EncryptState, true) return opt.NewBool(sp) default: // Probably self-compiled tailscaled, we don't use the Keychain diff --git a/util/syspolicy/pkey/pkey.go b/util/syspolicy/pkey/pkey.go index 1ef969d72..79b4af1e6 100644 --- a/util/syspolicy/pkey/pkey.go +++ b/util/syspolicy/pkey/pkey.go @@ -136,7 +136,9 @@ const ( FlushDNSOnSessionUnlock Key = "FlushDNSOnSessionUnlock" // EncryptState is a boolean setting that specifies whether to encrypt the - // tailscaled state file with a TPM device. + // tailscaled state file. + // Windows and Linux use a TPM device, Apple uses the Keychain. + // It's a noop on other platforms. EncryptState Key = "EncryptState" // PostureChecking indicates if posture checking is enabled and the client shall gather