mirror of https://github.com/tailscale/tailscale/
client,cmd/tailscale,ipn,tka,types: implement tka initialization flow
This PR implements the client-side of initializing network-lock with the Coordination server. Signed-off-by: Tom DNetto <tom@tailscale.com>pull/5366/head
parent
18edd79421
commit
facafd8819
@ -0,0 +1,101 @@
|
|||||||
|
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"tailscale.com/tka"
|
||||||
|
"tailscale.com/types/key"
|
||||||
|
)
|
||||||
|
|
||||||
|
var netlockCmd = &ffcli.Command{
|
||||||
|
Name: "lock",
|
||||||
|
ShortUsage: "lock <sub-command> <arguments>",
|
||||||
|
ShortHelp: "Manipulate the tailnet key authority",
|
||||||
|
Subcommands: []*ffcli.Command{nlInitCmd, nlStatusCmd},
|
||||||
|
Exec: runNetworkLockStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
var nlInitCmd = &ffcli.Command{
|
||||||
|
Name: "init",
|
||||||
|
ShortUsage: "init <public-key>...",
|
||||||
|
ShortHelp: "Initialize the tailnet key authority",
|
||||||
|
Exec: runNetworkLockInit,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNetworkLockInit(ctx context.Context, args []string) error {
|
||||||
|
st, err := localClient.NetworkLockStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fixTailscaledConnectError(err)
|
||||||
|
}
|
||||||
|
if st.Enabled {
|
||||||
|
return errors.New("network-lock is already enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the set of initially-trusted keys.
|
||||||
|
// Keys are specified using their key.NLPublic.MarshalText representation,
|
||||||
|
// with an optional '?<votes>' suffix.
|
||||||
|
var keys []tka.Key
|
||||||
|
for i, a := range args {
|
||||||
|
var key key.NLPublic
|
||||||
|
spl := strings.SplitN(a, "?", 2)
|
||||||
|
if err := key.UnmarshalText([]byte(spl[0])); err != nil {
|
||||||
|
return fmt.Errorf("parsing key %d: %v", i+1, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
k := tka.Key{
|
||||||
|
Kind: tka.Key25519,
|
||||||
|
Public: key.Verifier(),
|
||||||
|
Votes: 1,
|
||||||
|
}
|
||||||
|
if len(spl) > 1 {
|
||||||
|
votes, err := strconv.Atoi(spl[1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing key %d votes: %v", i+1, err)
|
||||||
|
}
|
||||||
|
k.Votes = uint(votes)
|
||||||
|
}
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := localClient.NetworkLockInit(ctx, keys)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Status: %+v\n\n", status)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var nlStatusCmd = &ffcli.Command{
|
||||||
|
Name: "status",
|
||||||
|
ShortUsage: "status",
|
||||||
|
ShortHelp: "Outputs the state of network lock",
|
||||||
|
Exec: runNetworkLockStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||||
|
st, err := localClient.NetworkLockStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fixTailscaledConnectError(err)
|
||||||
|
}
|
||||||
|
if st.Enabled {
|
||||||
|
fmt.Println("Network-lock is ENABLED.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Network-lock is NOT enabled.")
|
||||||
|
}
|
||||||
|
p, err := st.PublicKey.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("our public-key: %s\n", p)
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,226 @@
|
|||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package ipnlocal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/envknob"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/logtail/backoff"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/tka"
|
||||||
|
"tailscale.com/types/key"
|
||||||
|
"tailscale.com/types/tkatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
var networkLockAvailable = envknob.Bool("TS_EXPERIMENTAL_NETWORK_LOCK")
|
||||||
|
|
||||||
|
// CanSupportNetworkLock returns true if tailscaled is able to operate
|
||||||
|
// a local tailnet key authority (and hence enforce network lock).
|
||||||
|
func (b *LocalBackend) CanSupportNetworkLock() bool {
|
||||||
|
if b.tka != nil {
|
||||||
|
// The TKA is being used, so yeah its supported.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.TailscaleVarRoot() != "" {
|
||||||
|
// Theres a var root (aka --statedir), so if network lock gets
|
||||||
|
// initialized we have somewhere to store our AUMs. Thats all
|
||||||
|
// we need.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockStatus returns a structure describing the state of the
|
||||||
|
// tailnet key authority, if any.
|
||||||
|
func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
|
||||||
|
if b.tka == nil {
|
||||||
|
return &ipnstate.NetworkLockStatus{
|
||||||
|
Enabled: false,
|
||||||
|
PublicKey: b.nlPrivKey.Public(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var head [32]byte
|
||||||
|
h := b.tka.Head()
|
||||||
|
copy(head[:], h[:])
|
||||||
|
|
||||||
|
return &ipnstate.NetworkLockStatus{
|
||||||
|
Enabled: true,
|
||||||
|
Head: &head,
|
||||||
|
PublicKey: b.nlPrivKey.Public(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockInit enables network-lock for the tailnet, with the tailnets'
|
||||||
|
// key authority initialized to trust the provided keys.
|
||||||
|
//
|
||||||
|
// Initialization involves two RPCs with control, termed 'begin' and 'finish'.
|
||||||
|
// The Begin RPC transmits the genesis Authority Update Message, which
|
||||||
|
// encodes the initial state of the authority, and the list of all nodes
|
||||||
|
// 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) error {
|
||||||
|
if b.tka != nil {
|
||||||
|
return errors.New("network-lock is already initialized")
|
||||||
|
}
|
||||||
|
if !networkLockAvailable {
|
||||||
|
return errors.New("this is an experimental feature in your version of tailscale - Please upgrade to the latest to use this.")
|
||||||
|
}
|
||||||
|
if !b.CanSupportNetworkLock() {
|
||||||
|
return errors.New("network-lock is not supported in this configuration. Did you supply a --statedir?")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a genesis AUM representing trust in the provided keys.
|
||||||
|
// We use an in-memory tailchonk because we don't want to commit to
|
||||||
|
// the filesystem until we've finished the initialization sequence,
|
||||||
|
// just in case something goes wrong.
|
||||||
|
_, genesisAUM, err := tka.Create(&tka.Mem{}, tka.State{
|
||||||
|
Keys: keys,
|
||||||
|
// TODO(tom): Actually plumb a real disablement value.
|
||||||
|
DisablementSecrets: [][]byte{bytes.Repeat([]byte{1}, 32)},
|
||||||
|
}, b.nlPrivKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("tka.Create: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logf("Generated genesis AUM to initialize network lock, trusting the following keys:")
|
||||||
|
for i, k := range genesisAUM.State.Keys {
|
||||||
|
b.logf(" - key[%d] = nlpub:%x with %d votes", i, k.Public, k.Votes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1/2 of initialization: Transmit the genesis AUM to Control.
|
||||||
|
initResp, err := b.tkaInitBegin(genesisAUM)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("tka init-begin RPC: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our genesis AUM was accepted but before Control turns on enforcement of
|
||||||
|
// node-key signatures, we need to sign keys for all the existing nodes.
|
||||||
|
// If we don't get these signatures ahead of time, everyone will loose
|
||||||
|
// connectivity because control won't have any signatures to send which
|
||||||
|
// satisfy network-lock checks.
|
||||||
|
var sigs []tkatype.MarshaledSignature
|
||||||
|
for _, nkp := range initResp.NeedSignatures {
|
||||||
|
nks, err := signNodeKey(nkp, b.nlPrivKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("generating signature: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sigs = append(sigs, nks.Serialize())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize enablement by transmitting signature for all nodes to Control.
|
||||||
|
_, err = b.tkaInitFinish(sigs)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func signNodeKey(nk key.NodePublic, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
|
||||||
|
p, err := nk.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sig := tka.NodeKeySignature{
|
||||||
|
SigKind: tka.SigDirect,
|
||||||
|
KeyID: signer.KeyID(),
|
||||||
|
Pubkey: p,
|
||||||
|
}
|
||||||
|
sig.Signature, err = signer.SignNKS(sig.SigHash())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("signature failed: %w", err)
|
||||||
|
}
|
||||||
|
return &sig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) tkaInitBegin(aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
|
||||||
|
var req bytes.Buffer
|
||||||
|
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitBeginRequest{
|
||||||
|
GenesisAUM: aum.Serialize(),
|
||||||
|
}); err != nil {
|
||||||
|
return nil, fmt.Errorf("encoding request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
bo := backoff.NewBackoff("tka-init-begin", b.logf, 5*time.Second)
|
||||||
|
for {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("ctx: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/begin", &req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("req: %w", err)
|
||||||
|
}
|
||||||
|
res, err := b.DoNoiseRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
bo.BackOff(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
res.Body.Close()
|
||||||
|
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
a := new(tailcfg.TKAInitBeginResponse)
|
||||||
|
err = json.NewDecoder(res.Body).Decode(a)
|
||||||
|
res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decoding JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) tkaInitFinish(nks []tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
|
||||||
|
var req bytes.Buffer
|
||||||
|
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{
|
||||||
|
Signatures: nks,
|
||||||
|
}); err != nil {
|
||||||
|
return nil, fmt.Errorf("encoding request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
bo := backoff.NewBackoff("tka-init-finish", b.logf, 5*time.Second)
|
||||||
|
for {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("ctx: %w", err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/finish", &req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("req: %w", err)
|
||||||
|
}
|
||||||
|
res, err := b.DoNoiseRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
bo.BackOff(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
res.Body.Close()
|
||||||
|
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
a := new(tailcfg.TKAInitFinishResponse)
|
||||||
|
err = json.NewDecoder(res.Body).Decode(a)
|
||||||
|
res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decoding JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue