diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index 2e2f6800f..9d006303d 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -787,14 +787,15 @@ func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.Network // NetworkLockInit initializes the tailnet key authority. // // 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 type initRequest struct { - Keys []tka.Key - DisablementValues [][]byte + Keys []tka.Key + 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 } diff --git a/cmd/tailscale/cli/network-lock.go b/cmd/tailscale/cli/network-lock.go index a7c701761..26a5863fe 100644 --- a/cmd/tailscale/cli/network-lock.go +++ b/cmd/tailscale/cli/network-lock.go @@ -6,6 +6,7 @@ import ( "context" + "crypto/rand" "encoding/hex" "encoding/json" "errors" @@ -40,11 +41,44 @@ Exec: runNetworkLockStatus, } +var nlInitArgs struct { + numDisablements int + disablementForSupport bool + confirm bool +} + var nlInitCmd = &ffcli.Command{ Name: "init", - ShortUsage: "init ...", - ShortHelp: "Initialize the tailnet key authority", - Exec: runNetworkLockInit, + ShortUsage: "init [--gen-disablement-for-support] --gen-disablements N ...", + 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, + 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 { @@ -62,12 +96,55 @@ func runNetworkLockInit(ctx context.Context, args []string) error { return err } - status, err := localClient.NetworkLockInit(ctx, keys, disablementValues) - if err != nil { + fmt.Println("You are initializing tailnet lock with trust in the following keys:") + 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 } - fmt.Printf("Status: %+v\n\n", status) + fmt.Println("Initialization complete.") return nil } @@ -84,18 +161,18 @@ func runNetworkLockStatus(ctx context.Context, args []string) error { return fixTailscaledConnectError(err) } if st.Enabled { - fmt.Println("Network-lock is ENABLED.") + fmt.Println("Tailnet-lock is ENABLED.") } else { - fmt.Println("Network-lock is NOT enabled.") + fmt.Println("Tailnet-lock is NOT enabled.") } fmt.Println() if st.Enabled && st.NodeKey != nil { if st.NodeKeySigned { - fmt.Println("This node is trusted by network-lock.") + fmt.Println("This node is accessible under tailnet-lock.") } else { - fmt.Println("This node IS NOT trusted by network-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.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 trusted key:\n\ttailscale lock sign %v\n", st.NodeKey) } fmt.Println() } @@ -105,12 +182,12 @@ func runNetworkLockStatus(ctx context.Context, args []string) error { if err != nil { 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() } 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 { key, err := k.Key.MarshalText() if err != nil { @@ -204,7 +281,7 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err return fixTailscaledConnectError(err) } 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) @@ -256,7 +333,7 @@ func runNetworkLockSign(ctx context.Context, args []string) error { var nlDisableCmd = &ffcli.Command{ Name: "disable", ShortUsage: "disable ", - 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, } @@ -274,7 +351,7 @@ func runNetworkLockDisable(ctx context.Context, args []string) error { var nlDisablementKDFCmd = &ffcli.Command{ Name: "disablement-kdf", ShortUsage: "disablement-kdf ", - ShortHelp: "Computes a disablement value from a disablement secret", + ShortHelp: "Computes a disablement value from a disablement secret (advanced users only)", Exec: runNetworkLockDisablementKDF, } @@ -297,7 +374,7 @@ func runNetworkLockDisablementKDF(ctx context.Context, args []string) error { var nlLogCmd = &ffcli.Command{ Name: "log", ShortUsage: "log [--limit N]", - ShortHelp: "List changes applied to network-lock", + ShortHelp: "List changes applied to tailnet-lock", Exec: runNetworkLockLog, FlagSet: (func() *flag.FlagSet { fs := newFlagSet("lock log") diff --git a/ipn/ipnlocal/network-lock.go b/ipn/ipnlocal/network-lock.go index 780043bbb..8346ed7cf 100644 --- a/ipn/ipnlocal/network-lock.go +++ b/ipn/ipnlocal/network-lock.go @@ -403,7 +403,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus { // needing signatures is returned as a response. // The Finish RPC submits signatures for all these nodes, at which point // 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 { 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. - _, err = b.tkaInitFinish(ourNodeKey, sigs) + _, err = b.tkaInitFinish(ourNodeKey, sigs, supportDisablement) return err } @@ -748,12 +748,13 @@ func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*ta 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 if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{ - Version: tailcfg.CurrentCapabilityVersion, - NodeKey: ourNodeKey, - Signatures: nks, + Version: tailcfg.CurrentCapabilityVersion, + NodeKey: ourNodeKey, + Signatures: nks, + SupportDisablement: supportDisablement, }); err != nil { return nil, fmt.Errorf("encoding request: %v", err) } diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 93170ea3f..ec87b25cc 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -1161,8 +1161,9 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) { } type initRequest struct { - Keys []tka.Key - DisablementValues [][]byte + Keys []tka.Key + DisablementValues [][]byte + SupportDisablement []byte } var req initRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -1170,7 +1171,7 @@ type initRequest struct { 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) return } diff --git a/tailcfg/tka.go b/tailcfg/tka.go index c1cec6a0f..6ce0b7cf5 100644 --- a/tailcfg/tka.go +++ b/tailcfg/tka.go @@ -69,6 +69,11 @@ type TKAInitFinishRequest struct { // Signatures are serialized tka.NodeKeySignatures for all nodes // in the tailnet. 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.