Move create-bootstrap logic into daemon, rename to hosts create

This commit is contained in:
Brian Picciano 2024-07-13 16:31:52 +02:00
parent cb8fef38c4
commit b5059be7fa
7 changed files with 158 additions and 99 deletions

View File

@ -29,7 +29,7 @@ To create a `bootstrap.json` file for the new host, the admin should perform the
following command from their own host: following command from their own host:
``` ```
isle admin create-bootstrap \ isle hosts create \
--hostname <name> \ --hostname <name> \
--ip <ip> \ --ip <ip> \
--admin-path <path to admin.json> \ --admin-path <path to admin.json> \
@ -48,12 +48,12 @@ The user can now proceed with calling `isle network join`, as described in the
### Encrypted `admin.json` ### Encrypted `admin.json`
If `admin.json` is kept in an encrypted format on disk (it should be!) then the If `admin.json` is kept in an encrypted format on disk (it should be!) then the
decrypted form can be piped into `create-bootstrap` over stdin. For example, if decrypted form can be piped into `isle hosts create` over stdin. For example, if
GPG is being used to secure `admin.json` then the following could be used to GPG is being used to secure `admin.json` then the following could be used to
generate a `bootstrap.json`: generate a `bootstrap.json`:
``` ```
gpg -d <path to admin.json.gpg> | isle admin create-bootstrap \ gpg -d <path to admin.json.gpg> | isle hosts create \
--hostname <name> \ --hostname <name> \
--ip <ip> \ --ip <ip> \
--admin-path - \ --admin-path - \

View File

@ -1,25 +1,11 @@
package main package main
import ( import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt" "fmt"
"isle/admin" "isle/admin"
"isle/bootstrap"
"isle/nebula"
"net/netip"
"os" "os"
) )
func randStr(l int) string {
b := make([]byte, l)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)
}
func readAdmin(path string) (admin.Admin, error) { func readAdmin(path string) (admin.Admin, error) {
if path == "-" { if path == "-" {
@ -40,83 +26,3 @@ func readAdmin(path string) (admin.Admin, error) {
return admin.FromReader(f) return admin.FromReader(f)
} }
var subCmdAdminCreateBootstrap = subCmd{
name: "create-bootstrap",
descr: "Creates a new bootstrap.json file for a particular host and writes it to stdout",
do: func(subCmdCtx subCmdCtx) error {
var (
flags = subCmdCtx.flagSet(false)
hostName nebula.HostName
ip netip.Addr
)
hostNameF := flags.VarPF(
textUnmarshalerFlag{&hostName},
"hostname", "h",
"Name of the host to generate bootstrap.json for",
)
ipF := flags.VarPF(
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
)
adminPath := flags.StringP(
"admin-path", "a", "",
`Path to admin.json file. If the given path is "-" then stdin is used.`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed ||
!ipF.Changed ||
*adminPath == "" {
return errors.New("--hostname, --ip, and --admin-path are required")
}
adm, err := readAdmin(*adminPath)
if err != nil {
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
}
garageBootstrap := bootstrap.Garage{
RPCSecret: adm.Garage.RPCSecret,
AdminToken: randStr(32),
GlobalBucketS3APICredentials: adm.Garage.GlobalBucketS3APICredentials,
}
newHostBootstrap, err := bootstrap.New(
adm.Nebula.CACredentials,
adm.CreationParams,
garageBootstrap,
hostName,
ip,
)
if err != nil {
return fmt.Errorf("initializing bootstrap data: %w", err)
}
hostsRes, err := subCmdCtx.getHosts()
if err != nil {
return fmt.Errorf("getting hosts: %w", err)
}
for _, host := range hostsRes.Hosts {
newHostBootstrap.Hosts[host.Name] = host
}
return newHostBootstrap.WriteTo(os.Stdout)
},
}
var subCmdAdmin = subCmd{
name: "admin",
descr: "Sub-commands which only admins can run",
do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd(
subCmdAdminCreateBootstrap,
)
},
}

View File

@ -7,10 +7,70 @@ import (
"isle/daemon" "isle/daemon"
"isle/jsonutil" "isle/jsonutil"
"isle/nebula" "isle/nebula"
"net/netip"
"os" "os"
"sort" "sort"
) )
var subCmdHostsCreate = subCmd{
name: "create",
descr: "Creates a new host in the network, writing its new bootstrap.json to stdout",
do: func(subCmdCtx subCmdCtx) error {
var (
flags = subCmdCtx.flagSet(false)
hostName nebula.HostName
ip netip.Addr
)
hostNameF := flags.VarPF(
textUnmarshalerFlag{&hostName},
"hostname", "h",
"Name of the host to generate bootstrap.json for",
)
ipF := flags.VarPF(
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
)
adminPath := flags.StringP(
"admin-path", "a", "",
`Path to admin.json file. If the given path is "-" then stdin is used.`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed ||
!ipF.Changed ||
*adminPath == "" {
return errors.New("--hostname, --ip, and --admin-path are required")
}
adm, err := readAdmin(*adminPath)
if err != nil {
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
}
var res daemon.CreateHostResult
err = subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx,
&res,
"CreateHost",
daemon.CreateHostRequest{
CASigningPrivateKey: adm.Nebula.CACredentials.SigningPrivateKey,
HostName: hostName,
IP: ip,
},
)
if err != nil {
return fmt.Errorf("calling CreateHost: %w", err)
}
return res.HostBootstrap.WriteTo(os.Stdout)
},
}
var subCmdHostsList = subCmd{ var subCmdHostsList = subCmd{
name: "list", name: "list",
descr: "Lists all hosts in the network, and their IPs", descr: "Lists all hosts in the network, and their IPs",
@ -88,6 +148,7 @@ var subCmdHosts = subCmd{
descr: "Sub-commands having to do with configuration of hosts in the network", descr: "Sub-commands having to do with configuration of hosts in the network",
do: func(subCmdCtx subCmdCtx) error { do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd( return subCmdCtx.doSubCmd(
subCmdHostsCreate,
subCmdHostsRemove, subCmdHostsRemove,
subCmdHostsList, subCmdHostsList,
) )

View File

@ -61,7 +61,6 @@ func main() {
ctx: ctx, ctx: ctx,
logger: logger, logger: logger,
}.doSubCmd( }.doSubCmd(
subCmdAdmin,
subCmdDaemon, subCmdDaemon,
subCmdGarage, subCmdGarage,
subCmdHosts, subCmdHosts,

View File

@ -13,6 +13,7 @@ import (
"isle/bootstrap" "isle/bootstrap"
"isle/garage" "isle/garage"
"isle/nebula" "isle/nebula"
"net/netip"
"os" "os"
"sync" "sync"
"time" "time"
@ -55,6 +56,17 @@ type Daemon interface {
// RemoveHost removes the host of the given name from the network. // RemoveHost removes the host of the given name from the network.
RemoveHost(context.Context, nebula.HostName) error RemoveHost(context.Context, nebula.HostName) error
// CreateHost creates a bootstrap for a new host with the given name and IP
// address.
CreateHost(
ctx context.Context,
caSigningPrivateKey nebula.SigningPrivateKey, // TODO load from secrets storage
hostName nebula.HostName,
ip netip.Addr, // TODO automatically choose IP address
) (
bootstrap.Bootstrap, error,
)
// CreateNebulaCertificate creates and signs a new nebula certficate for an // CreateNebulaCertificate creates and signs a new nebula certficate for an
// existing host, given the public key for that host. This is currently // existing host, given the public key for that host. This is currently
// mostly useful for creating certs for mobile devices. // mostly useful for creating certs for mobile devices.
@ -617,6 +629,54 @@ func makeCACreds(
} }
} }
func (d *daemon) CreateHost(
ctx context.Context,
caSigningPrivateKey nebula.SigningPrivateKey,
hostName nebula.HostName,
ip netip.Addr,
) (
bootstrap.Bootstrap, error,
) {
return withCurrBootstrap(d, func(
currBootstrap bootstrap.Bootstrap,
) (
bootstrap.Bootstrap, error,
) {
var (
garageRPCSecret = currBootstrap.Garage.RPCSecret
garageGlobalBucketS3APICreds = currBootstrap.Garage.GlobalBucketS3APICredentials
)
// TODO check if garageRPCSecret is actually set
garageBootstrap := bootstrap.Garage{
RPCSecret: garageRPCSecret,
AdminToken: randStr(32),
GlobalBucketS3APICredentials: garageGlobalBucketS3APICreds,
}
newHostBootstrap, err := bootstrap.New(
makeCACreds(currBootstrap, caSigningPrivateKey),
currBootstrap.AdminCreationParams,
garageBootstrap,
hostName,
ip,
)
if err != nil {
return bootstrap.Bootstrap{}, fmt.Errorf(
"initializing bootstrap data: %w", err,
)
}
newHostBootstrap.Hosts = currBootstrap.Hosts
// TODO persist new bootstrap to garage. Requires making the daemon
// config change watching logic smarter, so only dnsmasq gets restarted.
return newHostBootstrap, nil
})
}
func (d *daemon) CreateNebulaCertificate( func (d *daemon) CreateNebulaCertificate(
ctx context.Context, ctx context.Context,
caSigningPrivateKey nebula.SigningPrivateKey, caSigningPrivateKey nebula.SigningPrivateKey,

View File

@ -7,6 +7,7 @@ import (
"isle/admin" "isle/admin"
"isle/bootstrap" "isle/bootstrap"
"isle/nebula" "isle/nebula"
"net/netip"
"slices" "slices"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
@ -131,6 +132,38 @@ func (r *RPC) RemoveHost(ctx context.Context, req RemoveHostRequest) (struct{},
return struct{}{}, r.daemon.RemoveHost(ctx, req.HostName) return struct{}{}, r.daemon.RemoveHost(ctx, req.HostName)
} }
// CreateHostRequest contains the arguments to the
// CreateHost RPC method.
//
// All fields are required.
type CreateHostRequest struct {
CASigningPrivateKey nebula.SigningPrivateKey // TODO load from secrets storage
HostName nebula.HostName
IP netip.Addr
}
// CreateHostResult wraps the results from the CreateHost RPC method.
type CreateHostResult struct {
HostBootstrap bootstrap.Bootstrap
}
// CreateHost passes the call through to the Daemon method of the
// same name.
func (r *RPC) CreateHost(
ctx context.Context, req CreateHostRequest,
) (
CreateHostResult, error,
) {
hostBootstrap, err := r.daemon.CreateHost(
ctx, req.CASigningPrivateKey, req.HostName, req.IP,
)
if err != nil {
return CreateHostResult{}, err
}
return CreateHostResult{HostBootstrap: hostBootstrap}, nil
}
// CreateNebulaCertificateRequest contains the arguments to the // CreateNebulaCertificateRequest contains the arguments to the
// CreateNebulaCertificate RPC method. // CreateNebulaCertificate RPC method.
// //

View File

@ -67,7 +67,7 @@ EOF
> admin.json > admin.json
echo "Creating secondus bootstrap" echo "Creating secondus bootstrap"
isle admin create-bootstrap \ isle hosts create \
--admin-path admin.json \ --admin-path admin.json \
--hostname secondus \ --hostname secondus \
--ip "$secondus_ip" \ --ip "$secondus_ip" \