mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-29 23:33:45 +00:00
tailcfg: report StateEncrypted in Hostinfo (#16434)
Report whether the client is configured with state encryption (which varies by platform and can be optional on some). Wire it up to `--encrypt-state` in tailscaled, which is set for Linux/Windows, and set defaults for other platforms. Macsys will also report this if full Keychain migration is done. Updates #15830 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
parent
d2edf7133a
commit
172e26b3e3
@ -159,6 +159,8 @@ func newStore(logf logger.Logf, path string) (ipn.StateStore, error) {
|
|||||||
// tpmStore is an ipn.StateStore that stores the state in a secretbox-encrypted
|
// tpmStore is an ipn.StateStore that stores the state in a secretbox-encrypted
|
||||||
// file using a TPM-sealed symmetric key.
|
// file using a TPM-sealed symmetric key.
|
||||||
type tpmStore struct {
|
type tpmStore struct {
|
||||||
|
ipn.EncryptedStateStore
|
||||||
|
|
||||||
logf logger.Logf
|
logf logger.Logf
|
||||||
path string
|
path string
|
||||||
key [32]byte
|
key [32]byte
|
||||||
|
@ -2244,6 +2244,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
|||||||
hostinfo.Userspace.Set(b.sys.IsNetstack())
|
hostinfo.Userspace.Set(b.sys.IsNetstack())
|
||||||
hostinfo.UserspaceRouter.Set(b.sys.IsNetstackRouter())
|
hostinfo.UserspaceRouter.Set(b.sys.IsNetstackRouter())
|
||||||
hostinfo.AppConnector.Set(b.appConnector != nil)
|
hostinfo.AppConnector.Set(b.appConnector != nil)
|
||||||
|
hostinfo.StateEncrypted = b.stateEncrypted()
|
||||||
b.logf.JSON(1, "Hostinfo", hostinfo)
|
b.logf.JSON(1, "Hostinfo", hostinfo)
|
||||||
|
|
||||||
// TODO(apenwarr): avoid the need to reinit controlclient.
|
// TODO(apenwarr): avoid the need to reinit controlclient.
|
||||||
@ -7801,3 +7802,29 @@ func (b *LocalBackend) vipServicesFromPrefsLocked(prefs ipn.PrefsView) []*tailcf
|
|||||||
var (
|
var (
|
||||||
metricCurrentWatchIPNBus = clientmetric.NewGauge("localbackend_current_watch_ipn_bus")
|
metricCurrentWatchIPNBus = clientmetric.NewGauge("localbackend_current_watch_ipn_bus")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (b *LocalBackend) stateEncrypted() opt.Bool {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "android", "ios":
|
||||||
|
return opt.NewBool(true)
|
||||||
|
case "darwin":
|
||||||
|
switch {
|
||||||
|
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, _ := syspolicy.GetBoolean(syspolicy.EncryptState, false)
|
||||||
|
return opt.NewBool(sp)
|
||||||
|
default:
|
||||||
|
// Probably self-compiled tailscaled, we don't use the Keychain
|
||||||
|
// there.
|
||||||
|
return opt.NewBool(false)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
_, ok := b.store.(ipn.EncryptedStateStore)
|
||||||
|
return opt.NewBool(ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -113,3 +113,9 @@ func ReadStoreInt(store StateStore, id StateKey) (int64, error) {
|
|||||||
func PutStoreInt(store StateStore, id StateKey, val int64) error {
|
func PutStoreInt(store StateStore, id StateKey, val int64) error {
|
||||||
return WriteState(store, id, fmt.Appendf(nil, "%d", val))
|
return WriteState(store, id, fmt.Appendf(nil, "%d", val))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EncryptedStateStore is a marker interface implemented by StateStores that
|
||||||
|
// encrypt data at rest.
|
||||||
|
type EncryptedStateStore interface {
|
||||||
|
stateStoreIsEncrypted()
|
||||||
|
}
|
||||||
|
@ -162,7 +162,8 @@ type CapabilityVersion int
|
|||||||
// - 115: 2025-03-07: Client understands DERPRegion.NoMeasureNoHome.
|
// - 115: 2025-03-07: Client understands DERPRegion.NoMeasureNoHome.
|
||||||
// - 116: 2025-05-05: Client serves MagicDNS "AAAA" if NodeAttrMagicDNSPeerAAAA set on self node
|
// - 116: 2025-05-05: Client serves MagicDNS "AAAA" if NodeAttrMagicDNSPeerAAAA set on self node
|
||||||
// - 117: 2025-05-28: Client understands DisplayMessages (structured health messages), but not necessarily PrimaryAction.
|
// - 117: 2025-05-28: Client understands DisplayMessages (structured health messages), but not necessarily PrimaryAction.
|
||||||
const CurrentCapabilityVersion CapabilityVersion = 117
|
// - 118: 2025-07-01: Client sends Hostinfo.StateEncrypted to report whether the state file is encrypted at rest (#15830)
|
||||||
|
const CurrentCapabilityVersion CapabilityVersion = 118
|
||||||
|
|
||||||
// ID is an integer ID for a user, node, or login allocated by the
|
// ID is an integer ID for a user, node, or login allocated by the
|
||||||
// control plane.
|
// control plane.
|
||||||
@ -878,6 +879,12 @@ type Hostinfo struct {
|
|||||||
Location *Location `json:",omitempty"`
|
Location *Location `json:",omitempty"`
|
||||||
|
|
||||||
TPM *TPMInfo `json:",omitempty"` // TPM device metadata, if available
|
TPM *TPMInfo `json:",omitempty"` // TPM device metadata, if available
|
||||||
|
// StateEncrypted reports whether the node state is stored encrypted on
|
||||||
|
// disk. The actual mechanism is platform-specific:
|
||||||
|
// * Apple nodes use the Keychain
|
||||||
|
// * Linux and Windows nodes use the TPM
|
||||||
|
// * Android apps use EncryptedSharedPreferences
|
||||||
|
StateEncrypted opt.Bool `json:",omitempty"`
|
||||||
|
|
||||||
// NOTE: any new fields containing pointers in this type
|
// NOTE: any new fields containing pointers in this type
|
||||||
// require changes to Hostinfo.Equal.
|
// require changes to Hostinfo.Equal.
|
||||||
|
@ -188,6 +188,7 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
|
|||||||
ServicesHash string
|
ServicesHash string
|
||||||
Location *Location
|
Location *Location
|
||||||
TPM *TPMInfo
|
TPM *TPMInfo
|
||||||
|
StateEncrypted opt.Bool
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
// Clone makes a deep copy of NetInfo.
|
// Clone makes a deep copy of NetInfo.
|
||||||
|
@ -69,6 +69,7 @@ func TestHostinfoEqual(t *testing.T) {
|
|||||||
"ServicesHash",
|
"ServicesHash",
|
||||||
"Location",
|
"Location",
|
||||||
"TPM",
|
"TPM",
|
||||||
|
"StateEncrypted",
|
||||||
}
|
}
|
||||||
if have := fieldsOf(reflect.TypeFor[Hostinfo]()); !reflect.DeepEqual(have, hiHandles) {
|
if have := fieldsOf(reflect.TypeFor[Hostinfo]()); !reflect.DeepEqual(have, hiHandles) {
|
||||||
t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||||
|
@ -303,6 +303,7 @@ func (v HostinfoView) ServicesHash() string { return v.ж.Serv
|
|||||||
func (v HostinfoView) Location() LocationView { return v.ж.Location.View() }
|
func (v HostinfoView) Location() LocationView { return v.ж.Location.View() }
|
||||||
func (v HostinfoView) TPM() views.ValuePointer[TPMInfo] { return views.ValuePointerOf(v.ж.TPM) }
|
func (v HostinfoView) TPM() views.ValuePointer[TPMInfo] { return views.ValuePointerOf(v.ж.TPM) }
|
||||||
|
|
||||||
|
func (v HostinfoView) StateEncrypted() opt.Bool { return v.ж.StateEncrypted }
|
||||||
func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
|
func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
|
||||||
|
|
||||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||||
@ -346,6 +347,7 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
|
|||||||
ServicesHash string
|
ServicesHash string
|
||||||
Location *Location
|
Location *Location
|
||||||
TPM *TPMInfo
|
TPM *TPMInfo
|
||||||
|
StateEncrypted opt.Bool
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
// View returns a read-only view of NetInfo.
|
// View returns a read-only view of NetInfo.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user