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 .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 .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 .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 }