cmd/tailscale,ipn: improve UX of lock init command, cosmetic changes

Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
Tom DNetto 2022-11-23 11:19:30 -08:00 committed by Tom
parent e8cc78b1af
commit 5c8d2fa695
5 changed files with 115 additions and 30 deletions

View File

@ -787,14 +787,15 @@ func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.Network
// NetworkLockInit initializes the tailnet key authority. // NetworkLockInit initializes the tailnet key authority.
// //
// TODO(tom): Plumb through disablement secrets. // TODO(tom): Plumb through disablement secrets.
func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte) (*ipnstate.NetworkLockStatus, error) { func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
var b bytes.Buffer var b bytes.Buffer
type initRequest struct { type initRequest struct {
Keys []tka.Key Keys []tka.Key
DisablementValues [][]byte DisablementValues [][]byte
SupportDisablement []byte
} }
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues}); err != nil { if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues, SupportDisablement: supportDisablement}); err != nil {
return nil, err return nil, err
} }

View File

@ -6,6 +6,7 @@
import ( import (
"context" "context"
"crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
@ -40,11 +41,44 @@
Exec: runNetworkLockStatus, Exec: runNetworkLockStatus,
} }
var nlInitArgs struct {
numDisablements int
disablementForSupport bool
confirm bool
}
var nlInitCmd = &ffcli.Command{ var nlInitCmd = &ffcli.Command{
Name: "init", Name: "init",
ShortUsage: "init <public-key>...", ShortUsage: "init [--gen-disablement-for-support] --gen-disablements N <trusted-key>...",
ShortHelp: "Initialize the tailnet key authority", ShortHelp: "Initialize tailnet lock",
LongHelp: strings.TrimSpace(`
The 'tailscale lock init' command initializes tailnet lock across the
entire tailnet. The specified keys are initially trusted to sign nodes
or to make further changes to tailnet lock.
You can identify the key for a node you wish to trust by running 'tailscale lock'
on that node, and copying the node's tailnet lock key.
In the event that tailnet lock need be disabled, it can be disabled using
the 'tailscale lock disable' command and one of the disablement secrets.
The number of disablement secrets to be generated is specified using the
--gen-disablements flag. Initializing tailnet lock requires at least
one disablement.
If --gen-disablement-for-support is specified, an additional disablement secret
will be generated and transmitted to Tailscale, which support can use to disable
tailnet lock. We recommend setting this flag.
`),
Exec: runNetworkLockInit, Exec: runNetworkLockInit,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("lock init")
fs.IntVar(&nlInitArgs.numDisablements, "gen-disablements", 1, "number of disablement secrets to generate")
fs.BoolVar(&nlInitArgs.disablementForSupport, "gen-disablement-for-support", false, "generates and transmits a disablement secret for Tailscale support")
fs.BoolVar(&nlInitArgs.confirm, "confirm", false, "do not prompt for confirmation")
return fs
})(),
} }
func runNetworkLockInit(ctx context.Context, args []string) error { func runNetworkLockInit(ctx context.Context, args []string) error {
@ -62,12 +96,55 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
return err return err
} }
status, err := localClient.NetworkLockInit(ctx, keys, disablementValues) fmt.Println("You are initializing tailnet lock with trust in the following keys:")
if err != nil { for _, k := range keys {
fmt.Printf(" - %x (%s key)\n", k.Public, k.Kind.String())
}
fmt.Println()
if !nlInitArgs.confirm {
fmt.Printf("%d disablement secrets will be generated.\n", nlInitArgs.numDisablements)
if nlInitArgs.disablementForSupport {
fmt.Println("A disablement secret for support will be generated and transmitted to Tailscale.")
}
genSupportFlag := ""
if nlInitArgs.disablementForSupport {
genSupportFlag = "--gen-disablement-for-support "
}
fmt.Println("\nIf this is correct, please re-run this command with the --confirm flag:")
fmt.Printf("\t%s lock init --confirm --gen-disablements %d %s%s", os.Args[0], nlInitArgs.numDisablements, genSupportFlag, strings.Join(args, " "))
fmt.Println()
return nil
}
fmt.Printf("%d disablement secrets have been generated and are printed below. Take note of them now, they WILL NOT be shown again.\n", nlInitArgs.numDisablements)
for i := 0; i < nlInitArgs.numDisablements; i++ {
var secret [32]byte
if _, err := rand.Read(secret[:]); err != nil {
return err
}
fmt.Printf("\tdisablement-secret:%X\n", secret[:])
disablementValues = append(disablementValues, tka.DisablementKDF(secret[:]))
}
var supportDisablement []byte
if nlInitArgs.disablementForSupport {
supportDisablement = make([]byte, 32)
if _, err := rand.Read(supportDisablement); err != nil {
return err
}
disablementValues = append(disablementValues, tka.DisablementKDF(supportDisablement))
fmt.Println("A disablement secret for support has been generated and will be transmitted to Tailscale upon initialization.")
}
// The state returned by NetworkLockInit likely doesn't contain the initialized state,
// because that has to tick through from netmaps.
if _, err := localClient.NetworkLockInit(ctx, keys, disablementValues, supportDisablement); err != nil {
return err return err
} }
fmt.Printf("Status: %+v\n\n", status) fmt.Println("Initialization complete.")
return nil return nil
} }
@ -84,18 +161,18 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
return fixTailscaledConnectError(err) return fixTailscaledConnectError(err)
} }
if st.Enabled { if st.Enabled {
fmt.Println("Network-lock is ENABLED.") fmt.Println("Tailnet-lock is ENABLED.")
} else { } else {
fmt.Println("Network-lock is NOT enabled.") fmt.Println("Tailnet-lock is NOT enabled.")
} }
fmt.Println() fmt.Println()
if st.Enabled && st.NodeKey != nil { if st.Enabled && st.NodeKey != nil {
if st.NodeKeySigned { if st.NodeKeySigned {
fmt.Println("This node is trusted by network-lock.") fmt.Println("This node is accessible under tailnet-lock.")
} else { } else {
fmt.Println("This node IS NOT trusted by network-lock, and action is required to establish connectivity.") fmt.Println("This node is LOCKED OUT by tailnet-lock, and action is required to establish connectivity.")
fmt.Printf("Run the following command on a node with a network-lock key:\n\ttailscale lock sign %v\n", st.NodeKey) fmt.Printf("Run the following command on a node with a trusted key:\n\ttailscale lock sign %v\n", st.NodeKey)
} }
fmt.Println() fmt.Println()
} }
@ -105,12 +182,12 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("This node's public-key: %s\n", p) fmt.Printf("This node's tailnet-lock key: %s\n", p)
fmt.Println() fmt.Println()
} }
if st.Enabled && len(st.TrustedKeys) > 0 { if st.Enabled && len(st.TrustedKeys) > 0 {
fmt.Println("Keys trusted to make changes to network-lock:") fmt.Println("Keys trusted to make changes to tailnet-lock:")
for _, k := range st.TrustedKeys { for _, k := range st.TrustedKeys {
key, err := k.Key.MarshalText() key, err := k.Key.MarshalText()
if err != nil { if err != nil {
@ -204,7 +281,7 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
return fixTailscaledConnectError(err) return fixTailscaledConnectError(err)
} }
if !st.Enabled { if !st.Enabled {
return errors.New("network-lock is not enabled") return errors.New("tailnet-lock is not enabled")
} }
addKeys, _, err := parseNLArgs(addArgs, true, false) addKeys, _, err := parseNLArgs(addArgs, true, false)
@ -256,7 +333,7 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
var nlDisableCmd = &ffcli.Command{ var nlDisableCmd = &ffcli.Command{
Name: "disable", Name: "disable",
ShortUsage: "disable <disablement-secret>", ShortUsage: "disable <disablement-secret>",
ShortHelp: "Consumes a disablement secret to shut down network-lock across the tailnet", ShortHelp: "Consumes a disablement secret to shut down tailnet-lock across the tailnet",
Exec: runNetworkLockDisable, Exec: runNetworkLockDisable,
} }
@ -274,7 +351,7 @@ func runNetworkLockDisable(ctx context.Context, args []string) error {
var nlDisablementKDFCmd = &ffcli.Command{ var nlDisablementKDFCmd = &ffcli.Command{
Name: "disablement-kdf", Name: "disablement-kdf",
ShortUsage: "disablement-kdf <hex-encoded-disablement-secret>", ShortUsage: "disablement-kdf <hex-encoded-disablement-secret>",
ShortHelp: "Computes a disablement value from a disablement secret", ShortHelp: "Computes a disablement value from a disablement secret (advanced users only)",
Exec: runNetworkLockDisablementKDF, Exec: runNetworkLockDisablementKDF,
} }
@ -297,7 +374,7 @@ func runNetworkLockDisablementKDF(ctx context.Context, args []string) error {
var nlLogCmd = &ffcli.Command{ var nlLogCmd = &ffcli.Command{
Name: "log", Name: "log",
ShortUsage: "log [--limit N]", ShortUsage: "log [--limit N]",
ShortHelp: "List changes applied to network-lock", ShortHelp: "List changes applied to tailnet-lock",
Exec: runNetworkLockLog, Exec: runNetworkLockLog,
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("lock log") fs := newFlagSet("lock log")

View File

@ -403,7 +403,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
// needing signatures is returned as a response. // needing signatures is returned as a response.
// The Finish RPC submits signatures for all these nodes, at which point // The Finish RPC submits signatures for all these nodes, at which point
// Control has everything it needs to atomically enable network lock. // Control has everything it needs to atomically enable network lock.
func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byte) error { func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) error {
if err := b.CanSupportNetworkLock(); err != nil { if err := b.CanSupportNetworkLock(); err != nil {
return err return err
} }
@ -471,7 +471,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt
} }
// Finalize enablement by transmitting signature for all nodes to Control. // Finalize enablement by transmitting signature for all nodes to Control.
_, err = b.tkaInitFinish(ourNodeKey, sigs) _, err = b.tkaInitFinish(ourNodeKey, sigs, supportDisablement)
return err return err
} }
@ -748,12 +748,13 @@ func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*ta
return a, nil return a, nil
} }
func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) { func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.NodeID]tkatype.MarshaledSignature, supportDisablement []byte) (*tailcfg.TKAInitFinishResponse, error) {
var req bytes.Buffer var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{ if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{
Version: tailcfg.CurrentCapabilityVersion, Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey, NodeKey: ourNodeKey,
Signatures: nks, Signatures: nks,
SupportDisablement: supportDisablement,
}); err != nil { }); err != nil {
return nil, fmt.Errorf("encoding request: %v", err) return nil, fmt.Errorf("encoding request: %v", err)
} }

View File

@ -1163,6 +1163,7 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
type initRequest struct { type initRequest struct {
Keys []tka.Key Keys []tka.Key
DisablementValues [][]byte DisablementValues [][]byte
SupportDisablement []byte
} }
var req initRequest var req initRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@ -1170,7 +1171,7 @@ type initRequest struct {
return return
} }
if err := h.b.NetworkLockInit(req.Keys, req.DisablementValues); err != nil { if err := h.b.NetworkLockInit(req.Keys, req.DisablementValues, req.SupportDisablement); err != nil {
http.Error(w, "initialization failed: "+err.Error(), http.StatusInternalServerError) http.Error(w, "initialization failed: "+err.Error(), http.StatusInternalServerError)
return return
} }

View File

@ -69,6 +69,11 @@ type TKAInitFinishRequest struct {
// Signatures are serialized tka.NodeKeySignatures for all nodes // Signatures are serialized tka.NodeKeySignatures for all nodes
// in the tailnet. // in the tailnet.
Signatures map[NodeID]tkatype.MarshaledSignature Signatures map[NodeID]tkatype.MarshaledSignature
// SupportDisablement is a disablement secret for Tailscale support.
// This is only generated if --gen-disablement-for-support is specified
// in an invocation to 'tailscale lock init'.
SupportDisablement []byte `json:",omitempty"`
} }
// TKAInitFinishResponse is the JSON response from a /tka/init/finish RPC. // TKAInitFinishResponse is the JSON response from a /tka/init/finish RPC.