From 86abdb6ae197a4ed6d8eab35f6ff4a68a51b4fd5 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sun, 14 Jul 2024 11:58:39 +0200 Subject: [PATCH] Propagate garage RPC secret with created host bootstrap --- go/bootstrap/bootstrap.go | 31 ++--------- go/cmd/entrypoint/hosts.go | 3 +- go/cmd/entrypoint/network.go | 6 +- go/daemon/bootstrap.go | 19 +++++-- go/daemon/children.go | 3 +- go/daemon/daemon.go | 55 +++++++++++++------ go/daemon/garage_client_params.go | 2 +- go/daemon/rpc.go | 8 +-- go/daemon/secrets.go | 39 +++++++++++++ go/garage/secrets.go | 20 ------- go/secrets/store.go | 39 +++++++++++++ tests/cases/admin/02-create-bootstrap.sh | 14 ----- tests/cases/hosts/01-create.sh | 16 ++++++ .../00-create.sh} | 0 14 files changed, 161 insertions(+), 94 deletions(-) create mode 100644 go/daemon/secrets.go delete mode 100644 go/garage/secrets.go delete mode 100644 tests/cases/admin/02-create-bootstrap.sh create mode 100644 tests/cases/hosts/01-create.sh rename tests/cases/{admin/01-create-network.sh => network/00-create.sh} (100%) diff --git a/go/bootstrap/bootstrap.go b/go/bootstrap/bootstrap.go index 3f7c31e..2ea9078 100644 --- a/go/bootstrap/bootstrap.go +++ b/go/bootstrap/bootstrap.go @@ -6,12 +6,10 @@ import ( "crypto/sha512" "encoding/json" "fmt" - "io" "isle/admin" "isle/garage" "isle/nebula" "net/netip" - "os" "path/filepath" "sort" ) @@ -36,8 +34,8 @@ type Garage struct { GlobalBucketS3APICredentials garage.S3APICredentials } -// Bootstrap is used for accessing all information contained within a -// bootstrap.json file. +// Bootstrap contains all information which is needed by a host daemon to join a +// network on boot. type Bootstrap struct { AdminCreationParams admin.CreationParams CAPublicCredentials nebula.CAPublicCredentials @@ -89,23 +87,9 @@ func New( }, nil } -// FromFile reads a bootstrap from a file at the given path. The HostAssigned -// field will automatically be unwrapped. -func FromFile(path string) (Bootstrap, error) { - f, err := os.Open(path) - if err != nil { - return Bootstrap{}, fmt.Errorf("opening file: %w", err) - } - defer f.Close() - - var b Bootstrap - if err := json.NewDecoder(f).Decode(&b); err != nil { - return Bootstrap{}, fmt.Errorf("decoding json: %w", err) - } - - return b, nil -} - +// UnmarshalJSON implements the json.Unmarshaler interface. It will +// automatically populate the HostAssigned field by unwrapping the +// SignedHostAssigned field. func (b *Bootstrap) UnmarshalJSON(data []byte) error { type inner Bootstrap @@ -124,11 +108,6 @@ func (b *Bootstrap) UnmarshalJSON(data []byte) error { return nil } -// WriteTo writes the Bootstrap as a new bootstrap to the given io.Writer. -func (b Bootstrap) WriteTo(into io.Writer) error { - return json.NewEncoder(into).Encode(b) -} - // ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the // HostName isn't found in the Hosts map. func (b Bootstrap) ThisHost() Host { diff --git a/go/cmd/entrypoint/hosts.go b/go/cmd/entrypoint/hosts.go index 957e6c6..40a0b0b 100644 --- a/go/cmd/entrypoint/hosts.go +++ b/go/cmd/entrypoint/hosts.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "errors" "fmt" "isle/bootstrap" @@ -67,7 +68,7 @@ var subCmdHostsCreate = subCmd{ return fmt.Errorf("calling CreateHost: %w", err) } - return res.HostBootstrap.WriteTo(os.Stdout) + return json.NewEncoder(os.Stdout).Encode(res.JoiningBootstrap) }, } diff --git a/go/cmd/entrypoint/network.go b/go/cmd/entrypoint/network.go index d31d87f..db38758 100644 --- a/go/cmd/entrypoint/network.go +++ b/go/cmd/entrypoint/network.go @@ -4,8 +4,8 @@ import ( "errors" "fmt" "isle/admin" - "isle/bootstrap" "isle/daemon" + "isle/jsonutil" "os" ) @@ -88,8 +88,8 @@ var subCmdNetworkJoin = subCmd{ return errors.New("--bootstrap-path is required") } - newBootstrap, err := bootstrap.FromFile(*bootstrapPath) - if err != nil { + var newBootstrap daemon.JoiningBootstrap + if err := jsonutil.LoadFile(&newBootstrap, *bootstrapPath); err != nil { return fmt.Errorf( "loading bootstrap from %q: %w", *bootstrapPath, err, ) diff --git a/go/daemon/bootstrap.go b/go/daemon/bootstrap.go index b37678b..125b9f9 100644 --- a/go/daemon/bootstrap.go +++ b/go/daemon/bootstrap.go @@ -1,14 +1,24 @@ package daemon import ( + "encoding/json" "fmt" "os" "path/filepath" "isle/bootstrap" "isle/garage/garagesrv" + "isle/jsonutil" + "isle/secrets" ) +// JoiningBootstrap wraps a normal Bootstrap to include extra data which a host +// might need while joining a network. +type JoiningBootstrap struct { + Bootstrap bootstrap.Bootstrap + Secrets map[secrets.ID]json.RawMessage +} + func writeBootstrapToStateDir( stateDirPath string, hostBootstrap bootstrap.Bootstrap, ) error { @@ -21,14 +31,11 @@ func writeBootstrapToStateDir( return fmt.Errorf("creating directory %q: %w", dirPath, err) } - f, err := os.Create(path) - if err != nil { - return fmt.Errorf("creating file %q: %w", path, err) + if err := jsonutil.WriteFile(hostBootstrap, path, 0700); err != nil { + return fmt.Errorf("writing bootstrap to %q: %w", path, err) } - defer f.Close() - - return hostBootstrap.WriteTo(f) + return nil } func coalesceDaemonConfigAndBootstrap( diff --git a/go/daemon/children.go b/go/daemon/children.go index 16bdf99..74b0554 100644 --- a/go/daemon/children.go +++ b/go/daemon/children.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "isle/bootstrap" - "isle/garage" "isle/secrets" "code.betamike.com/micropelago/pmux/pmuxlib" @@ -41,7 +40,7 @@ func NewChildren( opts = opts.withDefaults() logger.Info(ctx, "Loading secrets") - garageRPCSecret, err := garage.GetRPCSecret(ctx, secretsStore) + garageRPCSecret, err := getGarageRPCSecret(ctx, secretsStore) if err != nil && !errors.Is(err, secrets.ErrNotFound) { return nil, fmt.Errorf("loading garage RPC secret: %w", err) } diff --git a/go/daemon/daemon.go b/go/daemon/daemon.go index 128d4f7..8ac7115 100644 --- a/go/daemon/daemon.go +++ b/go/daemon/daemon.go @@ -12,6 +12,7 @@ import ( "isle/admin" "isle/bootstrap" "isle/garage" + "isle/jsonutil" "isle/nebula" "isle/secrets" "net/netip" @@ -50,7 +51,7 @@ type Daemon interface { // // Errors: // - ErrAlreadyJoined - JoinNetwork(context.Context, bootstrap.Bootstrap) error + JoinNetwork(context.Context, JoiningBootstrap) error // GetBootstraps returns the currently active Bootstrap. GetBootstrap(context.Context) (bootstrap.Bootstrap, error) @@ -70,7 +71,7 @@ type Daemon interface { hostName nebula.HostName, ip netip.Addr, // TODO automatically choose IP address ) ( - bootstrap.Bootstrap, error, + JoiningBootstrap, error, ) // CreateNebulaCertificate creates and signs a new nebula certficate for an @@ -200,7 +201,8 @@ func NewDaemon( ) } - currBootstrap, err := bootstrap.FromFile(bootstrapFilePath) + var currBootstrap bootstrap.Bootstrap + err = jsonutil.LoadFile(&currBootstrap, bootstrapFilePath) if errors.Is(err, fs.ErrNotExist) { // daemon has never had a network created or joined } else if err != nil { @@ -538,7 +540,7 @@ func (d *daemon) CreateNetwork( garageRPCSecret := randStr(32) - err = garage.SetRPCSecret(ctx, d.secretsStore, garageRPCSecret) + err = setGarageRPCSecret(ctx, d.secretsStore, garageRPCSecret) if err != nil { return admin.Admin{}, fmt.Errorf("storing garage RPC secret: %w", err) } @@ -597,7 +599,7 @@ func (d *daemon) CreateNetwork( } func (d *daemon) JoinNetwork( - ctx context.Context, newBootstrap bootstrap.Bootstrap, + ctx context.Context, newBootstrap JoiningBootstrap, ) error { d.l.Lock() @@ -608,7 +610,13 @@ func (d *daemon) JoinNetwork( readyCh := make(chan struct{}, 1) - err := d.initialize(newBootstrap, readyCh) + err := secrets.Import(ctx, d.secretsStore, newBootstrap.Secrets) + if err != nil { + d.l.Unlock() + return fmt.Errorf("importing secrets: %w", err) + } + + err = d.initialize(newBootstrap.Bootstrap, readyCh) d.l.Unlock() if err != nil { return fmt.Errorf("initializing daemon: %w", err) @@ -681,21 +689,26 @@ func (d *daemon) CreateHost( hostName nebula.HostName, ip netip.Addr, ) ( - bootstrap.Bootstrap, error, + JoiningBootstrap, error, ) { return withCurrBootstrap(d, func( currBootstrap bootstrap.Bootstrap, ) ( - bootstrap.Bootstrap, error, + JoiningBootstrap, error, ) { - garageGlobalBucketS3APICreds := currBootstrap.Garage.GlobalBucketS3APICredentials + var ( + garageGlobalBucketS3APICreds = currBootstrap.Garage.GlobalBucketS3APICredentials - garageBootstrap := bootstrap.Garage{ - AdminToken: randStr(32), - GlobalBucketS3APICredentials: garageGlobalBucketS3APICreds, - } + garageBootstrap = bootstrap.Garage{ + AdminToken: randStr(32), + GlobalBucketS3APICredentials: garageGlobalBucketS3APICreds, + } - newHostBootstrap, err := bootstrap.New( + joiningBootstrap JoiningBootstrap + err error + ) + + joiningBootstrap.Bootstrap, err = bootstrap.New( makeCACreds(currBootstrap, caSigningPrivateKey), currBootstrap.AdminCreationParams, garageBootstrap, @@ -703,17 +716,25 @@ func (d *daemon) CreateHost( ip, ) if err != nil { - return bootstrap.Bootstrap{}, fmt.Errorf( + return JoiningBootstrap{}, fmt.Errorf( "initializing bootstrap data: %w", err, ) } - newHostBootstrap.Hosts = currBootstrap.Hosts + joiningBootstrap.Bootstrap.Hosts = currBootstrap.Hosts + + if joiningBootstrap.Secrets, err = secrets.Export( + ctx, d.secretsStore, []secrets.ID{ + garageRPCSecretSecretID, + }, + ); err != nil { + return JoiningBootstrap{}, fmt.Errorf("exporting secrets: %w", err) + } // TODO persist new bootstrap to garage. Requires making the daemon // config change watching logic smarter, so only dnsmasq gets restarted. - return newHostBootstrap, nil + return joiningBootstrap, nil }) } diff --git a/go/daemon/garage_client_params.go b/go/daemon/garage_client_params.go index c853181..fd16a9a 100644 --- a/go/daemon/garage_client_params.go +++ b/go/daemon/garage_client_params.go @@ -24,7 +24,7 @@ func (d *daemon) getGarageClientParams( ) ( GarageClientParams, error, ) { - rpcSecret, err := garage.GetRPCSecret(ctx, d.secretsStore) + rpcSecret, err := getGarageRPCSecret(ctx, d.secretsStore) if err != nil && !errors.Is(err, secrets.ErrNotFound) { return GarageClientParams{}, fmt.Errorf("getting garage rpc secret: %w", err) } diff --git a/go/daemon/rpc.go b/go/daemon/rpc.go index c80c001..6671d1e 100644 --- a/go/daemon/rpc.go +++ b/go/daemon/rpc.go @@ -56,7 +56,7 @@ func (r *RPC) CreateNetwork( // JoinNetwork passes through to the Daemon method of the same name. func (r *RPC) JoinNetwork( - ctx context.Context, req bootstrap.Bootstrap, + ctx context.Context, req JoiningBootstrap, ) ( struct{}, error, ) { @@ -137,7 +137,7 @@ type CreateHostRequest struct { // CreateHostResult wraps the results from the CreateHost RPC method. type CreateHostResult struct { - HostBootstrap bootstrap.Bootstrap + JoiningBootstrap JoiningBootstrap } // CreateHost passes the call through to the Daemon method of the @@ -147,14 +147,14 @@ func (r *RPC) CreateHost( ) ( CreateHostResult, error, ) { - hostBootstrap, err := r.daemon.CreateHost( + joiningBootstrap, err := r.daemon.CreateHost( ctx, req.CASigningPrivateKey, req.HostName, req.IP, ) if err != nil { return CreateHostResult{}, err } - return CreateHostResult{HostBootstrap: hostBootstrap}, nil + return CreateHostResult{JoiningBootstrap: joiningBootstrap}, nil } // CreateNebulaCertificateRequest contains the arguments to the diff --git a/go/daemon/secrets.go b/go/daemon/secrets.go new file mode 100644 index 0000000..1b363d3 --- /dev/null +++ b/go/daemon/secrets.go @@ -0,0 +1,39 @@ +package daemon + +import ( + "fmt" + "isle/garage" + "isle/secrets" +) + +const ( + secretsNSGarage = "garage" +) + +//////////////////////////////////////////////////////////////////////////////// +// Garage-related secrets + +func garageS3APIBucketCredentialsSecretID(credsName string) secrets.ID { + return secrets.NewID( + secretsNSGarage, fmt.Sprintf("s3-api-bucket-credentials-%s", credsName), + ) +} + +var ( + garageRPCSecretSecretID = secrets.NewID(secretsNSGarage, "rpc-secret") + garageS3APIGlobalBucketCredentialsSecretID = garageS3APIBucketCredentialsSecretID( + garage.GlobalBucketS3APICredentialsName, + ) +) + +// Get/Set functions for garage-related secrets. +var ( + getGarageRPCSecret, setGarageRPCSecret = secrets.GetSetFunctions[string]( + garageRPCSecretSecretID, + ) + + getGarageS3APIGlobalBucketCredentials, + setGarageS3APIGlobalBucketCredentials = secrets.GetSetFunctions[garage.S3APICredentials]( + garageS3APIGlobalBucketCredentialsSecretID, + ) +) diff --git a/go/garage/secrets.go b/go/garage/secrets.go deleted file mode 100644 index d5a85f9..0000000 --- a/go/garage/secrets.go +++ /dev/null @@ -1,20 +0,0 @@ -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, - ) -) diff --git a/go/secrets/store.go b/go/secrets/store.go index 8b65061..0e41cdd 100644 --- a/go/secrets/store.go +++ b/go/secrets/store.go @@ -2,6 +2,7 @@ package secrets import ( "context" + "encoding/json" "errors" "fmt" ) @@ -64,3 +65,41 @@ func MultiGet(ctx context.Context, s Store, m map[ID]any) error { } return errors.Join(errs...) } + +// Export returns a map of ID to raw payload for each ID given. An error is +// returned for _each_ ID which could not be exported, wrapped using +// `errors.Join`, alongside whatever keys could be exported. +func Export( + ctx context.Context, s Store, ids []ID, +) ( + map[ID]json.RawMessage, error, +) { + var ( + m = map[ID]json.RawMessage{} + errs []error + ) + + for _, id := range ids { + var into json.RawMessage + if err := s.Get(ctx, &into, id); err != nil { + errs = append(errs, fmt.Errorf("exporting %q: %w", id, err)) + continue + } + m[id] = into + } + + return m, errors.Join(errs...) +} + +// Import sets all given ID/payload pairs into the Store. +func Import( + ctx context.Context, s Store, m map[ID]json.RawMessage, +) error { + var errs []error + for id, payload := range m { + if err := s.Set(ctx, id, payload); err != nil { + errs = append(errs, fmt.Errorf("importing %q: %w", id, err)) + } + } + return errors.Join(errs...) +} diff --git a/tests/cases/admin/02-create-bootstrap.sh b/tests/cases/admin/02-create-bootstrap.sh deleted file mode 100644 index a60bcd3..0000000 --- a/tests/cases/admin/02-create-bootstrap.sh +++ /dev/null @@ -1,14 +0,0 @@ -# shellcheck source=../../utils/with-1-data-1-empty-node-network.sh -source "$UTILS"/with-1-data-1-empty-node-network.sh - -adminBS="$XDG_STATE_HOME"/isle/bootstrap.json -bs="$secondus_bootstrap" # set in with-1-data-1-empty-node-network.sh - -[ "$(jq -r <"$bs" '.AdminCreationParams')" = "$(jq -r