94 lines
3.0 KiB
Go
94 lines
3.0 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Program tl-longchain prints commands to re-sign Tailscale nodes that have
|
|
// long rotation signature chains.
|
|
//
|
|
// 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.
|
|
// Commands are printed to stdout, while log messages are printed to stderr.
|
|
//
|
|
// Note that the Tailscale client this command is executed on must have
|
|
// ACL visibility to all other nodes to be able to see their signatures.
|
|
// https://tailscale.com/kb/1087/device-visibility
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"tailscale.com/client/tailscale"
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/tka"
|
|
"tailscale.com/types/key"
|
|
)
|
|
|
|
var (
|
|
flagSocket = flag.String("socket", "", "custom path to tailscaled socket")
|
|
maxRotations = flag.Int("rotations", 10, "number of rotation signatures before re-signing (max 16)")
|
|
showFiltered = flag.Bool("show-filtered", false, "include nodes with invalid signatures")
|
|
)
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
lc := tailscale.LocalClient{Socket: *flagSocket}
|
|
if lc.Socket != "" {
|
|
lc.UseSocketOnly = true
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
st, err := lc.NetworkLockStatus(ctx)
|
|
if err != nil {
|
|
log.Fatalf("could not get Tailnet Lock status: %v", err)
|
|
}
|
|
if !st.Enabled {
|
|
log.Print("Tailnet Lock is not enabled")
|
|
return
|
|
}
|
|
print("Self", *st.NodeKey, *st.NodeKeySignature)
|
|
if len(st.VisiblePeers) > 0 {
|
|
log.Print("Visible peers with valid signatures:")
|
|
for _, peer := range st.VisiblePeers {
|
|
print(peerInfo(peer), peer.NodeKey, peer.NodeKeySignature)
|
|
}
|
|
}
|
|
if *showFiltered && len(st.FilteredPeers) > 0 {
|
|
log.Print("Visible peers with invalid signatures:")
|
|
for _, peer := range st.FilteredPeers {
|
|
print(peerInfo(peer), peer.NodeKey, peer.NodeKeySignature)
|
|
}
|
|
}
|
|
}
|
|
|
|
// peerInfo returns a string with information about a peer.
|
|
func peerInfo(peer *ipnstate.TKAPeer) string {
|
|
return fmt.Sprintf("Peer %s (%s) nodeid=%s, current signature kind=%v", peer.Name, peer.TailscaleIPs[0], peer.StableID, peer.NodeKeySignature.SigKind)
|
|
}
|
|
|
|
// print prints a message about a node key signature and a re-signing command if needed.
|
|
func print(info string, nodeKey key.NodePublic, sig tka.NodeKeySignature) {
|
|
if l := chainLength(sig); l > *maxRotations {
|
|
log.Printf("%s: chain length %d, printing command to re-sign", info, l)
|
|
wrapping, _ := sig.UnverifiedWrappingPublic()
|
|
fmt.Printf("tailscale lock sign %s %s\n", nodeKey, key.NLPublicFromEd25519Unsafe(wrapping).CLIString())
|
|
} else {
|
|
log.Printf("%s: does not need re-signing", info)
|
|
}
|
|
}
|
|
|
|
// chainLength returns the length of the rotation signature chain.
|
|
func chainLength(sig tka.NodeKeySignature) int {
|
|
if sig.SigKind != tka.SigRotation {
|
|
return 1
|
|
}
|
|
return 1 + chainLength(*sig.Nested)
|
|
}
|