cmd/tailscaled: default --encrypt-state to true if TPM is available (#17376)

Whenever running on a platform that has a TPM (and tailscaled can access
it), default to encrypting the state. The user can still explicitly set
this flag to disable encryption.

Updates https://github.com/tailscale/corp/issues/32909

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
Andrew Lytvynov
2025-10-01 20:18:58 -07:00
committed by GitHub
parent 78af49dd1a
commit cca70ddbfc
7 changed files with 65 additions and 20 deletions

31
cmd/tailscaled/flag.go Normal file
View File

@@ -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 }

View File

@@ -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:<secret-name>' 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 <statedir>/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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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