Compare commits

..

3 Commits

35 changed files with 831 additions and 311 deletions

View File

@ -173,6 +173,7 @@ in rec {
pkgs.yq-go pkgs.yq-go
pkgs.jq pkgs.jq
pkgs.dig pkgs.dig
pkgs.nebula
]} ]}
export SHELL=${pkgs.bash}/bin/bash export SHELL=${pkgs.bash}/bin/bash
exec ${pkgs.bash}/bin/bash ${./tests}/entrypoint.sh "$@" exec ${pkgs.bash}/bin/bash ${./tests}/entrypoint.sh "$@"

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

@ -25,7 +25,6 @@ type Admin struct {
} }
Garage struct { Garage struct {
RPCSecret string
GlobalBucketS3APICredentials garage.S3APICredentials GlobalBucketS3APICredentials garage.S3APICredentials
} }
} }

View File

@ -30,10 +30,6 @@ func AppDirPath(appDirPath string) string {
// Garage contains parameters needed to connect to and use the garage cluster. // Garage contains parameters needed to connect to and use the garage cluster.
type Garage struct { type Garage struct {
// TODO this should be part of some new configuration section related to
// secrets which may or may not be granted to this host
RPCSecret string
AdminToken string AdminToken string
// TODO this should be part of admin.CreationParams // TODO this should be part of admin.CreationParams

View File

@ -4,24 +4,6 @@ import (
"isle/garage" "isle/garage"
) )
// GarageClientParams contains all the data needed to instantiate garage
// clients.
type GarageClientParams struct {
Peer garage.RemotePeer
GlobalBucketS3APICredentials garage.S3APICredentials
RPCSecret string
}
// GlobalBucketS3APIClient returns an S3 client pre-configured with access to
// the global bucket.
func (p GarageClientParams) GlobalBucketS3APIClient() garage.S3APIClient {
var (
addr = p.Peer.S3APIAddr()
creds = p.GlobalBucketS3APICredentials
)
return garage.NewS3APIClient(addr, creds)
}
// GaragePeers returns a Peer for each known garage instance in the network. // GaragePeers returns a Peer for each known garage instance in the network.
func (b Bootstrap) GaragePeers() []garage.RemotePeer { func (b Bootstrap) GaragePeers() []garage.RemotePeer {
@ -69,12 +51,3 @@ func (b Bootstrap) ChooseGaragePeer() garage.RemotePeer {
panic("no garage instances configured") panic("no garage instances configured")
} }
// GarageClientParams returns a GarageClientParams.
func (b Bootstrap) GarageClientParams() GarageClientParams {
return GarageClientParams{
Peer: b.ChooseGaragePeer(),
GlobalBucketS3APICredentials: b.Garage.GlobalBucketS3APICredentials,
RPCSecret: b.Garage.RPCSecret,
}
}

View File

@ -3,7 +3,7 @@ package bootstrap
import ( import (
"fmt" "fmt"
"isle/nebula" "isle/nebula"
"net" "net/netip"
) )
// NebulaHost describes the nebula configuration of a Host which is relevant for // NebulaHost describes the nebula configuration of a Host which is relevant for
@ -77,10 +77,17 @@ type Host struct {
// //
// This assumes that the Host and its data has already been verified against the // This assumes that the Host and its data has already been verified against the
// CA signing key. // CA signing key.
func (h Host) IP() net.IP { func (h Host) IP() netip.Addr {
cert := h.PublicCredentials.Cert.Unwrap() cert := h.PublicCredentials.Cert.Unwrap()
if len(cert.Details.Ips) == 0 { if len(cert.Details.Ips) == 0 {
panic(fmt.Sprintf("host %q not configured with any ips: %+v", h.Name, h)) panic(fmt.Sprintf("host %q not configured with any ips: %+v", h.Name, h))
} }
return cert.Details.Ips[0].IP
ip := cert.Details.Ips[0].IP
addr, ok := netip.AddrFromSlice(ip)
if !ok {
panic(fmt.Sprintf("ip %q (%#v) is not valid, somehow", ip, ip))
}
return addr
} }

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,160 +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 subCmdAdminCreateNebulaCert = subCmd{
name: "create-nebula-cert",
descr: "Creates a signed nebula certificate file 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 a certificate 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.`,
)
pubKeyPath := flags.StringP(
"public-key-path", "p", "",
`Path to PEM file containing public key which will be embedded in the cert.`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed ||
!ipF.Changed ||
*adminPath == "" ||
*pubKeyPath == "" {
return errors.New("--hostname, --ip, --admin-path, and --pub-key-path are required")
}
adm, err := readAdmin(*adminPath)
if err != nil {
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
}
hostPubPEM, err := os.ReadFile(*pubKeyPath)
if err != nil {
return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err)
}
var hostPub nebula.EncryptingPublicKey
if err := hostPub.UnmarshalNebulaPEM(hostPubPEM); err != nil {
return fmt.Errorf("unmarshaling public key as PEM: %w", err)
}
nebulaHostCert, err := nebula.NewHostCert(
adm.Nebula.CACredentials, hostPub, hostName, ip,
)
if err != nil {
return fmt.Errorf("creating cert: %w", err)
}
nebulaHostCertPEM, err := nebulaHostCert.Unwrap().MarshalToPEM()
if err != nil {
return fmt.Errorf("marshaling cert to PEM: %w", err)
}
if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil {
return fmt.Errorf("writing to stdout: %w", err)
}
return nil
},
}
var subCmdAdmin = subCmd{
name: "admin",
descr: "Sub-commands which only admins can run",
do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd(
subCmdAdminCreateBootstrap,
subCmdAdminCreateNebulaCert,
)
},
}

View File

@ -1,12 +1,13 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"syscall" "syscall"
"isle/bootstrap" "isle/daemon"
) )
// minio-client keeps a configuration directory which contains various pieces of // minio-client keeps a configuration directory which contains various pieces of
@ -52,7 +53,7 @@ var subCmdGarageMC = subCmd{
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
var clientParams bootstrap.GarageClientParams var clientParams daemon.GarageClientParams
err := subCmdCtx.daemonRCPClient.Call( err := subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil, subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
) )
@ -119,7 +120,7 @@ var subCmdGarageCLI = subCmd{
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon", descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
do: func(subCmdCtx subCmdCtx) error { do: func(subCmdCtx subCmdCtx) error {
var clientParams bootstrap.GarageClientParams var clientParams daemon.GarageClientParams
err := subCmdCtx.daemonRCPClient.Call( err := subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil, subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
) )
@ -127,6 +128,10 @@ var subCmdGarageCLI = subCmd{
return fmt.Errorf("calling GetGarageClientParams: %w", err) return fmt.Errorf("calling GetGarageClientParams: %w", err)
} }
if clientParams.RPCSecret == "" {
return errors.New("this host does not have the garage RPC secret")
}
var ( var (
binPath = binPath("garage") binPath = binPath("garage")
args = append([]string{"garage"}, subCmdCtx.args...) args = append([]string{"garage"}, subCmdCtx.args...)

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

@ -1,12 +1,92 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"isle/daemon"
"isle/jsonutil" "isle/jsonutil"
"isle/nebula" "isle/nebula"
"os" "os"
) )
var subCmdNebulaCreateCert = subCmd{
name: "create-cert",
descr: "Creates a signed nebula certificate file for an existing host and writes it to stdout",
do: func(subCmdCtx subCmdCtx) error {
var (
flags = subCmdCtx.flagSet(false)
hostName nebula.HostName
)
hostNameF := flags.VarPF(
textUnmarshalerFlag{&hostName},
"hostname", "h",
"Name of the host to generate a certificate for",
)
adminPath := flags.StringP(
"admin-path", "a", "",
`Path to admin.json file. If the given path is "-" then stdin is used.`,
)
pubKeyPath := flags.StringP(
"public-key-path", "p", "",
`Path to PEM file containing public key which will be embedded in the cert.`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed ||
*adminPath == "" ||
*pubKeyPath == "" {
return errors.New("--hostname, --admin-path, and --pub-key-path are required")
}
adm, err := readAdmin(*adminPath)
if err != nil {
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
}
hostPubPEM, err := os.ReadFile(*pubKeyPath)
if err != nil {
return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err)
}
var hostPub nebula.EncryptingPublicKey
if err := hostPub.UnmarshalNebulaPEM(hostPubPEM); err != nil {
return fmt.Errorf("unmarshaling public key as PEM: %w", err)
}
var res daemon.CreateNebulaCertificateResult
err = subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx,
&res,
"CreateNebulaCertificate",
daemon.CreateNebulaCertificateRequest{
CASigningPrivateKey: adm.Nebula.CACredentials.SigningPrivateKey,
HostName: hostName,
HostEncryptingPublicKey: hostPub,
},
)
if err != nil {
return fmt.Errorf("calling CreateNebulaCertificate: %w", err)
}
nebulaHostCertPEM, err := res.HostNebulaCertifcate.Unwrap().MarshalToPEM()
if err != nil {
return fmt.Errorf("marshaling cert to PEM: %w", err)
}
if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil {
return fmt.Errorf("writing to stdout: %w", err)
}
return nil
},
}
var subCmdNebulaShow = subCmd{ var subCmdNebulaShow = subCmd{
name: "show", name: "show",
descr: "Writes nebula network information to stdout in JSON format", descr: "Writes nebula network information to stdout in JSON format",
@ -30,20 +110,17 @@ var subCmdNebulaShow = subCmd{
return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err) return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err)
} }
caCert := caPublicCreds.Cert.Unwrap() caCert := caPublicCreds.Cert
caCertPEM, err := caCert.MarshalToPEM() caCertDetails := caCert.Unwrap().Details
if err != nil {
return fmt.Errorf("marshaling CA cert to PEM: %w", err)
}
if len(caCert.Details.Subnets) != 1 { if len(caCertDetails.Subnets) != 1 {
return fmt.Errorf( return fmt.Errorf(
"malformed ca.crt, contains unexpected subnets %#v", "malformed ca.crt, contains unexpected subnets %#v",
caCert.Details.Subnets, caCertDetails.Subnets,
) )
} }
subnet := caCert.Details.Subnets[0] subnet := caCertDetails.Subnets[0]
type outLighthouse struct { type outLighthouse struct {
PublicAddr string PublicAddr string
@ -51,11 +128,11 @@ var subCmdNebulaShow = subCmd{
} }
out := struct { out := struct {
CACert string CACert nebula.Certificate
SubnetCIDR string SubnetCIDR string
Lighthouses []outLighthouse Lighthouses []outLighthouse
}{ }{
CACert: string(caCertPEM), CACert: caCert,
SubnetCIDR: subnet.String(), SubnetCIDR: subnet.String(),
} }
@ -83,6 +160,7 @@ var subCmdNebula = subCmd{
descr: "Sub-commands related to the nebula VPN", descr: "Sub-commands related to the nebula VPN",
do: func(subCmdCtx subCmdCtx) error { do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd( return subCmdCtx.doSubCmd(
subCmdNebulaCreateCert,
subCmdNebulaShow, subCmdNebulaShow,
) )
}, },

View File

@ -101,7 +101,7 @@ func bootstrapGarageHostForAlloc(
} }
func garageWriteChildConfig( func garageWriteChildConfig(
runtimeDirPath string, rpcSecret, runtimeDirPath string,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
alloc ConfigStorageAllocation, alloc ConfigStorageAllocation,
) ( ) (
@ -129,7 +129,7 @@ func garageWriteChildConfig(
MetaPath: alloc.MetaPath, MetaPath: alloc.MetaPath,
DataPath: alloc.DataPath, DataPath: alloc.DataPath,
RPCSecret: hostBootstrap.Garage.RPCSecret, RPCSecret: rpcSecret,
AdminToken: hostBootstrap.Garage.AdminToken, AdminToken: hostBootstrap.Garage.AdminToken,
LocalPeer: peer, LocalPeer: peer,
@ -144,20 +144,28 @@ func garageWriteChildConfig(
} }
func garagePmuxProcConfigs( func garagePmuxProcConfigs(
ctx context.Context,
logger *mlog.Logger, logger *mlog.Logger,
runtimeDirPath, binDirPath string, rpcSecret, runtimeDirPath, binDirPath string,
daemonConfig Config, daemonConfig Config,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) ( ) (
[]pmuxlib.ProcessConfig, error, []pmuxlib.ProcessConfig, error,
) { ) {
var (
pmuxProcConfigs []pmuxlib.ProcessConfig
allocs = daemonConfig.Storage.Allocations
)
var pmuxProcConfigs []pmuxlib.ProcessConfig if len(allocs) > 0 && rpcSecret == "" {
logger.WarnString(ctx, "Not starting garage instances for storage allocations, missing garage RPC secret")
return nil, nil
}
for _, alloc := range daemonConfig.Storage.Allocations { for _, alloc := range allocs {
childConfigPath, err := garageWriteChildConfig( childConfigPath, err := garageWriteChildConfig(
runtimeDirPath, hostBootstrap, alloc, rpcSecret, runtimeDirPath, hostBootstrap, alloc,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err) return nil, fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err)
@ -176,9 +184,7 @@ func garagePmuxProcConfigs(
return pmuxProcConfigs, nil return pmuxProcConfigs, nil
} }
// TODO don't expose this publicly once cluster creation is done via Daemon func garageApplyLayout(
// interface.
func GarageApplyLayout(
ctx context.Context, ctx context.Context,
logger *mlog.Logger, logger *mlog.Logger,
daemonConfig Config, daemonConfig Config,

View File

@ -22,7 +22,7 @@ func waitForNebula(
ctx context.Context, logger *mlog.Logger, hostBootstrap bootstrap.Bootstrap, ctx context.Context, logger *mlog.Logger, hostBootstrap bootstrap.Bootstrap,
) error { ) error {
var ( var (
ip = hostBootstrap.ThisHost().IP() ip = net.IP(hostBootstrap.ThisHost().IP().AsSlice())
lUDPAddr = &net.UDPAddr{IP: ip, Port: 0} lUDPAddr = &net.UDPAddr{IP: ip, Port: 0}
rUDPAddr = &net.UDPAddr{IP: ip, Port: 45535} rUDPAddr = &net.UDPAddr{IP: ip, Port: 45535}
) )

View File

@ -9,8 +9,13 @@ import (
) )
func (c *Children) newPmuxConfig( func (c *Children) newPmuxConfig(
binDirPath string, daemonConfig Config, hostBootstrap bootstrap.Bootstrap, ctx context.Context,
) (pmuxlib.Config, error) { garageRPCSecret, binDirPath string,
daemonConfig Config,
hostBootstrap bootstrap.Bootstrap,
) (
pmuxlib.Config, error,
) {
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig( nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(
c.opts.EnvVars.RuntimeDirPath, c.opts.EnvVars.RuntimeDirPath,
binDirPath, binDirPath,
@ -34,7 +39,9 @@ func (c *Children) newPmuxConfig(
} }
garagePmuxProcConfigs, err := garagePmuxProcConfigs( garagePmuxProcConfigs, err := garagePmuxProcConfigs(
ctx,
c.logger, c.logger,
garageRPCSecret,
c.opts.EnvVars.RuntimeDirPath, c.opts.EnvVars.RuntimeDirPath,
binDirPath, binDirPath,
daemonConfig, daemonConfig,

View File

@ -2,8 +2,11 @@ package daemon
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"isle/bootstrap" "isle/bootstrap"
"isle/garage"
"isle/secrets"
"code.betamike.com/micropelago/pmux/pmuxlib" "code.betamike.com/micropelago/pmux/pmuxlib"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog" "dev.mediocregopher.com/mediocre-go-lib.git/mlog"
@ -27,15 +30,22 @@ type Children struct {
func NewChildren( func NewChildren(
ctx context.Context, ctx context.Context,
logger *mlog.Logger, logger *mlog.Logger,
binDirPath string,
secretsStore secrets.Store,
daemonConfig Config, daemonConfig Config,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
binDirPath string,
opts *Opts, opts *Opts,
) ( ) (
*Children, error, *Children, error,
) { ) {
opts = opts.withDefaults() opts = opts.withDefaults()
logger.Info(ctx, "Loading secrets")
garageRPCSecret, err := garage.GetRPCSecret(ctx, secretsStore)
if err != nil && !errors.Is(err, secrets.ErrNotFound) {
return nil, fmt.Errorf("loading garage RPC secret: %w", err)
}
pmuxCtx, pmuxCancelFn := context.WithCancel(context.Background()) pmuxCtx, pmuxCancelFn := context.WithCancel(context.Background())
c := &Children{ c := &Children{
@ -45,7 +55,9 @@ func NewChildren(
pmuxStoppedCh: make(chan struct{}), pmuxStoppedCh: make(chan struct{}),
} }
pmuxConfig, err := c.newPmuxConfig(binDirPath, daemonConfig, hostBootstrap) pmuxConfig, err := c.newPmuxConfig(
ctx, garageRPCSecret, binDirPath, daemonConfig, hostBootstrap,
)
if err != nil { if err != nil {
return nil, fmt.Errorf("generating pmux config: %w", err) return nil, fmt.Errorf("generating pmux config: %w", err)
} }

View File

@ -13,7 +13,10 @@ import (
"isle/bootstrap" "isle/bootstrap"
"isle/garage" "isle/garage"
"isle/nebula" "isle/nebula"
"isle/secrets"
"net/netip"
"os" "os"
"path/filepath"
"sync" "sync"
"time" "time"
@ -52,9 +55,39 @@ type Daemon interface {
// GetBootstraps returns the currently active Bootstrap. // GetBootstraps returns the currently active Bootstrap.
GetBootstrap(context.Context) (bootstrap.Bootstrap, error) GetBootstrap(context.Context) (bootstrap.Bootstrap, error)
// GetGarageClientParams returns a GarageClientParams for the current
// network state.
GetGarageClientParams(context.Context) (GarageClientParams, error)
// 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
// existing host, given the public key for that host. This is currently
// mostly useful for creating certs for mobile devices.
//
// Errors:
// - ErrHostNotFound
CreateNebulaCertificate(
ctx context.Context,
caSigningPrivateKey nebula.SigningPrivateKey, // TODO load from secrets storage
hostName nebula.HostName,
hostPubKey nebula.EncryptingPublicKey,
) (
nebula.Certificate, error,
)
// Shutdown blocks until all resources held or created by the daemon, // Shutdown blocks until all resources held or created by the daemon,
// including child processes it has started, have been cleaned up. // including child processes it has started, have been cleaned up.
// //
@ -107,6 +140,8 @@ type daemon struct {
envBinDirPath string envBinDirPath string
opts *Opts opts *Opts
secretsStore secrets.Store
l sync.RWMutex l sync.RWMutex
state int state int
children *Children children *Children
@ -154,6 +189,17 @@ func NewDaemon(
return nil, fmt.Errorf("initializing daemon directories: %w", err) return nil, fmt.Errorf("initializing daemon directories: %w", err)
} }
var (
secretsPath = filepath.Join(d.opts.EnvVars.StateDirPath, "secrets")
err error
)
if d.secretsStore, err = secrets.NewFSStore(secretsPath); err != nil {
return nil, fmt.Errorf(
"initializing secrets store at %q: %w", secretsPath, err,
)
}
currBootstrap, err := bootstrap.FromFile(bootstrapFilePath) currBootstrap, err := bootstrap.FromFile(bootstrapFilePath)
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
// daemon has never had a network created or joined // daemon has never had a network created or joined
@ -249,7 +295,7 @@ func (d *daemon) checkBootstrap(
thisHost := hostBootstrap.ThisHost() thisHost := hostBootstrap.ThisHost()
newHosts, err := getGarageBootstrapHosts(ctx, d.logger, hostBootstrap) newHosts, err := d.getGarageBootstrapHosts(ctx, d.logger, hostBootstrap)
if err != nil { if err != nil {
return bootstrap.Bootstrap{}, false, fmt.Errorf("getting hosts from garage: %w", err) return bootstrap.Bootstrap{}, false, fmt.Errorf("getting hosts from garage: %w", err)
} }
@ -321,7 +367,7 @@ func (d *daemon) postInit(ctx context.Context) bool {
d.logger, d.logger,
"Applying garage layout", "Applying garage layout",
func(ctx context.Context) error { func(ctx context.Context) error {
return GarageApplyLayout( return garageApplyLayout(
ctx, d.logger, d.daemonConfig, d.currBootstrap, ctx, d.logger, d.daemonConfig, d.currBootstrap,
) )
}, },
@ -375,7 +421,7 @@ func (d *daemon) postInit(ctx context.Context) bool {
d.logger, d.logger,
"Updating host info in garage", "Updating host info in garage",
func(ctx context.Context) error { func(ctx context.Context) error {
return putGarageBoostrapHost(ctx, d.logger, d.currBootstrap) return d.putGarageBoostrapHost(ctx, d.logger, d.currBootstrap)
}, },
) { ) {
return false return false
@ -410,15 +456,16 @@ func (d *daemon) restartLoop(ctx context.Context, readyCh chan<- struct{}) {
children, err := NewChildren( children, err := NewChildren(
ctx, ctx,
d.logger.WithNamespace("children"), d.logger.WithNamespace("children"),
d.envBinDirPath,
d.secretsStore,
d.daemonConfig, d.daemonConfig,
d.currBootstrap, d.currBootstrap,
d.envBinDirPath,
d.opts, d.opts,
) )
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return return
} else if err != nil { } else if err != nil {
d.logger.Error(ctx, "failed to initialize daemon", err) d.logger.Error(ctx, "failed to initialize child processes", err)
if !wait(1 * time.Second) { if !wait(1 * time.Second) {
return return
} }
@ -475,7 +522,8 @@ func (d *daemon) CreateNetwork(
return admin.Admin{}, fmt.Errorf("creating nebula CA cert: %w", err) return admin.Admin{}, fmt.Errorf("creating nebula CA cert: %w", err)
} }
adm := admin.Admin{ var (
adm = admin.Admin{
CreationParams: admin.CreationParams{ CreationParams: admin.CreationParams{
ID: randStr(32), ID: randStr(32),
Name: name, Name: name,
@ -483,10 +531,17 @@ func (d *daemon) CreateNetwork(
}, },
} }
garageBootstrap := bootstrap.Garage{ garageBootstrap = bootstrap.Garage{
RPCSecret: randStr(32),
AdminToken: randStr(32), AdminToken: randStr(32),
} }
)
garageRPCSecret := randStr(32)
err = garage.SetRPCSecret(ctx, d.secretsStore, garageRPCSecret)
if err != nil {
return admin.Admin{}, fmt.Errorf("storing garage RPC secret: %w", err)
}
hostBootstrap, err := bootstrap.New( hostBootstrap, err := bootstrap.New(
nebulaCACreds, nebulaCACreds,
@ -536,7 +591,6 @@ func (d *daemon) CreateNetwork(
d.l.RUnlock() d.l.RUnlock()
adm.Nebula.CACredentials = nebulaCACreds adm.Nebula.CACredentials = nebulaCACreds
adm.Garage.RPCSecret = hostBootstrap.Garage.RPCSecret
adm.Garage.GlobalBucketS3APICredentials = garageGlobalBucketCreds adm.Garage.GlobalBucketS3APICredentials = garageGlobalBucketCreds
return adm, nil return adm, nil
@ -578,6 +632,20 @@ func (d *daemon) GetBootstrap(ctx context.Context) (bootstrap.Bootstrap, error)
}) })
} }
func (d *daemon) GetGarageClientParams(
ctx context.Context,
) (
GarageClientParams, error,
) {
return withCurrBootstrap(d, func(
currBootstrap bootstrap.Bootstrap,
) (
GarageClientParams, error,
) {
return d.getGarageClientParams(ctx, currBootstrap)
})
}
func (d *daemon) RemoveHost(ctx context.Context, hostName nebula.HostName) error { func (d *daemon) RemoveHost(ctx context.Context, hostName nebula.HostName) error {
// TODO RemoveHost should publish a certificate revocation for the host // TODO RemoveHost should publish a certificate revocation for the host
// being removed. // being removed.
@ -586,12 +654,93 @@ func (d *daemon) RemoveHost(ctx context.Context, hostName nebula.HostName) error
) ( ) (
struct{}, error, struct{}, error,
) { ) {
client := currBootstrap.GarageClientParams().GlobalBucketS3APIClient() garageClientParams, err := d.getGarageClientParams(ctx, currBootstrap)
if err != nil {
return struct{}{}, fmt.Errorf("get garage client params: %w", err)
}
client := garageClientParams.GlobalBucketS3APIClient()
return struct{}{}, removeGarageBootstrapHost(ctx, client, hostName) return struct{}{}, removeGarageBootstrapHost(ctx, client, hostName)
}) })
return err return err
} }
func makeCACreds(
currBootstrap bootstrap.Bootstrap,
caSigningPrivateKey nebula.SigningPrivateKey,
) nebula.CACredentials {
return nebula.CACredentials{
Public: currBootstrap.CAPublicCredentials,
SigningPrivateKey: caSigningPrivateKey,
}
}
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,
) {
garageGlobalBucketS3APICreds := currBootstrap.Garage.GlobalBucketS3APICredentials
garageBootstrap := bootstrap.Garage{
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(
ctx context.Context,
caSigningPrivateKey nebula.SigningPrivateKey,
hostName nebula.HostName,
hostPubKey nebula.EncryptingPublicKey,
) (
nebula.Certificate, error,
) {
return withCurrBootstrap(d, func(
currBootstrap bootstrap.Bootstrap,
) (
nebula.Certificate, error,
) {
host, ok := currBootstrap.Hosts[hostName]
if !ok {
return nebula.Certificate{}, ErrHostNotFound
}
caCreds := makeCACreds(currBootstrap, caSigningPrivateKey)
return nebula.NewHostCert(caCreds, hostPubKey, hostName, host.IP())
})
}
func (d *daemon) Shutdown() error { func (d *daemon) Shutdown() error {
d.l.Lock() d.l.Lock()
defer d.l.Unlock() defer d.l.Unlock()

View File

@ -22,34 +22,6 @@ type EnvVars struct {
func (e EnvVars) init() error { func (e EnvVars) init() error {
var errs []error var errs []error
mkDir := func(path string) error {
{
parentPath := filepath.Dir(path)
parentInfo, err := os.Stat(parentPath)
if err != nil {
return fmt.Errorf("checking parent path %q: %w", parentPath, err)
} else if !parentInfo.IsDir() {
return fmt.Errorf("%q is not a directory", parentPath)
}
}
info, err := os.Stat(path)
if errors.Is(err, fs.ErrNotExist) {
// fine
} else if err != nil {
return fmt.Errorf("checking path: %w", err)
} else if !info.IsDir() {
return fmt.Errorf("path is not a directory")
} else {
return nil
}
if err := os.Mkdir(path, 0700); err != nil {
return fmt.Errorf("creating directory: %w", err)
}
return nil
}
if err := mkDir(e.RuntimeDirPath); err != nil { if err := mkDir(e.RuntimeDirPath); err != nil {
errs = append(errs, fmt.Errorf( errs = append(errs, fmt.Errorf(
"creating runtime directory %q: %w", "creating runtime directory %q: %w",

View File

@ -24,4 +24,8 @@ var (
// //
// The Data field will be a string containing further details. // The Data field will be a string containing further details.
ErrInvalidConfig = jsonrpc2.NewError(5, "Invalid daemon config") ErrInvalidConfig = jsonrpc2.NewError(5, "Invalid daemon config")
// ErrHostNotFound is returned when performing an operation which expected a
// host to exist in the network, but that host wasn't found.
ErrHostNotFound = jsonrpc2.NewError(6, "Host not found")
) )

View File

@ -0,0 +1,47 @@
package daemon
import (
"context"
"errors"
"fmt"
"isle/bootstrap"
"isle/garage"
"isle/secrets"
)
// GarageClientParams contains all the data needed to instantiate garage
// clients.
type GarageClientParams struct {
Peer garage.RemotePeer
GlobalBucketS3APICredentials garage.S3APICredentials
// RPCSecret may be empty, if the secret is not available on the host.
RPCSecret string
}
func (d *daemon) getGarageClientParams(
ctx context.Context, currBootstrap bootstrap.Bootstrap,
) (
GarageClientParams, error,
) {
rpcSecret, err := garage.GetRPCSecret(ctx, d.secretsStore)
if err != nil && !errors.Is(err, secrets.ErrNotFound) {
return GarageClientParams{}, fmt.Errorf("getting garage rpc secret: %w", err)
}
return GarageClientParams{
Peer: currBootstrap.ChooseGaragePeer(),
GlobalBucketS3APICredentials: currBootstrap.Garage.GlobalBucketS3APICredentials,
RPCSecret: rpcSecret,
}, nil
}
// GlobalBucketS3APIClient returns an S3 client pre-configured with access to
// the global bucket.
func (p GarageClientParams) GlobalBucketS3APIClient() garage.S3APIClient {
var (
addr = p.Peer.S3APIAddr()
creds = p.GlobalBucketS3APICredentials
)
return garage.NewS3APIClient(addr, creds)
}

View File

@ -62,24 +62,28 @@ func garageInitializeGlobalBucket(
// putGarageBoostrapHost places the <hostname>.json.signed file for this host // putGarageBoostrapHost places the <hostname>.json.signed file for this host
// into garage so that other hosts are able to see relevant configuration for // into garage so that other hosts are able to see relevant configuration for
// it. // it.
func putGarageBoostrapHost( func (d *daemon) putGarageBoostrapHost(
ctx context.Context, logger *mlog.Logger, currBootstrap bootstrap.Bootstrap, ctx context.Context, logger *mlog.Logger, currBootstrap bootstrap.Bootstrap,
) error { ) error {
garageClientParams, err := d.getGarageClientParams(ctx, currBootstrap)
if err != nil {
return fmt.Errorf("getting garage client params: %w", err)
}
var ( var (
b = currBootstrap host = currBootstrap.ThisHost()
host = b.ThisHost() client = garageClientParams.GlobalBucketS3APIClient()
client = b.GarageClientParams().GlobalBucketS3APIClient()
) )
configured, err := nebula.Sign( configured, err := nebula.Sign(
host.HostConfigured, b.PrivateCredentials.SigningPrivateKey, host.HostConfigured, currBootstrap.PrivateCredentials.SigningPrivateKey,
) )
if err != nil { if err != nil {
return fmt.Errorf("signing host configured data: %w", err) return fmt.Errorf("signing host configured data: %w", err)
} }
hostB, err := json.Marshal(bootstrap.AuthenticatedHost{ hostB, err := json.Marshal(bootstrap.AuthenticatedHost{
Assigned: b.SignedHostAssigned, Assigned: currBootstrap.SignedHostAssigned,
Configured: configured, Configured: configured,
}) })
if err != nil { if err != nil {
@ -107,14 +111,18 @@ func putGarageBoostrapHost(
return nil return nil
} }
func getGarageBootstrapHosts( func (d *daemon) getGarageBootstrapHosts(
ctx context.Context, logger *mlog.Logger, currBootstrap bootstrap.Bootstrap, ctx context.Context, logger *mlog.Logger, currBootstrap bootstrap.Bootstrap,
) ( ) (
map[nebula.HostName]bootstrap.Host, error, map[nebula.HostName]bootstrap.Host, error,
) { ) {
garageClientParams, err := d.getGarageClientParams(ctx, currBootstrap)
if err != nil {
return nil, fmt.Errorf("getting garage client params: %w", err)
}
var ( var (
b = currBootstrap client = garageClientParams.GlobalBucketS3APIClient()
client = b.GarageClientParams().GlobalBucketS3APIClient()
hosts = map[nebula.HostName]bootstrap.Host{} hosts = map[nebula.HostName]bootstrap.Host{}
objInfoCh = client.ListObjects( objInfoCh = client.ListObjects(
@ -152,7 +160,7 @@ func getGarageBootstrapHosts(
continue continue
} }
host, err := authedHost.Unwrap(b.CAPublicCredentials) host, err := authedHost.Unwrap(currBootstrap.CAPublicCredentials)
if err != nil { if err != nil {
logger.Warn(ctx, "Host could not be authenticated", err) logger.Warn(ctx, "Host could not be authenticated", err)
} }

View File

@ -4,6 +4,11 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"time" "time"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog" "dev.mediocregopher.com/mediocre-go-lib.git/mlog"
@ -38,3 +43,34 @@ func randStr(l int) string {
} }
return hex.EncodeToString(b) return hex.EncodeToString(b)
} }
// mkDir is like os.Mkdir but it returns better error messages. If the directory
// already exists then nil is returned.
func mkDir(path string) error {
{
parentPath := filepath.Dir(path)
parentInfo, err := os.Stat(parentPath)
if err != nil {
return fmt.Errorf("checking fs node of parent %q: %w", parentPath, err)
} else if !parentInfo.IsDir() {
return fmt.Errorf("%q is not a directory", parentPath)
}
}
info, err := os.Stat(path)
if errors.Is(err, fs.ErrNotExist) {
// fine
} else if err != nil {
return fmt.Errorf("checking fs node: %w", err)
} else if !info.IsDir() {
return fmt.Errorf("exists but is not a directory")
} else {
return nil
}
if err := os.Mkdir(path, 0700); err != nil {
return fmt.Errorf("creating directory: %w", err)
}
return nil
}

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"
@ -86,21 +87,14 @@ func (r *RPC) GetHosts(
return GetHostsResult{hosts}, nil return GetHostsResult{hosts}, nil
} }
// GetGarageClientParams returns a GarageClientParams which can be used to // GetGarageClientParams passes the call through to the Daemon method of the
// interact with garage. // same name.
func (r *RPC) GetGarageClientParams( func (r *RPC) GetGarageClientParams(
ctx context.Context, req struct{}, ctx context.Context, req struct{},
) ( ) (
bootstrap.GarageClientParams, error, GarageClientParams, error,
) { ) {
b, err := r.daemon.GetBootstrap(ctx) return r.daemon.GetGarageClientParams(ctx)
if err != nil {
return bootstrap.GarageClientParams{}, fmt.Errorf(
"retrieving bootstrap: %w", err,
)
}
return b.GarageClientParams(), nil
} }
// GetNebulaCAPublicCredentials returns the CAPublicCredentials for the network. // GetNebulaCAPublicCredentials returns the CAPublicCredentials for the network.
@ -119,7 +113,7 @@ func (r *RPC) GetNebulaCAPublicCredentials(
return b.CAPublicCredentials, nil return b.CAPublicCredentials, nil
} }
// RemoveHostRequest contains the arguments to the RemoveHost method. // RemoveHostRequest contains the arguments to the RemoveHost RPC method.
// //
// All fields are required. // All fields are required.
type RemoveHostRequest struct { type RemoveHostRequest struct {
@ -130,3 +124,70 @@ type RemoveHostRequest struct {
func (r *RPC) RemoveHost(ctx context.Context, req RemoveHostRequest) (struct{}, error) { func (r *RPC) RemoveHost(ctx context.Context, req RemoveHostRequest) (struct{}, error) {
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
// CreateNebulaCertificate RPC method.
//
// All fields are required.
type CreateNebulaCertificateRequest struct {
CASigningPrivateKey nebula.SigningPrivateKey // TODO load from secrets storage
HostName nebula.HostName
HostEncryptingPublicKey nebula.EncryptingPublicKey
}
// CreateNebulaCertificateResult wraps the results from the
// CreateNebulaCertificate RPC method.
type CreateNebulaCertificateResult struct {
HostNebulaCertifcate nebula.Certificate
}
// CreateNebulaCertificate passes the call through to the Daemon method of the
// same name.
func (r *RPC) CreateNebulaCertificate(
ctx context.Context, req CreateNebulaCertificateRequest,
) (
CreateNebulaCertificateResult, error,
) {
cert, err := r.daemon.CreateNebulaCertificate(
ctx, req.CASigningPrivateKey, req.HostName, req.HostEncryptingPublicKey,
)
if err != nil {
return CreateNebulaCertificateResult{}, err
}
return CreateNebulaCertificateResult{
HostNebulaCertifcate: cert,
}, nil
}

View File

@ -11,6 +11,8 @@ const (
// accessible to all hosts in the network. // accessible to all hosts in the network.
GlobalBucket = "global-shared" GlobalBucket = "global-shared"
// GlobalBucketS3APICredentialsName is the main alias of the shared API key
// used to write to the global bucket.
GlobalBucketS3APICredentialsName = "global-shared-key" GlobalBucketS3APICredentialsName = "global-shared-key"
// ReplicationFactor indicates the replication factor set on the garage // ReplicationFactor indicates the replication factor set on the garage

20
go/garage/secrets.go Normal file
View File

@ -0,0 +1,20 @@
package garage
import "isle/secrets"
var (
rpcSecretSecretID = secrets.NewID("garage", "rpc-secret")
globalBucketS3APICredentialsSecretID = secrets.NewID("garage", "global-bucket-s3-api-credentials")
)
// Get/Set functions for garage-related secrets.
var (
GetRPCSecret, SetRPCSecret = secrets.GetSetFunctions[string](
rpcSecretSecretID,
)
GetGlobalBucketS3APICredentials,
SetGlobalBucketS3APICredentials = secrets.GetSetFunctions[S3APICredentials](
globalBucketS3APICredentialsSecretID,
)
)

View File

@ -46,7 +46,7 @@ func (pk *EncryptingPublicKey) UnmarshalText(b []byte) error {
// UnmarshalNebulaPEM unmarshals the EncryptingPublicKey as a nebula host public // UnmarshalNebulaPEM unmarshals the EncryptingPublicKey as a nebula host public
// key PEM. // key PEM.
func (pk *EncryptingPublicKey) UnmarshalNebulaPEM(b []byte) error { func (pk *EncryptingPublicKey) UnmarshalNebulaPEM(b []byte) error {
b, _, err := cert.UnmarshalEd25519PublicKey(b) b, _, err := cert.UnmarshalX25519PublicKey(b)
if err != nil { if err != nil {
return fmt.Errorf("unmarshaling: %w", err) return fmt.Errorf("unmarshaling: %w", err)
} }

14
go/nebula/secrets.go Normal file
View File

@ -0,0 +1,14 @@
package nebula
import (
"isle/secrets"
)
var (
caSigningPrivateKeySecretID = secrets.NewID("nebula", "ca-signing-private-key")
)
// Get/Set functions for the CA SigningPrivateKey secret.
var GetCASigningPrivateKey, SetCASigningPrivateKey = secrets.GetSetFunctions[SigningPrivateKey](
caSigningPrivateKeySecretID,
)

13
go/secrets/secrets.go Normal file
View File

@ -0,0 +1,13 @@
// Package secrets manages the storage and distributions of secret values that
// hosts need to perform various actions.
package secrets
import "fmt"
// ID is a unique identifier for a Secret.
type ID string
// NewID returns a new ID within the given namespace.
func NewID(namespace, id string) ID {
return ID(fmt.Sprintf("%s-%s", namespace, id))
}

66
go/secrets/store.go Normal file
View File

@ -0,0 +1,66 @@
package secrets
import (
"context"
"errors"
"fmt"
)
// ErrNotFound is returned when an ID could not be found.
var ErrNotFound = errors.New("not found")
// Store is used to persist and retrieve secrets. If a Store serializes a
// payload it will do so using JSON.
type Store interface {
// Set stores the secret payload of the given ID.
Set(context.Context, ID, any) error
// Get retrieves the secret of the given ID, setting it into the given
// pointer value, or returns ErrNotFound.
Get(context.Context, any, ID) error
}
// GetSetFunctions returns a Get/Set function pair for the given ID and payload
// type.
func GetSetFunctions[T any](
id ID,
) (
func(context.Context, Store) (T, error), // Get
func(context.Context, Store, T) error, // Set
) {
var (
get = func(ctx context.Context, store Store) (T, error) {
var v T
err := store.Get(ctx, &v, id)
return v, err
}
set = func(ctx context.Context, store Store, v T) error {
return store.Set(ctx, id, v)
}
)
return get, set
}
// MultiSet will call Set on the given Store for every key-value pair in the
// given map.
func MultiSet(ctx context.Context, s Store, m map[ID]any) error {
var errs []error
for id, payload := range m {
if err := s.Set(ctx, id, payload); err != nil {
errs = append(errs, fmt.Errorf("setting payload for %q: %w", id, err))
}
}
return errors.Join(errs...)
}
// MultiGet will call Get on the given Store for every key-value pair in the
// given map. Each value in the map must be a pointer receiver.
func MultiGet(ctx context.Context, s Store, m map[ID]any) error {
var errs []error
for id, into := range m {
if err := s.Get(ctx, into, id); err != nil {
errs = append(errs, fmt.Errorf("getting payload for %q: %w", id, err))
}
}
return errors.Join(errs...)
}

83
go/secrets/store_fs.go Normal file
View File

@ -0,0 +1,83 @@
package secrets
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
)
type fsStore struct {
dirPath string
}
type fsStorePayload[Body any] struct {
Version int
Body Body
}
// NewFSStore returns a Store which will store secrets to the given directory.
func NewFSStore(dirPath string) (Store, error) {
err := os.Mkdir(dirPath, 0700)
if err != nil && !errors.Is(err, fs.ErrExist) {
return nil, fmt.Errorf("making directory: %w", err)
}
return &fsStore{dirPath}, nil
}
func (s *fsStore) path(id ID) string {
return filepath.Join(s.dirPath, string(id))
}
func (s *fsStore) Set(_ context.Context, id ID, payload any) error {
path := s.path(id)
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating file %q: %w", path, err)
}
defer f.Close()
if err := json.NewEncoder(f).Encode(fsStorePayload[any]{
Version: 1,
Body: payload,
}); err != nil {
return fmt.Errorf("writing JSON encoded payload to %q: %w", path, err)
}
return nil
}
func (s *fsStore) Get(_ context.Context, into any, id ID) error {
path := s.path(id)
f, err := os.Open(path)
if errors.Is(err, fs.ErrNotExist) {
return ErrNotFound
} else if err != nil {
return fmt.Errorf("creating file %q: %w", path, err)
}
defer f.Close()
var fullPayload fsStorePayload[json.RawMessage]
if err := json.NewDecoder(f).Decode(&fullPayload); err != nil {
return fmt.Errorf("decoding JSON payload from %q: %w", path, err)
}
if fullPayload.Version != 1 {
return fmt.Errorf(
"unexpected JSON payload version %d", fullPayload.Version,
)
}
if err := json.Unmarshal(fullPayload.Body, into); err != nil {
return fmt.Errorf(
"decoding JSON payload body from %q into %T: %w", path, into, err,
)
}
return nil
}

View File

@ -0,0 +1,42 @@
package secrets
import (
"context"
"errors"
"testing"
)
func Test_fsStore(t *testing.T) {
type payload struct {
Foo int
}
var (
ctx = context.Background()
dir = t.TempDir()
id = NewID("testing", "a")
)
store, err := NewFSStore(dir)
if err != nil {
t.Fatal(err)
}
var got payload
if err := store.Get(ctx, &got, id); !errors.Is(err, ErrNotFound) {
t.Fatalf("expected %v, got: %v", ErrNotFound, err)
}
want := payload{Foo: 5}
if err := store.Set(ctx, id, want); err != nil {
t.Fatal(err)
}
if err := store.Get(ctx, &got, id); err != nil {
t.Fatal(err)
}
if want != got {
t.Fatalf("wanted %+v, got: %+v", want, got)
}
}

View File

@ -9,8 +9,6 @@ source "$UTILS"/with-1-data-1-empty-node-network.sh
[ "$(jq -r <admin.json '.CreationParams.Name')" = "testing" ] [ "$(jq -r <admin.json '.CreationParams.Name')" = "testing" ]
[ "$(jq -r <admin.json '.CreationParams.Domain')" = "shared.test" ] [ "$(jq -r <admin.json '.CreationParams.Domain')" = "shared.test" ]
bootstrap_file="$XDG_STATE_HOME/isle/bootstrap.json" [ "$(jq -rc <"$BOOTSTRAP_FILE" '.AdminCreationParams')" = "$(jq -rc <admin.json '.CreationParams')" ]
[ "$(jq -rc <"$BOOTSTRAP_FILE" '.CAPublicCredentials')" = "$(jq -rc <admin.json '.Nebula.CACredentials.Public')" ]
[ "$(jq -rc <"$bootstrap_file" '.AdminCreationParams')" = "$(jq -rc <admin.json '.CreationParams')" ] [ "$(jq -r <"$BOOTSTRAP_FILE" '.SignedHostAssigned.Body.Name')" = "primus" ]
[ "$(jq -rc <"$bootstrap_file" '.CAPublicCredentials')" = "$(jq -rc <admin.json '.Nebula.CACredentials.Public')" ]
[ "$(jq -r <"$bootstrap_file" '.SignedHostAssigned.Body.Name')" = "primus" ]

View File

@ -0,0 +1,12 @@
# shellcheck source=../../utils/with-1-data-1-empty-node-network.sh
source "$UTILS"/with-1-data-1-empty-node-network.sh
info="$(isle nebula show)"
[ "$(echo "$info" | jq -r '.CACert')" \
= "$(jq -r <"$BOOTSTRAP_FILE" '.CAPublicCredentials.Cert')" ]
[ "$(echo "$info" | jq -r '.SubnetCIDR')" = "10.6.9.0/24" ]
[ "$(echo "$info" | jq -r '.Lighthouses|length')" = "1" ]
[ "$(echo "$info" | jq -r '.Lighthouses[0].PublicAddr')" = "127.0.0.1:60000" ]
[ "$(echo "$info" | jq -r '.Lighthouses[0].IP')" = "10.6.9.1" ]

View File

@ -0,0 +1,19 @@
# shellcheck source=../../utils/with-1-data-1-empty-node-network.sh
source "$UTILS"/with-1-data-1-empty-node-network.sh
nebula-cert keygen -out-key /dev/null -out-pub pubkey
cat pubkey
(
isle nebula create-cert \
--admin-path admin.json \
--hostname non-esiste \
--public-key-path pubkey \
2>&1 || true \
) | grep '\[6\] Host not found'
isle nebula create-cert \
--admin-path admin.json \
--hostname primus \
--public-key-path pubkey \
| grep -- '-----BEGIN NEBULA CERTIFICATE-----'

View File

@ -13,5 +13,6 @@ export TMPDIR="$TMPDIR"
export XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" export XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR"
export XDG_STATE_HOME="$XDG_STATE_HOME" export XDG_STATE_HOME="$XDG_STATE_HOME"
export ISLE_DAEMON_HTTP_SOCKET_PATH="$ROOT_TMPDIR/$base-daemon.sock" export ISLE_DAEMON_HTTP_SOCKET_PATH="$ROOT_TMPDIR/$base-daemon.sock"
BOOTSTRAP_FILE="$XDG_STATE_HOME/isle/bootstrap.json"
cd "$TMPDIR" cd "$TMPDIR"
EOF EOF

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" \