From 3b6bc3b28f6c4c26ac79864201d56370c195f9f0 Mon Sep 17 00:00:00 2001 From: Leonard Cohnen Date: Sat, 26 Nov 2022 19:44:34 +0100 Subject: [PATCH] initserver: add client verification --- CHANGELOG.md | 8 +- bootstrapper/cmd/bootstrapper/run.go | 6 +- bootstrapper/cmd/bootstrapper/test.go | 4 + bootstrapper/initproto/init.pb.go | 58 +++--- bootstrapper/initproto/init.proto | 1 + .../internal/initserver/initserver.go | 38 +++- .../internal/initserver/initserver_test.go | 192 ++++++++++++------ cli/internal/cloudcmd/clients.go | 2 +- cli/internal/cloudcmd/clients_test.go | 5 +- cli/internal/cloudcmd/create.go | 12 +- cli/internal/clusterid/id.go | 2 + cli/internal/cmd/init.go | 1 + cli/internal/terraform/terraform.go | 23 ++- cli/internal/terraform/terraform/aws/main.tf | 23 +++ .../aws/modules/instance_group/main.tf | 27 +-- .../aws/modules/instance_group/variables.tf | 5 + .../terraform/terraform/aws/outputs.tf | 5 + .../terraform/terraform/azure/main.tf | 11 +- .../terraform/terraform/azure/outputs.tf | 5 + cli/internal/terraform/terraform/gcp/main.tf | 39 ++-- .../gcp/modules/instance_group/main.tf | 7 +- .../gcp/modules/instance_group/variables.tf | 5 + .../terraform/terraform/gcp/outputs.tf | 5 + cli/internal/terraform/terraform/qemu/main.tf | 9 + .../terraform/terraform/qemu/outputs.tf | 5 + cli/internal/terraform/terraform_test.go | 6 +- hack/qemu-metadata-api/main.go | 3 +- hack/qemu-metadata-api/server/server.go | 37 +++- hack/qemu-metadata-api/server/server_test.go | 62 +++++- internal/cloud/aws/cloud.go | 9 + internal/cloud/aws/cloud_test.go | 3 +- internal/cloud/azure/cloud.go | 15 +- internal/cloud/azure/cloud_test.go | 49 +++++ internal/cloud/azure/imds.go | 18 ++ internal/cloud/azure/interface.go | 1 + internal/cloud/cloud.go | 2 + internal/cloud/gcp/cloud.go | 49 ++++- internal/cloud/gcp/cloud_test.go | 117 ++++++++++- internal/cloud/qemu/cloud.go | 10 + 39 files changed, 704 insertions(+), 175 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4afde217..3ecd1bc88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,11 +21,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + - Environment variable `CONSTELL_AZURE_CLIENT_SECRET_VALUE` as an alternative way to provide the configuration value `provider.azure.clientSecretValue`. - - Automatic CSI driver deployment for Azure and GCP during Constellation init - - Improve reproducibility by pinning the Kubernetes components. +- Client verification during `constellation init` ### Changed @@ -51,7 +51,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `constellation create` on GCP now always uses the local default credentials. - ## [2.2.2] - 2022-11-17 ### Fixed @@ -69,6 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security Vulnerabilities in `kube-apiserver` fixed by upgrading to v1.23.14, v1.24.8 and v1.25.4: + - [CVE-2022-3162](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-3162) - [CVE-2022-3294](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-3294) @@ -135,7 +135,9 @@ Vulnerabilities in `kube-apiserver` fixed by upgrading to v1.23.14, v1.24.8 and ### Fixed ### Security + Vulnerability inside the Go standard library fixed by updating to Go 1.19.2: + - [GO-2022-1037](https://pkg.go.dev/vuln/GO-2022-1037) ([CVE-2022-2879](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-2879)) - [GO-2022-1038](https://pkg.go.dev/vuln/GO-2022-1038) ([CVE-2022-2880](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-2880)) - [GO-2022-0969](https://pkg.go.dev/vuln/GO-2022-0969) ([CVE-2022-27664](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-27664)) diff --git a/bootstrapper/cmd/bootstrapper/run.go b/bootstrapper/cmd/bootstrapper/run.go index 1cc9b9c90..1449118b0 100644 --- a/bootstrapper/cmd/bootstrapper/run.go +++ b/bootstrapper/cmd/bootstrapper/run.go @@ -56,7 +56,10 @@ func run(issuerWrapper initserver.IssuerWrapper, tpm vtpm.TPMOpenFunc, fileHandl } nodeLock := nodelock.New(tpm) - initServer := initserver.New(nodeLock, kube, issuerWrapper, fileHandler, log) + initServer, err := initserver.New(context.Background(), nodeLock, kube, issuerWrapper, fileHandler, metadata, log) + if err != nil { + log.With(zap.Error(err)).Fatalf("Failed to create init server") + } dialer := dialer.New(issuerWrapper, nil, &net.Dialer{}) joinClient := joinclient.New(nodeLock, dialer, kube, metadata, log) @@ -92,5 +95,6 @@ type clusterInitJoiner interface { type metadataAPI interface { joinclient.MetadataAPI + initserver.MetadataAPI GetLoadBalancerEndpoint(ctx context.Context) (string, error) } diff --git a/bootstrapper/cmd/bootstrapper/test.go b/bootstrapper/cmd/bootstrapper/test.go index d75199130..0a8e6b462 100644 --- a/bootstrapper/cmd/bootstrapper/test.go +++ b/bootstrapper/cmd/bootstrapper/test.go @@ -56,3 +56,7 @@ func (f *providerMetadataFake) Self(ctx context.Context) (metadata.InstanceMetad func (f *providerMetadataFake) GetLoadBalancerEndpoint(ctx context.Context) (string, error) { return "", nil } + +func (f *providerMetadataFake) InitSecretHash(ctx context.Context) ([]byte, error) { + return nil, nil +} diff --git a/bootstrapper/initproto/init.pb.go b/bootstrapper/initproto/init.pb.go index 91b0f9473..ac7318e7c 100644 --- a/bootstrapper/initproto/init.pb.go +++ b/bootstrapper/initproto/init.pb.go @@ -40,6 +40,7 @@ type InitRequest struct { EnforceIdkeydigest bool `protobuf:"varint,13,opt,name=enforce_idkeydigest,json=enforceIdkeydigest,proto3" json:"enforce_idkeydigest,omitempty"` ConformanceMode bool `protobuf:"varint,14,opt,name=conformance_mode,json=conformanceMode,proto3" json:"conformance_mode,omitempty"` KubernetesComponents []*KubernetesComponent `protobuf:"bytes,15,rep,name=kubernetes_components,json=kubernetesComponents,proto3" json:"kubernetes_components,omitempty"` + InitSecret []byte `protobuf:"bytes,16,opt,name=init_secret,json=initSecret,proto3" json:"init_secret,omitempty"` } func (x *InitRequest) Reset() { @@ -165,6 +166,13 @@ func (x *InitRequest) GetKubernetesComponents() []*KubernetesComponent { return nil } +func (x *InitRequest) GetInitSecret() []byte { + if x != nil { + return x.InitSecret + } + return nil +} + type InitResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -303,7 +311,7 @@ var File_init_proto protoreflect.FileDescriptor var file_init_proto_rawDesc = []byte{ 0x0a, 0x0a, 0x69, 0x6e, 0x69, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x69, 0x6e, - 0x69, 0x74, 0x22, 0xc3, 0x04, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x69, 0x74, 0x22, 0xe4, 0x04, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6b, 0x6d, 0x73, 0x5f, 0x75, @@ -339,29 +347,31 @@ var file_init_proto_rawDesc = []byte{ 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6e, 0x69, 0x74, 0x2e, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x52, 0x14, 0x6b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x43, 0x6f, - 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x68, 0x0a, 0x0c, 0x49, 0x6e, 0x69, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x6b, 0x75, 0x62, 0x65, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x6b, 0x75, - 0x62, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x77, 0x6e, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x77, 0x6e, 0x65, - 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x69, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, - 0x49, 0x64, 0x22, 0x78, 0x0a, 0x13, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, - 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x68, - 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, - 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x50, 0x61, - 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x32, 0x34, 0x0a, 0x03, - 0x41, 0x50, 0x49, 0x12, 0x2d, 0x0a, 0x04, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x11, 0x2e, 0x69, 0x6e, - 0x69, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, - 0x2e, 0x69, 0x6e, 0x69, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x65, 0x64, 0x67, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x73, 0x79, 0x73, 0x2f, 0x63, 0x6f, 0x6e, - 0x73, 0x74, 0x65, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x32, 0x2f, 0x62, 0x6f, - 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x69, 0x74, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x69, 0x74, + 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x69, + 0x6e, 0x69, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x22, 0x68, 0x0a, 0x0c, 0x49, 0x6e, 0x69, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x6b, 0x75, 0x62, + 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x6b, + 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x77, 0x6e, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x77, 0x6e, + 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, + 0x72, 0x49, 0x64, 0x22, 0x78, 0x0a, 0x13, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, + 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, + 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, + 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x50, + 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x32, 0x34, 0x0a, + 0x03, 0x41, 0x50, 0x49, 0x12, 0x2d, 0x0a, 0x04, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x11, 0x2e, 0x69, + 0x6e, 0x69, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x12, 0x2e, 0x69, 0x6e, 0x69, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x73, 0x79, 0x73, 0x2f, 0x63, 0x6f, + 0x6e, 0x73, 0x74, 0x65, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x32, 0x2f, 0x62, + 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x69, 0x74, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/bootstrapper/initproto/init.proto b/bootstrapper/initproto/init.proto index e916b463b..e3d02865c 100644 --- a/bootstrapper/initproto/init.proto +++ b/bootstrapper/initproto/init.proto @@ -24,6 +24,7 @@ message InitRequest { bool enforce_idkeydigest = 13; bool conformance_mode = 14; repeated KubernetesComponent kubernetes_components = 15; + bytes init_secret = 16; } message InitResponse { diff --git a/bootstrapper/internal/initserver/initserver.go b/bootstrapper/internal/initserver/initserver.go index d91a94330..476fd8256 100644 --- a/bootstrapper/internal/initserver/initserver.go +++ b/bootstrapper/internal/initserver/initserver.go @@ -27,6 +27,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/role" "github.com/edgelesssys/constellation/v2/internal/versions" "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/keepalive" @@ -45,22 +46,33 @@ type Server struct { cleaner cleaner issuerWrapper IssuerWrapper + initSecretHash []byte + log *logger.Logger initproto.UnimplementedAPIServer } // New creates a new initialization server. -func New(lock locker, kube ClusterInitializer, issuerWrapper IssuerWrapper, fh file.Handler, log *logger.Logger) *Server { +func New(ctx context.Context, lock locker, kube ClusterInitializer, issuerWrapper IssuerWrapper, fh file.Handler, metadata MetadataAPI, log *logger.Logger) (*Server, error) { log = log.Named("initServer") + initSecretHash, err := metadata.InitSecretHash(ctx) + if err != nil { + return nil, fmt.Errorf("retrieving init secret hash: %w", err) + } + if len(initSecretHash) == 0 { + return nil, fmt.Errorf("init secret hash is empty") + } + server := &Server{ - nodeLock: lock, - disk: diskencryption.New(), - initializer: kube, - fileHandler: fh, - issuerWrapper: issuerWrapper, - log: log, + nodeLock: lock, + disk: diskencryption.New(), + initializer: kube, + fileHandler: fh, + issuerWrapper: issuerWrapper, + log: log, + initSecretHash: initSecretHash, } grpcServer := grpc.NewServer( @@ -71,7 +83,7 @@ func New(lock locker, kube ClusterInitializer, issuerWrapper IssuerWrapper, fh f initproto.RegisterAPIServer(grpcServer, server) server.grpcServer = grpcServer - return server + return server, nil } // Serve starts the initialization server. @@ -92,6 +104,10 @@ func (s *Server) Init(ctx context.Context, req *initproto.InitRequest) (*initpro log := s.log.With(zap.String("peer", grpclog.PeerAddrFromContext(ctx))) log.Infof("Init called") + if err := bcrypt.CompareHashAndPassword(s.initSecretHash, req.InitSecret); err != nil { + return nil, status.Errorf(codes.Internal, "invalid init secret %s", err) + } + // generate values for cluster attestation measurementSalt, clusterID, err := deriveMeasurementValues(req.MasterSecret, req.Salt) if err != nil { @@ -267,3 +283,9 @@ type locker interface { type cleaner interface { Clean() } + +// MetadataAPI provides information about the instances. +type MetadataAPI interface { + // InitSecretHash returns the initSecretHash of the instance. + InitSecretHash(ctx context.Context) ([]byte, error) +} diff --git a/bootstrapper/internal/initserver/initserver_test.go b/bootstrapper/internal/initserver/initserver_test.go index 29b40913a..85b8b2f36 100644 --- a/bootstrapper/internal/initserver/initserver_test.go +++ b/bootstrapper/internal/initserver/initserver_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" + "golang.org/x/crypto/bcrypt" ) func TestMain(m *testing.M) { @@ -31,17 +32,45 @@ func TestMain(m *testing.M) { } func TestNew(t *testing.T) { - assert := assert.New(t) - fh := file.NewHandler(afero.NewMemMapFs()) - server := New(newFakeLock(), &stubClusterInitializer{}, IssuerWrapper{}, fh, logger.NewTest(t)) - assert.NotNil(server) - assert.NotNil(server.log) - assert.NotNil(server.nodeLock) - assert.NotNil(server.initializer) - assert.NotNil(server.grpcServer) - assert.NotNil(server.fileHandler) - assert.NotNil(server.disk) + + testCases := map[string]struct { + metadata stubMetadata + wantErr bool + }{ + "success": { + metadata: stubMetadata{initSecretHashVal: []byte("hash")}, + }, + "empty init secret hash": { + metadata: stubMetadata{initSecretHashVal: nil}, + wantErr: true, + }, + "metadata error": { + metadata: stubMetadata{initSecretHashErr: errors.New("error")}, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + server, err := New(context.TODO(), newFakeLock(), &stubClusterInitializer{}, IssuerWrapper{}, fh, &tc.metadata, logger.NewTest(t)) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.NotNil(server) + assert.NotEmpty(server.initSecretHash) + assert.NotNil(server.log) + assert.NotNil(server.nodeLock) + assert.NotNil(server.initializer) + assert.NotNil(server.grpcServer) + assert.NotNil(server.fileHandler) + assert.NotNil(server.disk) + }) + } } func TestInit(t *testing.T) { @@ -51,70 +80,91 @@ func TestInit(t *testing.T) { require.True(t, aqcuiredLock) require.Nil(t, lockErr) + initSecret := []byte("password") + initSecretHash, err := bcrypt.GenerateFromPassword(initSecret, bcrypt.DefaultCost) + require.NoError(t, err) + testCases := map[string]struct { - nodeLock *fakeLock - initializer ClusterInitializer - disk encryptedDisk - fileHandler file.Handler - req *initproto.InitRequest - wantErr bool - wantShutdown bool + nodeLock *fakeLock + initializer ClusterInitializer + disk encryptedDisk + fileHandler file.Handler + req *initproto.InitRequest + initSecretHash []byte + wantErr bool + wantShutdown bool }{ "successful init": { - nodeLock: newFakeLock(), - initializer: &stubClusterInitializer{}, - disk: &stubDisk{}, - fileHandler: file.NewHandler(afero.NewMemMapFs()), - req: &initproto.InitRequest{}, + nodeLock: newFakeLock(), + initializer: &stubClusterInitializer{}, + disk: &stubDisk{}, + fileHandler: file.NewHandler(afero.NewMemMapFs()), + initSecretHash: initSecretHash, + req: &initproto.InitRequest{InitSecret: initSecret}, }, "node locked": { - nodeLock: lockedLock, - initializer: &stubClusterInitializer{}, - disk: &stubDisk{}, - fileHandler: file.NewHandler(afero.NewMemMapFs()), - req: &initproto.InitRequest{}, - wantErr: true, - wantShutdown: true, + nodeLock: lockedLock, + initializer: &stubClusterInitializer{}, + disk: &stubDisk{}, + fileHandler: file.NewHandler(afero.NewMemMapFs()), + req: &initproto.InitRequest{InitSecret: initSecret}, + initSecretHash: initSecretHash, + wantErr: true, + wantShutdown: true, }, "disk open error": { - nodeLock: newFakeLock(), - initializer: &stubClusterInitializer{}, - disk: &stubDisk{openErr: someErr}, - fileHandler: file.NewHandler(afero.NewMemMapFs()), - req: &initproto.InitRequest{}, - wantErr: true, + nodeLock: newFakeLock(), + initializer: &stubClusterInitializer{}, + disk: &stubDisk{openErr: someErr}, + fileHandler: file.NewHandler(afero.NewMemMapFs()), + req: &initproto.InitRequest{InitSecret: initSecret}, + initSecretHash: initSecretHash, + wantErr: true, }, "disk uuid error": { - nodeLock: newFakeLock(), - initializer: &stubClusterInitializer{}, - disk: &stubDisk{uuidErr: someErr}, - fileHandler: file.NewHandler(afero.NewMemMapFs()), - req: &initproto.InitRequest{}, - wantErr: true, + nodeLock: newFakeLock(), + initializer: &stubClusterInitializer{}, + disk: &stubDisk{uuidErr: someErr}, + fileHandler: file.NewHandler(afero.NewMemMapFs()), + req: &initproto.InitRequest{InitSecret: initSecret}, + initSecretHash: initSecretHash, + wantErr: true, }, "disk update passphrase error": { - nodeLock: newFakeLock(), - initializer: &stubClusterInitializer{}, - disk: &stubDisk{updatePassphraseErr: someErr}, - fileHandler: file.NewHandler(afero.NewMemMapFs()), - req: &initproto.InitRequest{}, - wantErr: true, + nodeLock: newFakeLock(), + initializer: &stubClusterInitializer{}, + disk: &stubDisk{updatePassphraseErr: someErr}, + fileHandler: file.NewHandler(afero.NewMemMapFs()), + req: &initproto.InitRequest{InitSecret: initSecret}, + initSecretHash: initSecretHash, + wantErr: true, }, "write state file error": { - nodeLock: newFakeLock(), - initializer: &stubClusterInitializer{}, - disk: &stubDisk{}, - fileHandler: file.NewHandler(afero.NewReadOnlyFs(afero.NewMemMapFs())), - req: &initproto.InitRequest{}, - wantErr: true, + nodeLock: newFakeLock(), + initializer: &stubClusterInitializer{}, + disk: &stubDisk{}, + fileHandler: file.NewHandler(afero.NewReadOnlyFs(afero.NewMemMapFs())), + req: &initproto.InitRequest{InitSecret: initSecret}, + initSecretHash: initSecretHash, + wantErr: true, }, "initialize cluster error": { - nodeLock: newFakeLock(), - initializer: &stubClusterInitializer{initClusterErr: someErr}, - disk: &stubDisk{}, - fileHandler: file.NewHandler(afero.NewMemMapFs()), - req: &initproto.InitRequest{}, - wantErr: true, + nodeLock: newFakeLock(), + initializer: &stubClusterInitializer{initClusterErr: someErr}, + disk: &stubDisk{}, + fileHandler: file.NewHandler(afero.NewMemMapFs()), + req: &initproto.InitRequest{InitSecret: initSecret}, + initSecretHash: initSecretHash, + wantErr: true, + }, + "wrong initSecret": { + nodeLock: newFakeLock(), + initializer: &stubClusterInitializer{}, + disk: &stubDisk{}, + fileHandler: file.NewHandler(afero.NewMemMapFs()), + initSecretHash: initSecretHash, + req: &initproto.InitRequest{InitSecret: []byte("wrongpassword")}, + wantErr: true, }, } @@ -124,13 +174,14 @@ func TestInit(t *testing.T) { serveStopper := newStubServeStopper() server := &Server{ - nodeLock: tc.nodeLock, - initializer: tc.initializer, - disk: tc.disk, - fileHandler: tc.fileHandler, - log: logger.NewTest(t), - grpcServer: serveStopper, - cleaner: &fakeCleaner{serveStopper: serveStopper}, + nodeLock: tc.nodeLock, + initializer: tc.initializer, + disk: tc.disk, + fileHandler: tc.fileHandler, + log: logger.NewTest(t), + grpcServer: serveStopper, + cleaner: &fakeCleaner{serveStopper: serveStopper}, + initSecretHash: tc.initSecretHash, } kubeconfig, err := server.Init(context.Background(), tc.req) @@ -293,3 +344,12 @@ type fakeCleaner struct { func (f *fakeCleaner) Clean() { go f.serveStopper.GracefulStop() // this is not the correct way to do this, but it's fine for testing } + +type stubMetadata struct { + initSecretHashVal []byte + initSecretHashErr error +} + +func (m *stubMetadata) InitSecretHash(context.Context) ([]byte, error) { + return m.initSecretHashVal, m.initSecretHashErr +} diff --git a/cli/internal/cloudcmd/clients.go b/cli/internal/cloudcmd/clients.go index ba659a77e..195e3f34e 100644 --- a/cli/internal/cloudcmd/clients.go +++ b/cli/internal/cloudcmd/clients.go @@ -17,7 +17,7 @@ import ( type terraformClient interface { PrepareWorkspace(provider cloudprovider.Provider, input terraform.Variables) error - CreateCluster(ctx context.Context) (string, error) + CreateCluster(ctx context.Context) (string, string, error) DestroyCluster(ctx context.Context) error CleanUpWorkspace() error RemoveInstaller() diff --git a/cli/internal/cloudcmd/clients_test.go b/cli/internal/cloudcmd/clients_test.go index 81637663c..8a4d67872 100644 --- a/cli/internal/cloudcmd/clients_test.go +++ b/cli/internal/cloudcmd/clients_test.go @@ -27,6 +27,7 @@ func TestMain(m *testing.M) { type stubTerraformClient struct { ip string + initSecret string cleanUpWorkspaceCalled bool removeInstallerCalled bool destroyClusterCalled bool @@ -36,8 +37,8 @@ type stubTerraformClient struct { cleanUpWorkspaceErr error } -func (c *stubTerraformClient) CreateCluster(ctx context.Context) (string, error) { - return c.ip, c.createClusterErr +func (c *stubTerraformClient) CreateCluster(ctx context.Context) (string, string, error) { + return c.ip, c.initSecret, c.createClusterErr } func (c *stubTerraformClient) PrepareWorkspace(provider cloudprovider.Provider, input terraform.Variables) error { diff --git a/cli/internal/cloudcmd/create.go b/cli/internal/cloudcmd/create.go index b34d4916c..09c6a6dd2 100644 --- a/cli/internal/cloudcmd/create.go +++ b/cli/internal/cloudcmd/create.go @@ -122,13 +122,14 @@ func (c *Creator) createAWS(ctx context.Context, cl terraformClient, config *con } defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerTerraform{client: cl}) - ip, err := cl.CreateCluster(ctx) + ip, initSecret, err := cl.CreateCluster(ctx) if err != nil { return clusterid.File{}, err } return clusterid.File{ CloudProvider: cloudprovider.AWS, + InitSecret: []byte(initSecret), IP: ip, }, nil } @@ -158,13 +159,14 @@ func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *con } defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerTerraform{client: cl}) - ip, err := cl.CreateCluster(ctx) + ip, initSecret, err := cl.CreateCluster(ctx) if err != nil { return clusterid.File{}, err } return clusterid.File{ CloudProvider: cloudprovider.GCP, + InitSecret: []byte(initSecret), IP: ip, }, nil } @@ -197,7 +199,7 @@ func (c *Creator) createAzure(ctx context.Context, cl terraformClient, config *c } defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerTerraform{client: cl}) - ip, err := cl.CreateCluster(ctx) + ip, initSecret, err := cl.CreateCluster(ctx) if err != nil { return clusterid.File{}, err } @@ -205,6 +207,7 @@ func (c *Creator) createAzure(ctx context.Context, cl terraformClient, config *c return clusterid.File{ CloudProvider: cloudprovider.Azure, IP: ip, + InitSecret: []byte(initSecret), }, nil } @@ -309,13 +312,14 @@ func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirt // Allow rollback of QEMU Terraform workspace from this point on qemuRollbacker.createdWorkspace = true - ip, err := cl.CreateCluster(ctx) + ip, initSecret, err := cl.CreateCluster(ctx) if err != nil { return clusterid.File{}, err } return clusterid.File{ CloudProvider: cloudprovider.QEMU, + InitSecret: []byte(initSecret), IP: ip, }, nil } diff --git a/cli/internal/clusterid/id.go b/cli/internal/clusterid/id.go index ad85c72ac..577f672e6 100644 --- a/cli/internal/clusterid/id.go +++ b/cli/internal/clusterid/id.go @@ -22,4 +22,6 @@ type File struct { CloudProvider cloudprovider.Provider `json:"cloudprovider,omitempty"` // IP is the IP address the cluster can be reached at (often the load balancer). IP string `json:"ip,omitempty"` + // InitSecret is the secret the first Bootstrapper uses to verify the user. + InitSecret []byte `json:"initsecret,omitempty"` } diff --git a/cli/internal/cmd/init.go b/cli/internal/cmd/init.go index e211d783d..13a14706e 100644 --- a/cli/internal/cmd/init.go +++ b/cli/internal/cmd/init.go @@ -138,6 +138,7 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator EnforcedPcrs: conf.EnforcedPCRs(), EnforceIdkeydigest: conf.EnforcesIDKeyDigest(), ConformanceMode: flags.conformance, + InitSecret: idFile.InitSecret, } resp, err := initCall(cmd.Context(), newDialer(validator), idFile.IP, req) spinner.Stop() diff --git a/cli/internal/terraform/terraform.go b/cli/internal/terraform/terraform.go index 055a7def7..4a201f2a7 100644 --- a/cli/internal/terraform/terraform.go +++ b/cli/internal/terraform/terraform.go @@ -74,30 +74,39 @@ func (c *Client) PrepareWorkspace(provider cloudprovider.Provider, vars Variable } // CreateCluster creates a Constellation cluster using Terraform. -func (c *Client) CreateCluster(ctx context.Context) (string, error) { +func (c *Client) CreateCluster(ctx context.Context) (string, string, error) { if err := c.tf.Init(ctx); err != nil { - return "", err + return "", "", err } if err := c.tf.Apply(ctx); err != nil { - return "", err + return "", "", err } tfState, err := c.tf.Show(ctx) if err != nil { - return "", err + return "", "", err } ipOutput, ok := tfState.Values.Outputs["ip"] if !ok { - return "", errors.New("no IP output found") + return "", "", errors.New("no IP output found") } ip, ok := ipOutput.Value.(string) if !ok { - return "", errors.New("invalid type in IP output: not a string") + return "", "", errors.New("invalid type in IP output: not a string") } - return ip, nil + secretOutput, ok := tfState.Values.Outputs["initSecret"] + if !ok { + return "", "", errors.New("no initSecret output found") + } + secret, ok := secretOutput.Value.(string) + if !ok { + return "", "", errors.New("invalid type in initSecret output: not a string") + } + + return ip, secret, nil } // DestroyCluster destroys a Constellation cluster using Terraform. diff --git a/cli/internal/terraform/terraform/aws/main.tf b/cli/internal/terraform/terraform/aws/main.tf index e18576b8a..7f04f4dd6 100644 --- a/cli/internal/terraform/terraform/aws/main.tf +++ b/cli/internal/terraform/terraform/aws/main.tf @@ -19,6 +19,7 @@ provider "aws" { locals { uid = random_id.uid.hex name = "${var.name}-${local.uid}" + initSecretHash = random_password.initSecret.bcrypt_hash ports_node_range = "30000-32767" ports_kubernetes = "6443" ports_bootstrapper = "9000" @@ -34,6 +35,12 @@ resource "random_id" "uid" { byte_length = 4 } +resource "random_password" "initSecret" { + length = 32 + special = true + override_special = "_%@" +} + resource "aws_vpc" "vpc" { cidr_block = "192.168.0.0/16" tags = merge(local.tags, { Name = "${local.name}-vpc" }) @@ -230,6 +237,14 @@ module "instance_group_control_plane" { security_groups = [aws_security_group.security_group.id] subnetwork = module.public_private_subnet.private_subnet_id iam_instance_profile = var.iam_instance_profile_control_plane + tags = merge( + local.tags, + { Name = local.name }, + { constellation-role = "control-plane" }, + { constellation-uid = local.uid }, + { KubernetesCluster = "Constellation-${local.uid}" }, + { constellation-init-secret-hash = local.initSecretHash } + ) } module "instance_group_worker_nodes" { @@ -246,4 +261,12 @@ module "instance_group_worker_nodes" { target_group_arns = [] security_groups = [aws_security_group.security_group.id] iam_instance_profile = var.iam_instance_profile_worker_nodes + tags = merge( + local.tags, + { Name = local.name }, + { constellation-role = "worker" }, + { constellation-uid = local.uid }, + { KubernetesCluster = "Constellation-${local.uid}" }, + { constellation-init-secret-hash = local.initSecretHash } + ) } diff --git a/cli/internal/terraform/terraform/aws/modules/instance_group/main.tf b/cli/internal/terraform/terraform/aws/modules/instance_group/main.tf index 8bd49a334..917d2023f 100644 --- a/cli/internal/terraform/terraform/aws/modules/instance_group/main.tf +++ b/cli/internal/terraform/terraform/aws/modules/instance_group/main.tf @@ -57,26 +57,13 @@ resource "aws_autoscaling_group" "autoscaling_group" { vpc_zone_identifier = [var.subnetwork] target_group_arns = var.target_group_arns - tag { - key = "Name" - value = local.name - propagate_at_launch = true - } - tag { - key = "constellation-role" - value = var.role - propagate_at_launch = true - } - tag { - key = "constellation-uid" - value = var.uid - propagate_at_launch = true - } - - tag { - key = "KubernetesCluster" - value = "Constellation-${var.uid}" - propagate_at_launch = true + dynamic "tag" { + for_each = var.tags + content { + key = tag.key + value = tag.value + propagate_at_launch = true + } } lifecycle { diff --git a/cli/internal/terraform/terraform/aws/modules/instance_group/variables.tf b/cli/internal/terraform/terraform/aws/modules/instance_group/variables.tf index 9ebb824c1..444265f48 100644 --- a/cli/internal/terraform/terraform/aws/modules/instance_group/variables.tf +++ b/cli/internal/terraform/terraform/aws/modules/instance_group/variables.tf @@ -57,3 +57,8 @@ variable "security_groups" { type = list(string) description = "List of IDs of the security groups for an instance." } + +variable "tags" { + type = map(string) + description = "The tags to add to the instance group." +} diff --git a/cli/internal/terraform/terraform/aws/outputs.tf b/cli/internal/terraform/terraform/aws/outputs.tf index 63cdfc514..3977e9777 100644 --- a/cli/internal/terraform/terraform/aws/outputs.tf +++ b/cli/internal/terraform/terraform/aws/outputs.tf @@ -1,3 +1,8 @@ output "ip" { value = aws_eip.lb.public_ip } + +output "initSecret" { + value = random_password.initSecret.result + sensitive = true +} diff --git a/cli/internal/terraform/terraform/azure/main.tf b/cli/internal/terraform/terraform/azure/main.tf index 0cd0bfe01..807dc74e1 100644 --- a/cli/internal/terraform/terraform/azure/main.tf +++ b/cli/internal/terraform/terraform/azure/main.tf @@ -18,6 +18,7 @@ provider "azurerm" { locals { uid = random_id.uid.hex name = "${var.name}-${local.uid}" + initSecretHash = random_password.initSecret.bcrypt_hash tags = { constellation-uid = local.uid } ports_node_range = "30000-32767" ports_kubernetes = "6443" @@ -34,6 +35,12 @@ resource "random_id" "uid" { byte_length = 4 } +resource "random_password" "initSecret" { + length = 32 + special = true + override_special = "_%@" +} + resource "azurerm_application_insights" "insights" { name = local.name location = var.location @@ -194,7 +201,7 @@ module "scale_set_control_plane" { instance_type = var.instance_type confidential_vm = var.confidential_vm secure_boot = var.secure_boot - tags = merge(local.tags, { constellation-role = "control-plane" }) + tags = merge(local.tags, { constellation-role = "control-plane" }, { constellation-init-secret-hash = local.initSecretHash }) image_id = var.image_id user_assigned_identity = var.user_assigned_identity network_security_group_id = azurerm_network_security_group.security_group.id @@ -217,7 +224,7 @@ module "scale_set_worker" { instance_type = var.instance_type confidential_vm = var.confidential_vm secure_boot = var.secure_boot - tags = merge(local.tags, { constellation-role = "worker" }) + tags = merge(local.tags, { constellation-role = "worker" }, { constellation-init-secret-hash = local.initSecretHash }) image_id = var.image_id user_assigned_identity = var.user_assigned_identity network_security_group_id = azurerm_network_security_group.security_group.id diff --git a/cli/internal/terraform/terraform/azure/outputs.tf b/cli/internal/terraform/terraform/azure/outputs.tf index 8f71b73ce..0941a9351 100644 --- a/cli/internal/terraform/terraform/azure/outputs.tf +++ b/cli/internal/terraform/terraform/azure/outputs.tf @@ -1,3 +1,8 @@ output "ip" { value = azurerm_public_ip.loadbalancer_ip.ip_address } + +output "initSecret" { + value = random_password.initSecret.result + sensitive = true +} diff --git a/cli/internal/terraform/terraform/gcp/main.tf b/cli/internal/terraform/terraform/gcp/main.tf index b90e0b047..841d17564 100644 --- a/cli/internal/terraform/terraform/gcp/main.tf +++ b/cli/internal/terraform/terraform/gcp/main.tf @@ -20,6 +20,7 @@ provider "google" { locals { uid = random_id.uid.hex name = "${var.name}-${local.uid}" + initSecretHash = random_password.initSecret.bcrypt_hash labels = { constellation-uid = local.uid } ports_node_range = "30000-32767" ports_kubernetes = "6443" @@ -37,6 +38,12 @@ resource "random_id" "uid" { byte_length = 4 } +resource "random_password" "initSecret" { + length = 32 + special = true + override_special = "_%@" +} + resource "google_compute_network" "vpc_network" { name = local.name description = "Constellation VPC network" @@ -136,24 +143,26 @@ module "instance_group_control_plane" { { name = "recovery", port = local.ports_recovery }, var.debug ? [{ name = "debugd", port = local.ports_debugd }] : [], ]) - labels = local.labels + labels = local.labels + init_secret_hash = local.initSecretHash } module "instance_group_worker" { - source = "./modules/instance_group" - name = local.name - role = "Worker" - uid = local.uid - instance_type = var.instance_type - instance_count = var.worker_count - image_id = var.image_id - disk_size = var.state_disk_size - disk_type = var.state_disk_type - network = google_compute_network.vpc_network.id - subnetwork = google_compute_subnetwork.vpc_subnetwork.id - kube_env = local.kube_env - debug = var.debug - labels = local.labels + source = "./modules/instance_group" + name = local.name + role = "Worker" + uid = local.uid + instance_type = var.instance_type + instance_count = var.worker_count + image_id = var.image_id + disk_size = var.state_disk_size + disk_type = var.state_disk_type + network = google_compute_network.vpc_network.id + subnetwork = google_compute_subnetwork.vpc_subnetwork.id + kube_env = local.kube_env + debug = var.debug + labels = local.labels + init_secret_hash = local.initSecretHash } resource "google_compute_global_address" "loadbalancer_ip" { diff --git a/cli/internal/terraform/terraform/gcp/modules/instance_group/main.tf b/cli/internal/terraform/terraform/gcp/modules/instance_group/main.tf index a7554afb2..117f2310d 100644 --- a/cli/internal/terraform/terraform/gcp/modules/instance_group/main.tf +++ b/cli/internal/terraform/terraform/gcp/modules/instance_group/main.tf @@ -15,7 +15,7 @@ locals { resource "google_compute_instance_template" "template" { name = local.name machine_type = var.instance_type - tags = ["constellation-${var.uid}"] + tags = ["constellation-${var.uid}"] // Note that this is also applied as a label labels = merge(var.labels, { constellation-role = local.role_dashed }) confidential_instance_config { @@ -41,8 +41,9 @@ resource "google_compute_instance_template" "template" { } metadata = { - kube-env = var.kube_env - serial-port-enable = var.debug ? "TRUE" : "FALSE" + kube-env = var.kube_env + constellation-init-secret-hash = var.init_secret_hash + serial-port-enable = var.debug ? "TRUE" : "FALSE" } network_interface { diff --git a/cli/internal/terraform/terraform/gcp/modules/instance_group/variables.tf b/cli/internal/terraform/terraform/gcp/modules/instance_group/variables.tf index 761996a22..b231b7b65 100644 --- a/cli/internal/terraform/terraform/gcp/modules/instance_group/variables.tf +++ b/cli/internal/terraform/terraform/gcp/modules/instance_group/variables.tf @@ -59,6 +59,11 @@ variable "kube_env" { description = "Kubernetes env." } +variable "init_secret_hash" { + type = string + description = "Hash of the init secret." +} + variable "named_ports" { type = list(object({ name = string, port = number })) default = [] diff --git a/cli/internal/terraform/terraform/gcp/outputs.tf b/cli/internal/terraform/terraform/gcp/outputs.tf index 68bf1de9b..8318b49ae 100644 --- a/cli/internal/terraform/terraform/gcp/outputs.tf +++ b/cli/internal/terraform/terraform/gcp/outputs.tf @@ -1,3 +1,8 @@ output "ip" { value = google_compute_global_address.loadbalancer_ip.address } + +output "initSecret" { + value = random_password.initSecret.result + sensitive = true +} diff --git a/cli/internal/terraform/terraform/qemu/main.tf b/cli/internal/terraform/terraform/qemu/main.tf index 6ff51bae9..8a724dc93 100644 --- a/cli/internal/terraform/terraform/qemu/main.tf +++ b/cli/internal/terraform/terraform/qemu/main.tf @@ -24,6 +24,11 @@ provider "docker" { } } +resource "random_password" "initSecret" { + length = 32 + special = true + override_special = "_%@" +} resource "docker_image" "qemu_metadata" { name = var.metadata_api_image keep_locally = true @@ -39,6 +44,8 @@ resource "docker_container" "qemu_metadata" { "${var.name}-network", "--libvirt-uri", "${var.metadata_libvirt_uri}", + "--initsecrethash", + "${random_password.initSecret.bcrypt_hash}", ] mounts { source = abspath(var.libvirt_socket_path) @@ -47,6 +54,8 @@ resource "docker_container" "qemu_metadata" { } } + + module "control_plane" { source = "./modules/instance_group" role = "control-plane" diff --git a/cli/internal/terraform/terraform/qemu/outputs.tf b/cli/internal/terraform/terraform/qemu/outputs.tf index 4df00fa1a..7d66b57fc 100644 --- a/cli/internal/terraform/terraform/qemu/outputs.tf +++ b/cli/internal/terraform/terraform/qemu/outputs.tf @@ -1,3 +1,8 @@ output "ip" { value = module.control_plane.instance_ips[0] } + +output "initSecret" { + value = random_password.initSecret.result + sensitive = true +} diff --git a/cli/internal/terraform/terraform_test.go b/cli/internal/terraform/terraform_test.go index aa9af1bac..499efdc8c 100644 --- a/cli/internal/terraform/terraform_test.go +++ b/cli/internal/terraform/terraform_test.go @@ -109,6 +109,9 @@ func TestCreateCluster(t *testing.T) { "ip": { Value: "192.0.2.100", }, + "initSecret": { + Value: "initSecret", + }, }, }, } @@ -202,7 +205,7 @@ func TestCreateCluster(t *testing.T) { } require.NoError(c.PrepareWorkspace(tc.provider, tc.vars)) - ip, err := c.CreateCluster(context.Background()) + ip, initSecret, err := c.CreateCluster(context.Background()) if tc.wantErr { assert.Error(err) @@ -210,6 +213,7 @@ func TestCreateCluster(t *testing.T) { } assert.NoError(err) assert.Equal("192.0.2.100", ip) + assert.Equal("initSecret", initSecret) }) } } diff --git a/hack/qemu-metadata-api/main.go b/hack/qemu-metadata-api/main.go index f64eedf9a..0d6c4bf1b 100644 --- a/hack/qemu-metadata-api/main.go +++ b/hack/qemu-metadata-api/main.go @@ -21,6 +21,7 @@ func main() { bindPort := flag.String("port", "8080", "Port to bind to") targetNetwork := flag.String("network", "constellation-network", "Name of the network in QEMU to use") libvirtURI := flag.String("libvirt-uri", "qemu:///system", "URI of the libvirt connection") + initSecretHash := flag.String("initsecrethash", "", "brcypt hash of the init secret") flag.Parse() log := logger.New(logger.JSONLog, zapcore.InfoLevel) @@ -31,7 +32,7 @@ func main() { } defer conn.Close() - serv := server.New(log, *targetNetwork, &virtwrapper.Connect{Conn: conn}) + serv := server.New(log, *targetNetwork, *initSecretHash, &virtwrapper.Connect{Conn: conn}) if err := serv.ListenAndServe(*bindPort); err != nil { log.With(zap.Error(err)).Fatalf("Failed to serve") } diff --git a/hack/qemu-metadata-api/server/server.go b/hack/qemu-metadata-api/server/server.go index f379e37cb..9468d0cd9 100644 --- a/hack/qemu-metadata-api/server/server.go +++ b/hack/qemu-metadata-api/server/server.go @@ -24,17 +24,19 @@ import ( // Server that provides QEMU metadata. type Server struct { - log *logger.Logger - virt virConnect - network string + log *logger.Logger + virt virConnect + network string + initSecretHashVal []byte } // New creates a new Server. -func New(log *logger.Logger, network string, conn virConnect) *Server { +func New(log *logger.Logger, network, initSecretHash string, conn virConnect) *Server { return &Server{ - log: log, - virt: conn, - network: network, + log: log, + virt: conn, + network: network, + initSecretHashVal: []byte(initSecretHash), } } @@ -46,6 +48,7 @@ func (s *Server) ListenAndServe(port string) error { mux.Handle("/log", http.HandlerFunc(s.postLog)) mux.Handle("/pcrs", http.HandlerFunc(s.exportPCRs)) mux.Handle("/endpoint", http.HandlerFunc(s.getEndpoint)) + mux.Handle("/initsecrethash", http.HandlerFunc(s.initSecretHash)) server := http.Server{ Handler: mux, @@ -115,6 +118,26 @@ func (s *Server) listPeers(w http.ResponseWriter, r *http.Request) { log.Infof("Request successful") } +// initSecretHash returns the hash of the init secret. +func (s *Server) initSecretHash(w http.ResponseWriter, r *http.Request) { + log := s.log.With(zap.String("initSecretHash", r.RemoteAddr)) + if r.Method != http.MethodGet { + log.With(zap.String("method", r.Method)).Errorf("Invalid method for /initSecretHash") + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + log.Infof("Serving GET request for /initsecrethash") + + w.Header().Set("Content-Type", "text/plain") + _, err := w.Write(s.initSecretHashVal) + if err != nil { + log.With(zap.Error(err)).Errorf("Failed to write init secret hash") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + log.Infof("Request successful") +} + // getEndpoint returns the IP address of the first control-plane instance. // This allows us to fake a load balancer for QEMU instances. func (s *Server) getEndpoint(w http.ResponseWriter, r *http.Request) { diff --git a/hack/qemu-metadata-api/server/server_test.go b/hack/qemu-metadata-api/server/server_test.go index 0f5bb9b0c..4ff1ba37e 100644 --- a/hack/qemu-metadata-api/server/server_test.go +++ b/hack/qemu-metadata-api/server/server_test.go @@ -72,7 +72,7 @@ func TestListAll(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - server := New(logger.NewTest(t), "test", tc.connect) + server := New(logger.NewTest(t), "test", "initSecretHash", tc.connect) res, err := server.listAll() @@ -149,7 +149,7 @@ func TestListSelf(t *testing.T) { assert := assert.New(t) require := require.New(t) - server := New(logger.NewTest(t), "test", tc.connect) + server := New(logger.NewTest(t), "test", "initSecretHash", tc.connect) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://192.0.0.1/self", nil) require.NoError(err) @@ -211,7 +211,7 @@ func TestListPeers(t *testing.T) { assert := assert.New(t) require := require.New(t) - server := New(logger.NewTest(t), "test", tc.connect) + server := New(logger.NewTest(t), "test", "initSecretHash", tc.connect) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://192.0.0.1/peers", nil) require.NoError(err) @@ -266,7 +266,7 @@ func TestPostLog(t *testing.T) { assert := assert.New(t) require := require.New(t) - server := New(logger.NewTest(t), "test", &stubConnect{}) + server := New(logger.NewTest(t), "test", "initSecretHash", &stubConnect{}) req, err := http.NewRequestWithContext(context.Background(), tc.method, "http://192.0.0.1/logs", tc.message) require.NoError(err) @@ -346,7 +346,7 @@ func TestExportPCRs(t *testing.T) { assert := assert.New(t) require := require.New(t) - server := New(logger.NewTest(t), "test", tc.connect) + server := New(logger.NewTest(t), "test", "initSecretHash", tc.connect) req, err := http.NewRequestWithContext(context.Background(), tc.method, "http://192.0.0.1/pcrs", strings.NewReader(tc.message)) require.NoError(err) @@ -365,6 +365,58 @@ func TestExportPCRs(t *testing.T) { } } +func TestInitSecretHash(t *testing.T) { + defaultConnect := &stubConnect{ + network: stubNetwork{ + leases: []libvirt.NetworkDHCPLease{ + { + IPaddr: "192.0.100.1", + Hostname: "control-plane-0", + }, + }, + }, + } + testCases := map[string]struct { + connect *stubConnect + method string + wantHash string + wantErr bool + }{ + "success": { + connect: defaultConnect, + method: http.MethodGet, + }, + "wrong method": { + connect: defaultConnect, + method: http.MethodPost, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + server := New(logger.NewTest(t), "test", tc.wantHash, defaultConnect) + + req, err := http.NewRequestWithContext(context.Background(), tc.method, "http://192.0.0.1/initsecrethash", nil) + require.NoError(err) + + w := httptest.NewRecorder() + server.initSecretHash(w, req) + + if tc.wantErr { + assert.NotEqual(http.StatusOK, w.Code) + return + } + + assert.Equal(http.StatusOK, w.Code) + assert.Equal(tc.wantHash, w.Body.String()) + }) + } +} + func mustMarshal(t *testing.T, v any) string { t.Helper() b, err := json.Marshal(v) diff --git a/internal/cloud/aws/cloud.go b/internal/cloud/aws/cloud.go index 275b92a14..95008a148 100644 --- a/internal/cloud/aws/cloud.go +++ b/internal/cloud/aws/cloud.go @@ -109,6 +109,15 @@ func (c *Cloud) UID(ctx context.Context) (string, error) { return readInstanceTag(ctx, c.imds, cloud.TagUID) } +// InitSecretHash returns the InitSecretHash of the current instance. +func (c *Cloud) InitSecretHash(ctx context.Context) ([]byte, error) { + initSecretHash, err := readInstanceTag(ctx, c.imds, cloud.TagInitSecretHash) + if err != nil { + return nil, fmt.Errorf("retrieving init secret hash tag: %w", err) + } + return []byte(initSecretHash), nil +} + // GetLoadBalancerEndpoint returns the endpoint of the load balancer. func (c *Cloud) GetLoadBalancerEndpoint(ctx context.Context) (string, error) { uid, err := readInstanceTag(ctx, c.imds, cloud.TagUID) diff --git a/internal/cloud/aws/cloud_test.go b/internal/cloud/aws/cloud_test.go index 0c1b4e828..6533e7cf3 100644 --- a/internal/cloud/aws/cloud_test.go +++ b/internal/cloud/aws/cloud_test.go @@ -67,7 +67,8 @@ func TestSelf(t *testing.T) { }, }, tags: map[string]string{ - cloud.TagRole: "worker", + cloud.TagRole: "worker", + cloud.TagInitSecretHash: "initSecretHash", }, }, wantSelf: metadata.InstanceMetadata{ diff --git a/internal/cloud/azure/cloud.go b/internal/cloud/azure/cloud.go index 2c24fa6a2..c6316f707 100644 --- a/internal/cloud/azure/cloud.go +++ b/internal/cloud/azure/cloud.go @@ -250,6 +250,15 @@ func (c *Cloud) UID(ctx context.Context) (string, error) { return uid, nil } +// InitSecretHash retrieves the InitSecretHash of the current instance. +func (c *Cloud) InitSecretHash(ctx context.Context) ([]byte, error) { + initSecretHash, err := c.imds.initSecretHash(ctx) + if err != nil { + return nil, fmt.Errorf("retrieving init secret hash: %w", err) + } + return []byte(initSecretHash), nil +} + // getLoadBalancer retrieves a load balancer from cloud provider metadata. func (c *Cloud) getLoadBalancer(ctx context.Context, resourceGroup, uid string) (*armnetwork.LoadBalancer, error) { pager := c.loadBalancerAPI.NewListPager(resourceGroup, nil) @@ -283,8 +292,12 @@ func (c *Cloud) getInstance(ctx context.Context, providerID string) (metadata.In if err != nil { return metadata.InstanceMetadata{}, fmt.Errorf("retrieving VM network interfaces: %w", err) } + instance, err := convertToInstanceMetadata(vmResp.VirtualMachineScaleSetVM, networkInterfaces) + if err != nil { + return metadata.InstanceMetadata{}, fmt.Errorf("converting VM to instance metadata: %w", err) + } - return convertToInstanceMetadata(vmResp.VirtualMachineScaleSetVM, networkInterfaces) + return instance, nil } // getNetworkSecurityGroupName returns the security group name of the resource group. diff --git a/internal/cloud/azure/cloud_test.go b/internal/cloud/azure/cloud_test.go index 359c4e52b..5c5338191 100644 --- a/internal/cloud/azure/cloud_test.go +++ b/internal/cloud/azure/cloud_test.go @@ -360,6 +360,7 @@ func TestGetInstance(t *testing.T) { testCases := map[string]struct { scaleSetsVMAPI *stubVirtualMachineScaleSetVMsAPI networkInterfacesAPI *stubNetworkInterfacesAPI + IMDSAPI *stubIMDSAPI providerID string wantInstance metadata.InstanceMetadata wantErr bool @@ -368,6 +369,7 @@ func TestGetInstance(t *testing.T) { scaleSetsVMAPI: successVMAPI, networkInterfacesAPI: successNetworkAPI, providerID: sampleProviderID, + IMDSAPI: &stubIMDSAPI{}, wantInstance: metadata.InstanceMetadata{ Name: "scale-set-name-instance-id", ProviderID: sampleProviderID, @@ -397,6 +399,7 @@ func TestGetInstance(t *testing.T) { }, }, }, + IMDSAPI: &stubIMDSAPI{}, networkInterfacesAPI: successNetworkAPI, providerID: sampleProviderID, wantInstance: metadata.InstanceMetadata{ @@ -408,12 +411,14 @@ func TestGetInstance(t *testing.T) { }, "invalid provider ID": { scaleSetsVMAPI: successVMAPI, + IMDSAPI: &stubIMDSAPI{}, networkInterfacesAPI: successNetworkAPI, providerID: "invalid", wantErr: true, }, "vm API error": { scaleSetsVMAPI: &stubVirtualMachineScaleSetVMsAPI{getErr: someErr}, + IMDSAPI: &stubIMDSAPI{}, networkInterfacesAPI: successNetworkAPI, providerID: sampleProviderID, wantErr: true, @@ -421,6 +426,7 @@ func TestGetInstance(t *testing.T) { "network API error": { scaleSetsVMAPI: successVMAPI, networkInterfacesAPI: &stubNetworkInterfacesAPI{getErr: someErr}, + IMDSAPI: &stubIMDSAPI{}, providerID: sampleProviderID, wantErr: true, }, @@ -431,6 +437,7 @@ func TestGetInstance(t *testing.T) { assert := assert.New(t) metadata := Cloud{ + imds: tc.IMDSAPI, scaleSetsVMAPI: tc.scaleSetsVMAPI, netIfacAPI: tc.networkInterfacesAPI, } @@ -481,6 +488,42 @@ func TestUID(t *testing.T) { } } +func TestInitSecretHash(t *testing.T) { + testCases := map[string]struct { + imdsAPI *stubIMDSAPI + wantErr bool + }{ + "success": { + imdsAPI: &stubIMDSAPI{ + initSecretHashVal: "initSecretHash", + }, + }, + "error": { + imdsAPI: &stubIMDSAPI{ + initSecretHashErr: errors.New("failed"), + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + cloud := &Cloud{ + imds: tc.imdsAPI, + } + initSecretHash, err := cloud.InitSecretHash(context.Background()) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Equal([]byte(tc.imdsAPI.initSecretHashVal), initSecretHash) + }) + } +} + func TestList(t *testing.T) { someErr := errors.New("failed") networkIfaceResponse := &stubNetworkInterfacesAPI{ @@ -978,6 +1021,8 @@ type stubIMDSAPI struct { uidVal string nameErr error nameVal string + initSecretHashVal string + initSecretHashErr error } func (a *stubIMDSAPI) providerID(ctx context.Context) (string, error) { @@ -1000,6 +1045,10 @@ func (a *stubIMDSAPI) name(ctx context.Context) (string, error) { return a.nameVal, a.nameErr } +func (a *stubIMDSAPI) initSecretHash(ctx context.Context) (string, error) { + return a.initSecretHashVal, a.initSecretHashErr +} + type stubVirtualMachineScaleSetVMPager struct { list []armcomputev2.VirtualMachineScaleSetVM fetchErr error diff --git a/internal/cloud/azure/imds.go b/internal/cloud/azure/imds.go index d55b46f16..93f7368ac 100644 --- a/internal/cloud/azure/imds.go +++ b/internal/cloud/azure/imds.go @@ -114,6 +114,24 @@ func (c *imdsClient) uid(ctx context.Context) (string, error) { return "", fmt.Errorf("unable to get uid from metadata tags %v", c.cache.Compute.Tags) } +// initSecretHash returns the hash of the init secret of the cluster, based on the tags on the instance +// the function is called from, which are inherited from the scale set. +func (c *imdsClient) initSecretHash(ctx context.Context) (string, error) { + if c.timeForUpdate() || len(c.cache.Compute.Tags) == 0 { + if err := c.update(ctx); err != nil { + return "", err + } + } + + for _, tag := range c.cache.Compute.Tags { + if tag.Name == cloud.TagInitSecretHash { + return tag.Value, nil + } + } + + return "", fmt.Errorf("unable to get tag %s from metadata tags %v", cloud.TagInitSecretHash, c.cache.Compute.Tags) +} + // role returns the role of the instance the function is called from. func (c *imdsClient) role(ctx context.Context) (role.Role, error) { if c.timeForUpdate() || len(c.cache.Compute.Tags) == 0 { diff --git a/internal/cloud/azure/interface.go b/internal/cloud/azure/interface.go index 415261915..7e0b9228e 100644 --- a/internal/cloud/azure/interface.go +++ b/internal/cloud/azure/interface.go @@ -21,6 +21,7 @@ type imdsAPI interface { resourceGroup(ctx context.Context) (string, error) subscriptionID(ctx context.Context) (string, error) uid(ctx context.Context) (string, error) + initSecretHash(ctx context.Context) (string, error) } type virtualNetworksAPI interface { diff --git a/internal/cloud/cloud.go b/internal/cloud/cloud.go index c1b608ec2..1b2c172fd 100644 --- a/internal/cloud/cloud.go +++ b/internal/cloud/cloud.go @@ -11,4 +11,6 @@ const ( TagRole = "constellation-role" // TagUID is the tag/label key used to identify the UID of a cluster. TagUID = "constellation-uid" + // TagInitSecretHash is the tag/label key used to identify the hash of the init secret. + TagInitSecretHash = "constellation-init-secret-hash" ) diff --git a/internal/cloud/gcp/cloud.go b/internal/cloud/gcp/cloud.go index a28fe9990..98f66d84e 100644 --- a/internal/cloud/gcp/cloud.go +++ b/internal/cloud/gcp/cloud.go @@ -181,6 +181,19 @@ func (c *Cloud) UID(ctx context.Context) (string, error) { return c.uid(ctx, project, zone, instanceName) } +// InitSecretHash retrieves the InitSecretHash of the current instance. +func (c *Cloud) InitSecretHash(ctx context.Context) ([]byte, error) { + project, zone, instanceName, err := c.retrieveInstanceInfo() + if err != nil { + return nil, err + } + initSecretHash, err := c.initSecretHash(ctx, project, zone, instanceName) + if err != nil { + return nil, fmt.Errorf("retrieving init secret hash: %w", err) + } + return []byte(initSecretHash), nil +} + // getInstance retrieves an instance using its project, zone and name, and parses it to metadata.InstanceMetadata. func (c *Cloud) getInstance(ctx context.Context, project, zone, instanceName string) (metadata.InstanceMetadata, error) { gcpInstance, err := c.instanceAPI.Get(ctx, &computepb.GetInstanceRequest{ @@ -205,7 +218,9 @@ func (c *Cloud) getInstance(ctx context.Context, project, zone, instanceName str if err != nil { return metadata.InstanceMetadata{}, fmt.Errorf("converting instance: %w", err) } + instance.SecondaryIPRange = subnetCIDR + return instance, nil } @@ -268,7 +283,39 @@ func (c *Cloud) uid(ctx context.Context, project, zone, instanceName string) (st if instance == nil || instance.Labels == nil { return "", errors.New("retrieving compute instance: received instance with invalid labels") } - return instance.Labels[cloud.TagUID], nil + uid, ok := instance.Labels[cloud.TagUID] + if !ok { + return "", errors.New("retrieving compute instance: received instance with no UID label") + } + return uid, nil +} + +// initSecretHash retrieves the init secret hash of the instance identified by project, zone and instanceName. +// The init secret hash is retrieved from the instance's labels. +func (c *Cloud) initSecretHash(ctx context.Context, project, zone, instanceName string) (string, error) { + instance, err := c.instanceAPI.Get(ctx, &computepb.GetInstanceRequest{ + Project: project, + Zone: zone, + Instance: instanceName, + }) + if err != nil { + return "", fmt.Errorf("retrieving compute instance: %w", err) + } + if instance == nil || instance.Metadata == nil { + return "", errors.New("retrieving compute instance: received instance with invalid metadata") + } + if len(instance.Metadata.Items) == 0 { + return "", errors.New("retrieving compute instance: received instance with empty metadata") + } + for _, item := range instance.Metadata.Items { + if item == nil || item.Key == nil || item.Value == nil { + return "", errors.New("retrieving compute instance: received instance with invalid metadata item") + } + if *item.Key == cloud.TagInitSecretHash { + return *item.Value, nil + } + } + return "", errors.New("retrieving compute instance: received instance with no init secret hash label") } // convertToInstanceMetadata converts a *computepb.Instance to a metadata.InstanceMetadata. diff --git a/internal/cloud/gcp/cloud_test.go b/internal/cloud/gcp/cloud_test.go index 6dad15e8f..bab5dc686 100644 --- a/internal/cloud/gcp/cloud_test.go +++ b/internal/cloud/gcp/cloud_test.go @@ -36,8 +36,17 @@ func TestGetInstance(t *testing.T) { Name: proto.String("someInstance"), Zone: proto.String("someZone-west3-b"), Labels: map[string]string{ - cloud.TagUID: "1234", - cloud.TagRole: role.ControlPlane.String(), + cloud.TagUID: "1234", + cloud.TagRole: role.ControlPlane.String(), + cloud.TagInitSecretHash: "initSecretHash", + }, + Metadata: &computepb.Metadata{ + Items: []*computepb.Items{ + { + Key: proto.String(cloud.TagInitSecretHash), + Value: proto.String("initSecretHash"), + }, + }, }, NetworkInterfaces: []*computepb.NetworkInterface{ { @@ -748,6 +757,110 @@ func TestUID(t *testing.T) { } } +func TestInitSecretHash(t *testing.T) { + someErr := errors.New("failed") + + testCases := map[string]struct { + imds stubIMDS + instanceAPI stubInstanceAPI + wantInitSecretHash string + wantErr bool + }{ + "success": { + imds: stubIMDS{ + projectID: "someProject", + zone: "someZone-west3-b", + instanceName: "someInstance", + }, + instanceAPI: stubInstanceAPI{ + instance: &computepb.Instance{ + Name: proto.String("someInstance"), + Zone: proto.String("someZone-west3-b"), + Labels: map[string]string{ + cloud.TagRole: role.ControlPlane.String(), + }, + Metadata: &computepb.Metadata{ + Items: []*computepb.Items{ + { + Key: proto.String(cloud.TagInitSecretHash), + Value: proto.String("initSecretHash"), + }, + }, + }, + }, + }, + wantInitSecretHash: "initSecretHash", + }, + "imds error": { + imds: stubIMDS{ + projectIDErr: someErr, + zone: "someZone-west3-b", + instanceName: "someInstance", + }, + instanceAPI: stubInstanceAPI{ + instance: &computepb.Instance{ + Name: proto.String("someInstance"), + Zone: proto.String("someZone-west3-b"), + Labels: map[string]string{ + cloud.TagInitSecretHash: "initSecretHash", + cloud.TagRole: role.ControlPlane.String(), + }, + Metadata: &computepb.Metadata{ + Items: []*computepb.Items{ + { + Key: proto.String(cloud.TagInitSecretHash), + Value: proto.String("initSecretHash"), + }, + }, + }, + }, + }, + wantErr: true, + }, + "instance error": { + imds: stubIMDS{ + projectID: "someProject", + zone: "someZone-west3-b", + instanceName: "someInstance", + }, + instanceAPI: stubInstanceAPI{ + instanceErr: someErr, + }, + wantErr: true, + }, + "invalid instance": { + imds: stubIMDS{ + projectID: "someProject", + zone: "someZone-west3-b", + instanceName: "someInstance", + }, + instanceAPI: stubInstanceAPI{ + instance: nil, + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + cloud := &Cloud{ + imds: &tc.imds, + instanceAPI: &tc.instanceAPI, + } + + initSecretHash, err := cloud.InitSecretHash(context.Background()) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Equal([]byte(tc.wantInitSecretHash), initSecretHash) + }) + } +} + type stubForwardingRulesAPI struct { iterator forwardingRuleIterator } diff --git a/internal/cloud/qemu/cloud.go b/internal/cloud/qemu/cloud.go index 37e1c7d46..55d707653 100644 --- a/internal/cloud/qemu/cloud.go +++ b/internal/cloud/qemu/cloud.go @@ -9,6 +9,7 @@ package qemu import ( "context" "encoding/json" + "fmt" "io" "net/http" "net/url" @@ -62,6 +63,15 @@ func (c *Cloud) GetLoadBalancerEndpoint(ctx context.Context) (string, error) { return endpoint, err } +// InitSecretHash returns the hash of the init secret. +func (c *Cloud) InitSecretHash(ctx context.Context) ([]byte, error) { + initSecretHash, err := c.retrieveMetadata(ctx, "/initsecrethash") + if err != nil { + return nil, fmt.Errorf("could not retrieve init secret hash: %w", err) + } + return initSecretHash, nil +} + // UID returns the UID of the constellation. func (c *Cloud) UID(ctx context.Context) (string, error) { // We expect only one constellation to be deployed in the same QEMU / libvirt environment.