tka: reject removal of the last signing key

Fixes tailscale/corp#19447

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
This commit is contained in:
Anton Tolchanov 2025-06-13 14:45:28 +01:00 committed by Anton Tolchanov
parent 59fab8bda7
commit 42da161b19
3 changed files with 25 additions and 0 deletions

View File

@ -326,6 +326,9 @@ func runNetworkLockRemove(ctx context.Context, args []string) error {
if !st.Enabled {
return errors.New("tailnet lock is not enabled")
}
if len(st.TrustedKeys) == 1 {
return errors.New("cannot remove the last trusted signing key; use 'tailscale lock disable' to disable tailnet lock instead, or add another signing key before removing one")
}
if nlRemoveArgs.resign {
// Validate we are not removing trust in ourselves while resigning. This is because

View File

@ -5,6 +5,7 @@ package tka
import (
"crypto/ed25519"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
@ -90,6 +91,20 @@ func TestAuthorityBuilderRemoveKey(t *testing.T) {
if _, err := a.state.GetKey(key2.MustID()); err != ErrNoSuchKey {
t.Errorf("GetKey(key2).err = %v, want %v", err, ErrNoSuchKey)
}
// Check that removing the remaining key errors out.
b = a.NewUpdater(signer25519(priv))
if err := b.RemoveKey(key.MustID()); err != nil {
t.Fatalf("RemoveKey(%v) failed: %v", key, err)
}
updates, err = b.Finalize(storage)
if err != nil {
t.Fatalf("Finalize() failed: %v", err)
}
wantErr := "cannot remove the last key"
if err := a.Inform(storage, updates); err == nil || !strings.Contains(err.Error(), wantErr) {
t.Fatalf("expected Inform() to return error %q, got: %v", wantErr, err)
}
}
func TestAuthorityBuilderSetKeyVote(t *testing.T) {

View File

@ -440,6 +440,13 @@ func aumVerify(aum AUM, state State, isGenesisAUM bool) error {
return fmt.Errorf("signature %d: %v", i, err)
}
}
if aum.MessageKind == AUMRemoveKey && len(state.Keys) == 1 {
if kid, err := state.Keys[0].ID(); err == nil && bytes.Equal(aum.KeyID, kid) {
return errors.New("cannot remove the last key in the state")
}
}
return nil
}