isle/go/bootstrap/garage_global_bucket.go

172 lines
4.1 KiB
Go

package bootstrap
import (
"bytes"
"context"
"isle/garage"
"isle/nebula"
"fmt"
"path/filepath"
"strings"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
"github.com/minio/minio-go/v7"
"gopkg.in/yaml.v3"
)
// Paths within garage's global bucket
const (
garageGlobalBucketBootstrapHostsDirPath = "bootstrap/hosts"
)
// PutGarageBoostrapHost places the <hostname>.yml.signed file for this host
// into garage so that other hosts are able to see relevant configuration for
// it.
func (b Bootstrap) PutGarageBoostrapHost(ctx context.Context) error {
host := b.ThisHost()
client := b.GlobalBucketS3APIClient()
// the base Bootstrap has the public credentials signed by the CA, but we
// need this to be presented in the data stored into garage, so other hosts
// can verify that the stored host object is signed by the host public key,
// and that the host public key is signed by the CA.
host.Nebula.SignedPublicCredentials = b.Nebula.SignedPublicCredentials
hostB, err := yaml.Marshal(host)
if err != nil {
return fmt.Errorf("yaml encoding host data: %w", err)
}
buf := new(bytes.Buffer)
err = nebula.SignAndWrap(buf, b.Nebula.HostCredentials.SigningPrivateKeyPEM, hostB)
if err != nil {
return fmt.Errorf("signing encoded host data: %w", err)
}
filePath := filepath.Join(
garageGlobalBucketBootstrapHostsDirPath,
host.Name+".yml.signed",
)
_, err = client.PutObject(
ctx, garage.GlobalBucket, filePath, buf, int64(buf.Len()),
minio.PutObjectOptions{},
)
if err != nil {
return fmt.Errorf("writing to %q in global bucket: %w", filePath, err)
}
return nil
}
// RemoveGarageBootstrapHost removes the <hostname>.yml.signed for the given
// host from garage.
//
// The given client should be for the global bucket.
func RemoveGarageBootstrapHost(
ctx context.Context, client garage.S3APIClient, hostName string,
) error {
filePath := filepath.Join(
garageGlobalBucketBootstrapHostsDirPath,
hostName+".yml.signed",
)
return client.RemoveObject(
ctx, garage.GlobalBucket, filePath,
minio.RemoveObjectOptions{},
)
}
// GetGarageBootstrapHosts loads the <hostname>.yml.signed file for all hosts
// stored in garage.
func (b Bootstrap) GetGarageBootstrapHosts(
ctx context.Context,
logger *mlog.Logger,
) (
map[string]Host, error,
) {
client := b.GlobalBucketS3APIClient()
hosts := map[string]Host{}
objInfoCh := client.ListObjects(
ctx, garage.GlobalBucket,
minio.ListObjectsOptions{
Prefix: garageGlobalBucketBootstrapHostsDirPath,
Recursive: true,
},
)
for objInfo := range objInfoCh {
ctx := mctx.Annotate(ctx, "objectKey", objInfo.Key)
if objInfo.Err != nil {
return nil, fmt.Errorf("listing objects: %w", objInfo.Err)
}
obj, err := client.GetObject(
ctx, garage.GlobalBucket, objInfo.Key, minio.GetObjectOptions{},
)
if err != nil {
return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err)
}
hostB, hostSig, err := nebula.Unwrap(obj)
obj.Close()
if err != nil {
return nil, fmt.Errorf("unwrapping signature from %q: %w", objInfo.Key, err)
}
var host Host
if err = yaml.Unmarshal(hostB, &host); err != nil {
return nil, fmt.Errorf("yaml decoding object %q: %w", objInfo.Key, err)
}
hostPublicCredsB, hostPublicCredsSig, err := nebula.Unwrap(
strings.NewReader(host.Nebula.SignedPublicCredentials),
)
if err != nil {
logger.Warn(ctx, "unwrapping signed public creds", err)
continue
}
err = nebula.ValidateSignature(
b.Nebula.CAPublicCredentials.SigningKeyPEM,
hostPublicCredsB,
hostPublicCredsSig,
)
if err != nil {
logger.Warn(ctx, "invalid signed public creds", err)
continue
}
var hostPublicCreds nebula.HostPublicCredentials
if err := yaml.Unmarshal(hostPublicCredsB, &hostPublicCreds); err != nil {
logger.Warn(ctx, "yaml unmarshaling signed public creds", err)
continue
}
err = nebula.ValidateSignature(hostPublicCreds.SigningKeyPEM, hostB, hostSig)
if err != nil {
logger.Warn(ctx, "invalid host data", err)
continue
}
hosts[host.Name] = host
}
return hosts, nil
}