cmd/tl-longchain: tool to re-sign nodes with long rotation signatures

In Tailnet Lock, there is an implicit limit on the number of rotation
signatures that can be chained before the signature becomes too long.

This program helps tailnet admins to identify nodes that have signatures
with long chains and prints commands to re-sign those node keys with a
fresh direct signature. It's a temporary mitigation measure, and we will
remove this tool as we design and implement a long-term approach for
rotation signatures.

Example output:

```
2024/08/20 18:25:03 Self: does not need re-signing
2024/08/20 18:25:03 Visible peers with valid signatures:
2024/08/20 18:25:03 Peer xxx2.yy.ts.net. (100.77.192.34) nodeid=nyDmhiZiGA11KTM59, current signature kind=direct: does not need re-signing
2024/08/20 18:25:03 Peer xxx3.yy.ts.net. (100.84.248.22) nodeid=ndQ64mDnaB11KTM59, current signature kind=direct: does not need re-signing
2024/08/20 18:25:03 Peer xxx4.yy.ts.net. (100.85.253.53) nodeid=nmZfVygzkB21KTM59, current signature kind=rotation: chain length 4, printing command to re-sign
tailscale lock sign nodekey:530bddbfbe69e91fe15758a1d6ead5337aa6307e55ac92dafad3794f8b3fc661 tlpub:4bf07597336703395f2149dce88e7c50dd8694ab5bbde3d7c2a1c7b3e231a3c2
```

To support this, the NetworkLockStatus localapi response now includes
information about signatures of all peers rather than just the invalid
ones. This is not displayed by default in `tailscale lock status`, but
will be surfaced in `tailscale lock status --json`.

Updates #13185

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
This commit is contained in:
Anton Tolchanov
2024-08-20 18:36:30 +01:00
committed by Anton Tolchanov
parent 7d83056a1b
commit 151b77f9d6
6 changed files with 185 additions and 37 deletions

View File

@@ -53,7 +53,7 @@ type tkaState struct {
profile ipn.ProfileID
authority *tka.Authority
storage *tka.FS
filtered []ipnstate.TKAFilteredPeer
filtered []ipnstate.TKAPeer
}
// tkaFilterNetmapLocked checks the signatures on each node key, dropping
@@ -99,7 +99,7 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
// nm.Peers is ordered, so deletion must be order-preserving.
if len(toDelete) > 0 || len(obsoleteByRotation) > 0 {
peers := make([]tailcfg.NodeView, 0, len(nm.Peers))
filtered := make([]ipnstate.TKAFilteredPeer, 0, len(toDelete)+len(obsoleteByRotation))
filtered := make([]ipnstate.TKAPeer, 0, len(toDelete)+len(obsoleteByRotation))
for i, p := range nm.Peers {
if !toDelete[i] && !obsoleteByRotation.Contains(p.Key()) {
peers = append(peers, p)
@@ -108,20 +108,7 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
b.logf("Network lock is dropping peer %v(%v) due to key rotation", p.ID(), p.StableID())
}
// Record information about the node we filtered out.
fp := ipnstate.TKAFilteredPeer{
Name: p.Name(),
ID: p.ID(),
StableID: p.StableID(),
TailscaleIPs: make([]netip.Addr, p.Addresses().Len()),
NodeKey: p.Key(),
}
for i := range p.Addresses().Len() {
addr := p.Addresses().At(i)
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
fp.TailscaleIPs[i] = addr.Addr()
}
}
filtered = append(filtered, fp)
filtered = append(filtered, tkaStateFromPeer(p))
}
}
nm.Peers = peers
@@ -546,11 +533,17 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
}
}
filtered := make([]*ipnstate.TKAFilteredPeer, len(b.tka.filtered))
filtered := make([]*ipnstate.TKAPeer, len(b.tka.filtered))
for i := range len(filtered) {
filtered[i] = b.tka.filtered[i].Clone()
}
visible := make([]*ipnstate.TKAPeer, len(b.netMap.Peers))
for i, p := range b.netMap.Peers {
s := tkaStateFromPeer(p)
visible[i] = &s
}
stateID1, _ := b.tka.authority.StateIDs()
return &ipnstate.NetworkLockStatus{
@@ -562,10 +555,32 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
NodeKeySignature: nodeKeySignature,
TrustedKeys: outKeys,
FilteredPeers: filtered,
VisiblePeers: visible,
StateID: stateID1,
}
}
func tkaStateFromPeer(p tailcfg.NodeView) ipnstate.TKAPeer {
fp := ipnstate.TKAPeer{
Name: p.Name(),
ID: p.ID(),
StableID: p.StableID(),
TailscaleIPs: make([]netip.Addr, 0, p.Addresses().Len()),
NodeKey: p.Key(),
}
for i := range p.Addresses().Len() {
addr := p.Addresses().At(i)
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
fp.TailscaleIPs = append(fp.TailscaleIPs, addr.Addr())
}
}
var decoded tka.NodeKeySignature
if err := decoded.Unserialize(p.KeySignature().AsSlice()); err == nil {
fp.NodeKeySignature = decoded
}
return fp
}
// NetworkLockInit enables network-lock for the tailnet, with the tailnets'
// key authority initialized to trust the provided keys.
//

View File

@@ -26,7 +26,7 @@ import (
"tailscale.com/version"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=TKAFilteredPeer
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=TKAPeer
// Status represents the entire state of the IPN network.
type Status struct {
@@ -94,15 +94,14 @@ type TKAKey struct {
Votes uint
}
// TKAFilteredPeer describes a peer which was removed from the netmap
// (i.e. no connectivity) because it failed tailnet lock
// checks.
type TKAFilteredPeer struct {
Name string // DNS
ID tailcfg.NodeID
StableID tailcfg.StableNodeID
TailscaleIPs []netip.Addr // Tailscale IP(s) assigned to this node
NodeKey key.NodePublic
// TKAPeer describes a peer and its network lock details.
type TKAPeer struct {
Name string // DNS
ID tailcfg.NodeID
StableID tailcfg.StableNodeID
TailscaleIPs []netip.Addr // Tailscale IP(s) assigned to this node
NodeKey key.NodePublic
NodeKeySignature tka.NodeKeySignature
}
// NetworkLockStatus represents whether network-lock is enabled,
@@ -134,10 +133,14 @@ type NetworkLockStatus struct {
// to network-lock.
TrustedKeys []TKAKey
// VisiblePeers describes peers which are visible in the netmap that
// have valid Tailnet Lock signatures signatures.
VisiblePeers []*TKAPeer
// FilteredPeers describes peers which were removed from the netmap
// (i.e. no connectivity) because they failed tailnet lock
// checks.
FilteredPeers []*TKAFilteredPeer
FilteredPeers []*TKAPeer
// StateID is a nonce associated with the network lock authority,
// generated upon enablement. This field is not populated if the

View File

@@ -9,26 +9,29 @@ import (
"net/netip"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/key"
)
// Clone makes a deep copy of TKAFilteredPeer.
// Clone makes a deep copy of TKAPeer.
// The result aliases no memory with the original.
func (src *TKAFilteredPeer) Clone() *TKAFilteredPeer {
func (src *TKAPeer) Clone() *TKAPeer {
if src == nil {
return nil
}
dst := new(TKAFilteredPeer)
dst := new(TKAPeer)
*dst = *src
dst.TailscaleIPs = append(src.TailscaleIPs[:0:0], src.TailscaleIPs...)
dst.NodeKeySignature = *src.NodeKeySignature.Clone()
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TKAFilteredPeerCloneNeedsRegeneration = TKAFilteredPeer(struct {
Name string
ID tailcfg.NodeID
StableID tailcfg.StableNodeID
TailscaleIPs []netip.Addr
NodeKey key.NodePublic
var _TKAPeerCloneNeedsRegeneration = TKAPeer(struct {
Name string
ID tailcfg.NodeID
StableID tailcfg.StableNodeID
TailscaleIPs []netip.Addr
NodeKey key.NodePublic
NodeKeySignature tka.NodeKeySignature
}{})