all: store NL keys per profile

This moves the NetworkLock key from a dedicated StateKey to be part of the persist.Persist struct.
This struct is stored as part for ipn.Prefs and is also the place where we store the NodeKey.

It also moves the ChonkDir from "/tka" to "/tka-profile/<profile-id>". The rename was intentional
to be able to delete the "/tka" dir if it exists.

This means that we will have a unique key per profile, and a unique directory per profile.

Note: `tailscale logout` will delete the entire profile, including any keys. It currently does not
delete the ChonkDir.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
Maisem Ali
2022-11-14 17:29:49 +05:00
committed by Tom
parent 751f866f01
commit 235309adc4
13 changed files with 198 additions and 167 deletions

View File

@@ -159,7 +159,6 @@ type LocalBackend struct {
ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto
inServerMode bool
machinePrivKey key.MachinePrivate
nlPrivKey key.NLPrivate
tka *tkaState
state ipn.State
capFileSharing bool // whether netMap contains the file sharing capability
@@ -832,6 +831,9 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
if !prefs.Persist.View().Equals(*st.Persist) {
prefsChanged = true
prefs.Persist = st.Persist.AsStruct()
if err := b.initTKALocked(); err != nil {
b.logf("initTKALocked: %v", err)
}
}
}
if st.URL != "" {
@@ -1174,9 +1176,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
return fmt.Errorf("initMachineKeyLocked: %w", err)
}
}
if err := b.initNLKeyLocked(); err != nil {
return fmt.Errorf("initNLKeyLocked: %w", err)
}
loggedOut := prefs.LoggedOut()
@@ -1232,7 +1231,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
// but it won't take effect until the next Start().
cc, err := b.getNewControlClientFunc()(controlclient.Options{
GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(),
GetNLPrivateKey: b.createGetNLPrivateKeyFunc(),
Logf: logger.WithPrefix(b.logf, "control: "),
Persist: *persistv,
ServerURL: b.serverURL,
@@ -1759,21 +1757,6 @@ func (b *LocalBackend) createGetMachinePrivateKeyFunc() func() (key.MachinePriva
}
}
func (b *LocalBackend) createGetNLPrivateKeyFunc() func() (key.NLPrivate, error) {
var cache syncs.AtomicValue[key.NLPrivate]
return func() (key.NLPrivate, error) {
b.mu.Lock()
defer b.mu.Unlock()
if v, ok := cache.LoadOk(); ok {
return v, nil
}
priv := b.nlPrivKey
cache.Store(priv)
return priv, nil
}
}
// initMachineKeyLocked is called to initialize b.machinePrivKey.
//
// b.prefs must already be initialized.
@@ -1827,46 +1810,6 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
return nil
}
// initNLKeyLocked is called to initialize b.nlPrivKey.
//
// b.prefs must already be initialized.
//
// b.stateKey should be set too, but just for nicer log messages.
// b.mu must be held.
func (b *LocalBackend) initNLKeyLocked() (err error) {
if !b.nlPrivKey.IsZero() {
// Already set.
return nil
}
keyText, err := b.store.ReadState(ipn.NLKeyStateKey)
if err == nil {
if err := b.nlPrivKey.UnmarshalText(keyText); err != nil {
return fmt.Errorf("invalid key in %s key of %v: %w", ipn.NLKeyStateKey, b.store, err)
}
if b.nlPrivKey.IsZero() {
return fmt.Errorf("invalid zero key stored in %v key of %v", ipn.NLKeyStateKey, b.store)
}
return nil
}
if err != ipn.ErrStateNotExist {
return fmt.Errorf("error reading %v key of %v: %w", ipn.NLKeyStateKey, b.store, err)
}
// If we didn't find one already on disk, generate a new one.
b.logf("generating new network-lock key")
b.nlPrivKey = key.NewNLPrivate()
keyText, _ = b.nlPrivKey.MarshalText()
if err := b.store.WriteState(ipn.NLKeyStateKey, keyText); err != nil {
b.logf("error writing network-lock key to store: %v", err)
return err
}
b.logf("network-lock key written to store")
return nil
}
// migrateStateLocked migrates state from the frontend to the backend.
// It is a no-op if prefs is nil
// b.mu must be held.
@@ -2705,17 +2648,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
return dcfg
}
// SetTailnetKeyAuthority sets the key authority which should be
// used for locked tailnets.
//
// It should only be called before the LocalBackend is used.
func (b *LocalBackend) SetTailnetKeyAuthority(a *tka.Authority, storage *tka.FS) {
b.tka = &tkaState{
authority: a,
storage: storage,
}
}
// SetVarRoot sets the root directory of Tailscale's writable
// storage area . (e.g. "/var/lib/tailscale")
//
@@ -4053,11 +3985,58 @@ func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
return b.resetForProfileChangeLockedOnEntry()
}
func (b *LocalBackend) initTKALocked() error {
cp := b.pm.CurrentProfile()
if cp.ID == "" {
b.tka = nil
return nil
}
if b.tka != nil {
if b.tka.profile == cp.ID {
// Already initialized.
return nil
}
// As we're switching profiles, we need to reset the TKA to nil.
b.tka = nil
}
root := b.TailscaleVarRoot()
if root == "" {
b.tka = nil
b.logf("network-lock unavailable; no state directory")
return nil
}
chonkDir := b.chonkPathLocked()
if _, err := os.Stat(chonkDir); err == nil {
// The directory exists, which means network-lock has been initialized.
storage, err := tka.ChonkDir(chonkDir)
if err != nil {
return fmt.Errorf("opening tailchonk: %v", err)
}
authority, err := tka.Open(storage)
if err != nil {
return fmt.Errorf("initializing tka: %v", err)
}
b.tka = &tkaState{
profile: cp.ID,
authority: authority,
storage: storage,
}
b.logf("tka initialized at head %x", authority.Head())
}
return nil
}
// resetForProfileChangeLockedOnEntry resets the backend for a profile change.
func (b *LocalBackend) resetForProfileChangeLockedOnEntry() error {
b.setNetMapLocked(nil) // Reset netmap.
// Reset the NetworkMap in the engine
b.e.SetNetworkMap(new(netmap.NetworkMap))
if err := b.initTKALocked(); err != nil {
return err
}
b.enterStateLockedOnEntry(ipn.NoState) // Reset state.
return b.Start(ipn.Options{})
}

View File

@@ -35,6 +35,7 @@ var (
)
type tkaState struct {
profile ipn.ProfileID
authority *tka.Authority
storage *tka.FS
}
@@ -251,7 +252,7 @@ func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error {
// b.mu must be held & TKA must be initialized.
func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error {
if b.tka.authority.ValidDisablement(secret) {
if err := os.RemoveAll(b.chonkPath()); err != nil {
if err := os.RemoveAll(b.chonkPathLocked()); err != nil {
return err
}
b.tka = nil
@@ -260,10 +261,10 @@ func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error {
return errors.New("incorrect disablement secret")
}
// chonkPath returns the absolute path to the directory in which TKA
// chonkPathLocked returns the absolute path to the directory in which TKA
// state (the 'tailchonk') is stored.
func (b *LocalBackend) chonkPath() string {
return filepath.Join(b.TailscaleVarRoot(), "tka")
func (b *LocalBackend) chonkPathLocked() string {
return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID))
}
// tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the
@@ -280,7 +281,10 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) err
return fmt.Errorf("reading genesis: %v", err)
}
chonkDir := b.chonkPath()
chonkDir := b.chonkPathLocked()
if err := os.Mkdir(filepath.Dir(chonkDir), 0755); err != nil && !os.IsExist(err) {
return fmt.Errorf("creating chonk root dir: %v", err)
}
if err := os.Mkdir(chonkDir, 0755); err != nil && !os.IsExist(err) {
return fmt.Errorf("mkdir: %v", err)
}
@@ -295,6 +299,7 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) err
}
b.tka = &tkaState{
profile: b.pm.CurrentProfile().ID,
authority: authority,
storage: chonk,
}
@@ -329,17 +334,27 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
b.mu.Lock()
defer b.mu.Unlock()
var nodeKey *key.NodePublic
var (
nodeKey *key.NodePublic
nlPriv key.NLPrivate
)
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero() {
nkp := p.Persist().PublicNodeKey()
nodeKey = &nkp
nlPriv = p.Persist().NetworkLockKey
}
if nlPriv.IsZero() {
return &ipnstate.NetworkLockStatus{
Enabled: false,
NodeKey: nodeKey,
}
}
if b.tka == nil {
return &ipnstate.NetworkLockStatus{
Enabled: false,
PublicKey: b.nlPrivKey.Public(),
NodeKey: nodeKey,
PublicKey: nlPriv.Public(),
}
}
@@ -365,7 +380,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
return &ipnstate.NetworkLockStatus{
Enabled: true,
Head: &head,
PublicKey: b.nlPrivKey.Public(),
PublicKey: nlPriv.Public(),
NodeKey: nodeKey,
NodeKeySigned: selfAuthorized,
TrustedKeys: outKeys,
@@ -387,12 +402,14 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt
}
var ourNodeKey key.NodePublic
var nlPriv key.NLPrivate
b.mu.Lock()
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil && !p.Persist().PrivateNodeKey.IsZero() {
ourNodeKey = p.Persist().PublicNodeKey()
nlPriv = p.Persist().NetworkLockKey
}
b.mu.Unlock()
if ourNodeKey.IsZero() {
if ourNodeKey.IsZero() || nlPriv.IsZero() {
return errors.New("no node-key: is tailscale logged in?")
}
@@ -407,7 +424,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt
// - DisablementSecret: value needed to disable.
// - DisablementValue: the KDF of the disablement secret, a public value.
DisablementSecrets: disablementValues,
}, b.nlPrivKey)
}, nlPriv)
if err != nil {
return fmt.Errorf("tka.Create: %v", err)
}
@@ -430,7 +447,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt
// satisfy network-lock checks.
sigs := make(map[tailcfg.NodeID]tkatype.MarshaledSignature, len(initResp.NeedSignatures))
for _, nodeInfo := range initResp.NeedSignatures {
nks, err := signNodeKey(nodeInfo, b.nlPrivKey)
nks, err := signNodeKey(nodeInfo, nlPriv)
if err != nil {
return fmt.Errorf("generating signature: %v", err)
}
@@ -470,10 +487,18 @@ func (b *LocalBackend) NetworkLockSign(nodeKey key.NodePublic, rotationPublic []
b.mu.Lock()
defer b.mu.Unlock()
var nlPriv key.NLPrivate
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil {
nlPriv = p.Persist().NetworkLockKey
}
if nlPriv.IsZero() {
return key.NodePublic{}, tka.NodeKeySignature{}, errMissingNetmap
}
if b.tka == nil {
return key.NodePublic{}, tka.NodeKeySignature{}, errNetworkLockNotActive
}
if !b.tka.authority.KeyTrusted(b.nlPrivKey.KeyID()) {
if !b.tka.authority.KeyTrusted(nlPriv.KeyID()) {
return key.NodePublic{}, tka.NodeKeySignature{}, errors.New("this node is not trusted by network lock")
}
@@ -483,11 +508,11 @@ func (b *LocalBackend) NetworkLockSign(nodeKey key.NodePublic, rotationPublic []
}
sig := tka.NodeKeySignature{
SigKind: tka.SigDirect,
KeyID: b.nlPrivKey.KeyID(),
KeyID: nlPriv.KeyID(),
Pubkey: p,
WrappingPubkey: rotationPublic,
}
sig.Signature, err = b.nlPrivKey.SignNKS(sig.SigHash())
sig.Signature, err = nlPriv.SignNKS(sig.SigHash())
if err != nil {
return key.NodePublic{}, tka.NodeKeySignature{}, fmt.Errorf("signature failed: %w", err)
}
@@ -527,11 +552,18 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err
if err := b.CanSupportNetworkLock(); err != nil {
return err
}
var nlPriv key.NLPrivate
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist() != nil {
nlPriv = p.Persist().NetworkLockKey
}
if nlPriv.IsZero() {
return errMissingNetmap
}
if b.tka == nil {
return errNetworkLockNotActive
}
updater := b.tka.authority.NewUpdater(b.nlPrivKey)
updater := b.tka.authority.NewUpdater(nlPriv)
for _, addKey := range addKeys {
if err := updater.AddKey(addKey); err != nil {

View File

@@ -122,7 +122,10 @@ func TestTKAEnablementFlow(t *testing.T) {
cc := fakeControlClient(t, client)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
Persist: &persist.Persist{
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
b := LocalBackend{
varRoot: temp,
@@ -151,15 +154,25 @@ func TestTKAEnablementFlow(t *testing.T) {
func TestTKADisablementFlow(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode()
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
nlPriv := key.NewNLPrivate()
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
chonk, err := tka.ChonkDir(filepath.Join(temp, "tka"))
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
os.Mkdir(tkaPath, 0755)
chonk, err := tka.ChonkDir(tkaPath)
if err != nil {
t.Fatal(err)
}
@@ -217,10 +230,6 @@ func TestTKADisablementFlow(t *testing.T) {
defer ts.Close()
cc := fakeControlClient(t, client)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
}).View()))
b := LocalBackend{
varRoot: temp,
cc: cc,
@@ -260,7 +269,7 @@ func TestTKADisablementFlow(t *testing.T) {
if b.tka != nil {
t.Fatal("tka was not shut down")
}
if _, err := os.Stat(b.chonkPath()); err == nil || !os.IsNotExist(err) {
if _, err := os.Stat(b.chonkPathLocked()); err == nil || !os.IsNotExist(err) {
t.Errorf("os.Stat(chonkDir) = %v, want ErrNotExist", err)
}
}
@@ -345,10 +354,15 @@ func TestTKASync(t *testing.T) {
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode()
nlPriv := key.NewNLPrivate()
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
// Setup the tka authority on the control plane.
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
@@ -366,8 +380,11 @@ func TestTKASync(t *testing.T) {
}
}
temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
os.Mkdir(tkaPath, 0755)
// Setup the TKA authority on the node.
nodeStorage, err := tka.ChonkDir(filepath.Join(temp, "tka"))
nodeStorage, err := tka.ChonkDir(tkaPath)
if err != nil {
t.Fatal(err)
}
@@ -467,10 +484,6 @@ func TestTKASync(t *testing.T) {
// Setup the client.
cc := fakeControlClient(t, client)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
}).View()))
b := LocalBackend{
varRoot: temp,
cc: cc,
@@ -564,15 +577,25 @@ func TestTKAFilterNetmap(t *testing.T) {
func TestTKADisable(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode()
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
nlPriv := key.NewNLPrivate()
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
os.Mkdir(tkaPath, 0755)
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
chonk, err := tka.ChonkDir(filepath.Join(temp, "tka"))
chonk, err := tka.ChonkDir(tkaPath)
if err != nil {
t.Fatal(err)
}
@@ -623,17 +646,13 @@ func TestTKADisable(t *testing.T) {
defer ts.Close()
cc := fakeControlClient(t, client)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
}).View()))
b := LocalBackend{
varRoot: temp,
cc: cc,
ccAuto: cc,
logf: t.Logf,
tka: &tkaState{
profile: pm.CurrentProfile().ID,
authority: authority,
storage: chonk,
},
@@ -653,16 +672,26 @@ func TestTKADisable(t *testing.T) {
func TestTKASign(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode()
toSign := key.NewNode()
nlPriv := key.NewNLPrivate()
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
nlPriv := key.NewNLPrivate()
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
chonk, err := tka.ChonkDir(filepath.Join(temp, "tka"))
temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
os.Mkdir(tkaPath, 0755)
chonk, err := tka.ChonkDir(tkaPath)
if err != nil {
t.Fatal(err)
}
@@ -709,10 +738,6 @@ func TestTKASign(t *testing.T) {
}
}))
defer ts.Close()
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ""))
must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
}).View()))
cc := fakeControlClient(t, client)
b := LocalBackend{
varRoot: temp,
@@ -723,9 +748,8 @@ func TestTKASign(t *testing.T) {
authority: authority,
storage: chonk,
},
pm: pm,
store: pm.Store(),
nlPrivKey: nlPriv,
pm: pm,
store: pm.Store(),
}
if err := b.NetworkLockSign(toSign.Public(), nil); err != nil {

View File

@@ -252,8 +252,8 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
return savedPrefs.View(), nil
}
// CurrentProfile returns the name and ID of the current profile, or "" if the profile
// is not named.
// CurrentProfile returns the current LoginProfile.
// The value may be zero if the profile is not persisted.
func (pm *profileManager) CurrentProfile() ipn.LoginProfile {
return *pm.currentProfile
}