initserver: add client verification

This commit is contained in:
Leonard Cohnen 2022-11-26 19:44:34 +01:00 committed by 3u13r
parent bffa5c580c
commit 3b6bc3b28f
39 changed files with 704 additions and 175 deletions

View File

@ -21,11 +21,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- Environment variable `CONSTELL_AZURE_CLIENT_SECRET_VALUE` as an alternative way to provide the configuration value `provider.azure.clientSecretValue`. - 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 - Automatic CSI driver deployment for Azure and GCP during Constellation init
- Improve reproducibility by pinning the Kubernetes components. - Improve reproducibility by pinning the Kubernetes components.
- Client verification during `constellation init`
### Changed ### Changed
<!-- For changes in existing functionality. --> <!-- For changes in existing functionality. -->
@ -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. - `constellation create` on GCP now always uses the local default credentials.
## [2.2.2] - 2022-11-17 ## [2.2.2] - 2022-11-17
### Fixed ### Fixed
@ -69,6 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Security ### Security
Vulnerabilities in `kube-apiserver` fixed by upgrading to v1.23.14, v1.24.8 and v1.25.4: 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-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) - [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 ### Fixed
### Security ### Security
Vulnerability inside the Go standard library fixed by updating to Go 1.19.2: 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-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-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)) - [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))

View File

@ -56,7 +56,10 @@ func run(issuerWrapper initserver.IssuerWrapper, tpm vtpm.TPMOpenFunc, fileHandl
} }
nodeLock := nodelock.New(tpm) 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{}) dialer := dialer.New(issuerWrapper, nil, &net.Dialer{})
joinClient := joinclient.New(nodeLock, dialer, kube, metadata, log) joinClient := joinclient.New(nodeLock, dialer, kube, metadata, log)
@ -92,5 +95,6 @@ type clusterInitJoiner interface {
type metadataAPI interface { type metadataAPI interface {
joinclient.MetadataAPI joinclient.MetadataAPI
initserver.MetadataAPI
GetLoadBalancerEndpoint(ctx context.Context) (string, error) GetLoadBalancerEndpoint(ctx context.Context) (string, error)
} }

View File

@ -56,3 +56,7 @@ func (f *providerMetadataFake) Self(ctx context.Context) (metadata.InstanceMetad
func (f *providerMetadataFake) GetLoadBalancerEndpoint(ctx context.Context) (string, error) { func (f *providerMetadataFake) GetLoadBalancerEndpoint(ctx context.Context) (string, error) {
return "", nil return "", nil
} }
func (f *providerMetadataFake) InitSecretHash(ctx context.Context) ([]byte, error) {
return nil, nil
}

View File

@ -40,6 +40,7 @@ type InitRequest struct {
EnforceIdkeydigest bool `protobuf:"varint,13,opt,name=enforce_idkeydigest,json=enforceIdkeydigest,proto3" json:"enforce_idkeydigest,omitempty"` 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"` 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"` 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() { func (x *InitRequest) Reset() {
@ -165,6 +166,13 @@ func (x *InitRequest) GetKubernetesComponents() []*KubernetesComponent {
return nil return nil
} }
func (x *InitRequest) GetInitSecret() []byte {
if x != nil {
return x.InitSecret
}
return nil
}
type InitResponse struct { type InitResponse struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@ -303,7 +311,7 @@ var File_init_proto protoreflect.FileDescriptor
var file_init_proto_rawDesc = []byte{ var file_init_proto_rawDesc = []byte{
0x0a, 0x0a, 0x69, 0x6e, 0x69, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x69, 0x6e, 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, 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, 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, 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, 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, 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, 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, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x69, 0x74,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x6b, 0x75, 0x62, 0x65, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x69,
0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x6b, 0x75, 0x6e, 0x69, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x22, 0x68, 0x0a, 0x0c, 0x49, 0x6e, 0x69,
0x62, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x77, 0x6e, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x6b, 0x75, 0x62,
0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x77, 0x6e, 0x65, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x6b,
0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x69, 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x77, 0x6e,
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x77, 0x6e,
0x49, 0x64, 0x22, 0x78, 0x0a, 0x13, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f,
0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x72, 0x49, 0x64, 0x22, 0x78, 0x0a, 0x13, 0x4b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65,
0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72,
0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x50, 0x61, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68,
0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x18, 0x04, 0x20, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68,
0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x32, 0x34, 0x0a, 0x03, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x50,
0x41, 0x50, 0x49, 0x12, 0x2d, 0x0a, 0x04, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x11, 0x2e, 0x69, 0x6e, 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x18, 0x04,
0x69, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x32, 0x34, 0x0a,
0x2e, 0x69, 0x6e, 0x69, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x03, 0x41, 0x50, 0x49, 0x12, 0x2d, 0x0a, 0x04, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x11, 0x2e, 0x69,
0x73, 0x65, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x6e, 0x69, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x2f, 0x65, 0x64, 0x67, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x73, 0x79, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x12, 0x2e, 0x69, 0x6e, 0x69, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x73, 0x74, 0x65, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x32, 0x2f, 0x62, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x69, 0x74, 0x70, 0x6d, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x73, 0x79, 0x73, 0x2f, 0x63, 0x6f,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 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 ( var (

View File

@ -24,6 +24,7 @@ message InitRequest {
bool enforce_idkeydigest = 13; bool enforce_idkeydigest = 13;
bool conformance_mode = 14; bool conformance_mode = 14;
repeated KubernetesComponent kubernetes_components = 15; repeated KubernetesComponent kubernetes_components = 15;
bytes init_secret = 16;
} }
message InitResponse { message InitResponse {

View File

@ -27,6 +27,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/role" "github.com/edgelesssys/constellation/v2/internal/role"
"github.com/edgelesssys/constellation/v2/internal/versions" "github.com/edgelesssys/constellation/v2/internal/versions"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/keepalive" "google.golang.org/grpc/keepalive"
@ -45,22 +46,33 @@ type Server struct {
cleaner cleaner cleaner cleaner
issuerWrapper IssuerWrapper issuerWrapper IssuerWrapper
initSecretHash []byte
log *logger.Logger log *logger.Logger
initproto.UnimplementedAPIServer initproto.UnimplementedAPIServer
} }
// New creates a new initialization server. // 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") 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{ server := &Server{
nodeLock: lock, nodeLock: lock,
disk: diskencryption.New(), disk: diskencryption.New(),
initializer: kube, initializer: kube,
fileHandler: fh, fileHandler: fh,
issuerWrapper: issuerWrapper, issuerWrapper: issuerWrapper,
log: log, log: log,
initSecretHash: initSecretHash,
} }
grpcServer := grpc.NewServer( grpcServer := grpc.NewServer(
@ -71,7 +83,7 @@ func New(lock locker, kube ClusterInitializer, issuerWrapper IssuerWrapper, fh f
initproto.RegisterAPIServer(grpcServer, server) initproto.RegisterAPIServer(grpcServer, server)
server.grpcServer = grpcServer server.grpcServer = grpcServer
return server return server, nil
} }
// Serve starts the initialization server. // 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 := s.log.With(zap.String("peer", grpclog.PeerAddrFromContext(ctx)))
log.Infof("Init called") 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 // generate values for cluster attestation
measurementSalt, clusterID, err := deriveMeasurementValues(req.MasterSecret, req.Salt) measurementSalt, clusterID, err := deriveMeasurementValues(req.MasterSecret, req.Salt)
if err != nil { if err != nil {
@ -267,3 +283,9 @@ type locker interface {
type cleaner interface { type cleaner interface {
Clean() Clean()
} }
// MetadataAPI provides information about the instances.
type MetadataAPI interface {
// InitSecretHash returns the initSecretHash of the instance.
InitSecretHash(ctx context.Context) ([]byte, error)
}

View File

@ -24,6 +24,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/goleak" "go.uber.org/goleak"
"golang.org/x/crypto/bcrypt"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -31,17 +32,45 @@ func TestMain(m *testing.M) {
} }
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
assert := assert.New(t)
fh := file.NewHandler(afero.NewMemMapFs()) fh := file.NewHandler(afero.NewMemMapFs())
server := New(newFakeLock(), &stubClusterInitializer{}, IssuerWrapper{}, fh, logger.NewTest(t))
assert.NotNil(server) testCases := map[string]struct {
assert.NotNil(server.log) metadata stubMetadata
assert.NotNil(server.nodeLock) wantErr bool
assert.NotNil(server.initializer) }{
assert.NotNil(server.grpcServer) "success": {
assert.NotNil(server.fileHandler) metadata: stubMetadata{initSecretHashVal: []byte("hash")},
assert.NotNil(server.disk) },
"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) { func TestInit(t *testing.T) {
@ -51,70 +80,91 @@ func TestInit(t *testing.T) {
require.True(t, aqcuiredLock) require.True(t, aqcuiredLock)
require.Nil(t, lockErr) require.Nil(t, lockErr)
initSecret := []byte("password")
initSecretHash, err := bcrypt.GenerateFromPassword(initSecret, bcrypt.DefaultCost)
require.NoError(t, err)
testCases := map[string]struct { testCases := map[string]struct {
nodeLock *fakeLock nodeLock *fakeLock
initializer ClusterInitializer initializer ClusterInitializer
disk encryptedDisk disk encryptedDisk
fileHandler file.Handler fileHandler file.Handler
req *initproto.InitRequest req *initproto.InitRequest
wantErr bool initSecretHash []byte
wantShutdown bool wantErr bool
wantShutdown bool
}{ }{
"successful init": { "successful init": {
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
initializer: &stubClusterInitializer{}, initializer: &stubClusterInitializer{},
disk: &stubDisk{}, disk: &stubDisk{},
fileHandler: file.NewHandler(afero.NewMemMapFs()), fileHandler: file.NewHandler(afero.NewMemMapFs()),
req: &initproto.InitRequest{}, initSecretHash: initSecretHash,
req: &initproto.InitRequest{InitSecret: initSecret},
}, },
"node locked": { "node locked": {
nodeLock: lockedLock, nodeLock: lockedLock,
initializer: &stubClusterInitializer{}, initializer: &stubClusterInitializer{},
disk: &stubDisk{}, disk: &stubDisk{},
fileHandler: file.NewHandler(afero.NewMemMapFs()), fileHandler: file.NewHandler(afero.NewMemMapFs()),
req: &initproto.InitRequest{}, req: &initproto.InitRequest{InitSecret: initSecret},
wantErr: true, initSecretHash: initSecretHash,
wantShutdown: true, wantErr: true,
wantShutdown: true,
}, },
"disk open error": { "disk open error": {
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
initializer: &stubClusterInitializer{}, initializer: &stubClusterInitializer{},
disk: &stubDisk{openErr: someErr}, disk: &stubDisk{openErr: someErr},
fileHandler: file.NewHandler(afero.NewMemMapFs()), fileHandler: file.NewHandler(afero.NewMemMapFs()),
req: &initproto.InitRequest{}, req: &initproto.InitRequest{InitSecret: initSecret},
wantErr: true, initSecretHash: initSecretHash,
wantErr: true,
}, },
"disk uuid error": { "disk uuid error": {
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
initializer: &stubClusterInitializer{}, initializer: &stubClusterInitializer{},
disk: &stubDisk{uuidErr: someErr}, disk: &stubDisk{uuidErr: someErr},
fileHandler: file.NewHandler(afero.NewMemMapFs()), fileHandler: file.NewHandler(afero.NewMemMapFs()),
req: &initproto.InitRequest{}, req: &initproto.InitRequest{InitSecret: initSecret},
wantErr: true, initSecretHash: initSecretHash,
wantErr: true,
}, },
"disk update passphrase error": { "disk update passphrase error": {
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
initializer: &stubClusterInitializer{}, initializer: &stubClusterInitializer{},
disk: &stubDisk{updatePassphraseErr: someErr}, disk: &stubDisk{updatePassphraseErr: someErr},
fileHandler: file.NewHandler(afero.NewMemMapFs()), fileHandler: file.NewHandler(afero.NewMemMapFs()),
req: &initproto.InitRequest{}, req: &initproto.InitRequest{InitSecret: initSecret},
wantErr: true, initSecretHash: initSecretHash,
wantErr: true,
}, },
"write state file error": { "write state file error": {
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
initializer: &stubClusterInitializer{}, initializer: &stubClusterInitializer{},
disk: &stubDisk{}, disk: &stubDisk{},
fileHandler: file.NewHandler(afero.NewReadOnlyFs(afero.NewMemMapFs())), fileHandler: file.NewHandler(afero.NewReadOnlyFs(afero.NewMemMapFs())),
req: &initproto.InitRequest{}, req: &initproto.InitRequest{InitSecret: initSecret},
wantErr: true, initSecretHash: initSecretHash,
wantErr: true,
}, },
"initialize cluster error": { "initialize cluster error": {
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
initializer: &stubClusterInitializer{initClusterErr: someErr}, initializer: &stubClusterInitializer{initClusterErr: someErr},
disk: &stubDisk{}, disk: &stubDisk{},
fileHandler: file.NewHandler(afero.NewMemMapFs()), fileHandler: file.NewHandler(afero.NewMemMapFs()),
req: &initproto.InitRequest{}, req: &initproto.InitRequest{InitSecret: initSecret},
wantErr: true, 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() serveStopper := newStubServeStopper()
server := &Server{ server := &Server{
nodeLock: tc.nodeLock, nodeLock: tc.nodeLock,
initializer: tc.initializer, initializer: tc.initializer,
disk: tc.disk, disk: tc.disk,
fileHandler: tc.fileHandler, fileHandler: tc.fileHandler,
log: logger.NewTest(t), log: logger.NewTest(t),
grpcServer: serveStopper, grpcServer: serveStopper,
cleaner: &fakeCleaner{serveStopper: serveStopper}, cleaner: &fakeCleaner{serveStopper: serveStopper},
initSecretHash: tc.initSecretHash,
} }
kubeconfig, err := server.Init(context.Background(), tc.req) kubeconfig, err := server.Init(context.Background(), tc.req)
@ -293,3 +344,12 @@ type fakeCleaner struct {
func (f *fakeCleaner) Clean() { func (f *fakeCleaner) Clean() {
go f.serveStopper.GracefulStop() // this is not the correct way to do this, but it's fine for testing 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
}

View File

@ -17,7 +17,7 @@ import (
type terraformClient interface { type terraformClient interface {
PrepareWorkspace(provider cloudprovider.Provider, input terraform.Variables) error 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 DestroyCluster(ctx context.Context) error
CleanUpWorkspace() error CleanUpWorkspace() error
RemoveInstaller() RemoveInstaller()

View File

@ -27,6 +27,7 @@ func TestMain(m *testing.M) {
type stubTerraformClient struct { type stubTerraformClient struct {
ip string ip string
initSecret string
cleanUpWorkspaceCalled bool cleanUpWorkspaceCalled bool
removeInstallerCalled bool removeInstallerCalled bool
destroyClusterCalled bool destroyClusterCalled bool
@ -36,8 +37,8 @@ type stubTerraformClient struct {
cleanUpWorkspaceErr error cleanUpWorkspaceErr error
} }
func (c *stubTerraformClient) CreateCluster(ctx context.Context) (string, error) { func (c *stubTerraformClient) CreateCluster(ctx context.Context) (string, string, error) {
return c.ip, c.createClusterErr return c.ip, c.initSecret, c.createClusterErr
} }
func (c *stubTerraformClient) PrepareWorkspace(provider cloudprovider.Provider, input terraform.Variables) error { func (c *stubTerraformClient) PrepareWorkspace(provider cloudprovider.Provider, input terraform.Variables) error {

View File

@ -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}) defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerTerraform{client: cl})
ip, err := cl.CreateCluster(ctx) ip, initSecret, err := cl.CreateCluster(ctx)
if err != nil { if err != nil {
return clusterid.File{}, err return clusterid.File{}, err
} }
return clusterid.File{ return clusterid.File{
CloudProvider: cloudprovider.AWS, CloudProvider: cloudprovider.AWS,
InitSecret: []byte(initSecret),
IP: ip, IP: ip,
}, nil }, 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}) defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerTerraform{client: cl})
ip, err := cl.CreateCluster(ctx) ip, initSecret, err := cl.CreateCluster(ctx)
if err != nil { if err != nil {
return clusterid.File{}, err return clusterid.File{}, err
} }
return clusterid.File{ return clusterid.File{
CloudProvider: cloudprovider.GCP, CloudProvider: cloudprovider.GCP,
InitSecret: []byte(initSecret),
IP: ip, IP: ip,
}, nil }, 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}) defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerTerraform{client: cl})
ip, err := cl.CreateCluster(ctx) ip, initSecret, err := cl.CreateCluster(ctx)
if err != nil { if err != nil {
return clusterid.File{}, err return clusterid.File{}, err
} }
@ -205,6 +207,7 @@ func (c *Creator) createAzure(ctx context.Context, cl terraformClient, config *c
return clusterid.File{ return clusterid.File{
CloudProvider: cloudprovider.Azure, CloudProvider: cloudprovider.Azure,
IP: ip, IP: ip,
InitSecret: []byte(initSecret),
}, nil }, 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 // Allow rollback of QEMU Terraform workspace from this point on
qemuRollbacker.createdWorkspace = true qemuRollbacker.createdWorkspace = true
ip, err := cl.CreateCluster(ctx) ip, initSecret, err := cl.CreateCluster(ctx)
if err != nil { if err != nil {
return clusterid.File{}, err return clusterid.File{}, err
} }
return clusterid.File{ return clusterid.File{
CloudProvider: cloudprovider.QEMU, CloudProvider: cloudprovider.QEMU,
InitSecret: []byte(initSecret),
IP: ip, IP: ip,
}, nil }, nil
} }

View File

@ -22,4 +22,6 @@ type File struct {
CloudProvider cloudprovider.Provider `json:"cloudprovider,omitempty"` CloudProvider cloudprovider.Provider `json:"cloudprovider,omitempty"`
// IP is the IP address the cluster can be reached at (often the load balancer). // IP is the IP address the cluster can be reached at (often the load balancer).
IP string `json:"ip,omitempty"` IP string `json:"ip,omitempty"`
// InitSecret is the secret the first Bootstrapper uses to verify the user.
InitSecret []byte `json:"initsecret,omitempty"`
} }

View File

@ -138,6 +138,7 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator
EnforcedPcrs: conf.EnforcedPCRs(), EnforcedPcrs: conf.EnforcedPCRs(),
EnforceIdkeydigest: conf.EnforcesIDKeyDigest(), EnforceIdkeydigest: conf.EnforcesIDKeyDigest(),
ConformanceMode: flags.conformance, ConformanceMode: flags.conformance,
InitSecret: idFile.InitSecret,
} }
resp, err := initCall(cmd.Context(), newDialer(validator), idFile.IP, req) resp, err := initCall(cmd.Context(), newDialer(validator), idFile.IP, req)
spinner.Stop() spinner.Stop()

View File

@ -74,30 +74,39 @@ func (c *Client) PrepareWorkspace(provider cloudprovider.Provider, vars Variable
} }
// CreateCluster creates a Constellation cluster using Terraform. // 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 { if err := c.tf.Init(ctx); err != nil {
return "", err return "", "", err
} }
if err := c.tf.Apply(ctx); err != nil { if err := c.tf.Apply(ctx); err != nil {
return "", err return "", "", err
} }
tfState, err := c.tf.Show(ctx) tfState, err := c.tf.Show(ctx)
if err != nil { if err != nil {
return "", err return "", "", err
} }
ipOutput, ok := tfState.Values.Outputs["ip"] ipOutput, ok := tfState.Values.Outputs["ip"]
if !ok { if !ok {
return "", errors.New("no IP output found") return "", "", errors.New("no IP output found")
} }
ip, ok := ipOutput.Value.(string) ip, ok := ipOutput.Value.(string)
if !ok { 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. // DestroyCluster destroys a Constellation cluster using Terraform.

View File

@ -19,6 +19,7 @@ provider "aws" {
locals { locals {
uid = random_id.uid.hex uid = random_id.uid.hex
name = "${var.name}-${local.uid}" name = "${var.name}-${local.uid}"
initSecretHash = random_password.initSecret.bcrypt_hash
ports_node_range = "30000-32767" ports_node_range = "30000-32767"
ports_kubernetes = "6443" ports_kubernetes = "6443"
ports_bootstrapper = "9000" ports_bootstrapper = "9000"
@ -34,6 +35,12 @@ resource "random_id" "uid" {
byte_length = 4 byte_length = 4
} }
resource "random_password" "initSecret" {
length = 32
special = true
override_special = "_%@"
}
resource "aws_vpc" "vpc" { resource "aws_vpc" "vpc" {
cidr_block = "192.168.0.0/16" cidr_block = "192.168.0.0/16"
tags = merge(local.tags, { Name = "${local.name}-vpc" }) 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] security_groups = [aws_security_group.security_group.id]
subnetwork = module.public_private_subnet.private_subnet_id subnetwork = module.public_private_subnet.private_subnet_id
iam_instance_profile = var.iam_instance_profile_control_plane 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" { module "instance_group_worker_nodes" {
@ -246,4 +261,12 @@ module "instance_group_worker_nodes" {
target_group_arns = [] target_group_arns = []
security_groups = [aws_security_group.security_group.id] security_groups = [aws_security_group.security_group.id]
iam_instance_profile = var.iam_instance_profile_worker_nodes 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 }
)
} }

View File

@ -57,26 +57,13 @@ resource "aws_autoscaling_group" "autoscaling_group" {
vpc_zone_identifier = [var.subnetwork] vpc_zone_identifier = [var.subnetwork]
target_group_arns = var.target_group_arns target_group_arns = var.target_group_arns
tag { dynamic "tag" {
key = "Name" for_each = var.tags
value = local.name content {
propagate_at_launch = true key = tag.key
} value = tag.value
tag { propagate_at_launch = true
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
} }
lifecycle { lifecycle {

View File

@ -57,3 +57,8 @@ variable "security_groups" {
type = list(string) type = list(string)
description = "List of IDs of the security groups for an instance." 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."
}

View File

@ -1,3 +1,8 @@
output "ip" { output "ip" {
value = aws_eip.lb.public_ip value = aws_eip.lb.public_ip
} }
output "initSecret" {
value = random_password.initSecret.result
sensitive = true
}

View File

@ -18,6 +18,7 @@ provider "azurerm" {
locals { locals {
uid = random_id.uid.hex uid = random_id.uid.hex
name = "${var.name}-${local.uid}" name = "${var.name}-${local.uid}"
initSecretHash = random_password.initSecret.bcrypt_hash
tags = { constellation-uid = local.uid } tags = { constellation-uid = local.uid }
ports_node_range = "30000-32767" ports_node_range = "30000-32767"
ports_kubernetes = "6443" ports_kubernetes = "6443"
@ -34,6 +35,12 @@ resource "random_id" "uid" {
byte_length = 4 byte_length = 4
} }
resource "random_password" "initSecret" {
length = 32
special = true
override_special = "_%@"
}
resource "azurerm_application_insights" "insights" { resource "azurerm_application_insights" "insights" {
name = local.name name = local.name
location = var.location location = var.location
@ -194,7 +201,7 @@ module "scale_set_control_plane" {
instance_type = var.instance_type instance_type = var.instance_type
confidential_vm = var.confidential_vm confidential_vm = var.confidential_vm
secure_boot = var.secure_boot 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 image_id = var.image_id
user_assigned_identity = var.user_assigned_identity user_assigned_identity = var.user_assigned_identity
network_security_group_id = azurerm_network_security_group.security_group.id network_security_group_id = azurerm_network_security_group.security_group.id
@ -217,7 +224,7 @@ module "scale_set_worker" {
instance_type = var.instance_type instance_type = var.instance_type
confidential_vm = var.confidential_vm confidential_vm = var.confidential_vm
secure_boot = var.secure_boot 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 image_id = var.image_id
user_assigned_identity = var.user_assigned_identity user_assigned_identity = var.user_assigned_identity
network_security_group_id = azurerm_network_security_group.security_group.id network_security_group_id = azurerm_network_security_group.security_group.id

View File

@ -1,3 +1,8 @@
output "ip" { output "ip" {
value = azurerm_public_ip.loadbalancer_ip.ip_address value = azurerm_public_ip.loadbalancer_ip.ip_address
} }
output "initSecret" {
value = random_password.initSecret.result
sensitive = true
}

View File

@ -20,6 +20,7 @@ provider "google" {
locals { locals {
uid = random_id.uid.hex uid = random_id.uid.hex
name = "${var.name}-${local.uid}" name = "${var.name}-${local.uid}"
initSecretHash = random_password.initSecret.bcrypt_hash
labels = { constellation-uid = local.uid } labels = { constellation-uid = local.uid }
ports_node_range = "30000-32767" ports_node_range = "30000-32767"
ports_kubernetes = "6443" ports_kubernetes = "6443"
@ -37,6 +38,12 @@ resource "random_id" "uid" {
byte_length = 4 byte_length = 4
} }
resource "random_password" "initSecret" {
length = 32
special = true
override_special = "_%@"
}
resource "google_compute_network" "vpc_network" { resource "google_compute_network" "vpc_network" {
name = local.name name = local.name
description = "Constellation VPC network" description = "Constellation VPC network"
@ -136,24 +143,26 @@ module "instance_group_control_plane" {
{ name = "recovery", port = local.ports_recovery }, { name = "recovery", port = local.ports_recovery },
var.debug ? [{ name = "debugd", port = local.ports_debugd }] : [], var.debug ? [{ name = "debugd", port = local.ports_debugd }] : [],
]) ])
labels = local.labels labels = local.labels
init_secret_hash = local.initSecretHash
} }
module "instance_group_worker" { module "instance_group_worker" {
source = "./modules/instance_group" source = "./modules/instance_group"
name = local.name name = local.name
role = "Worker" role = "Worker"
uid = local.uid uid = local.uid
instance_type = var.instance_type instance_type = var.instance_type
instance_count = var.worker_count instance_count = var.worker_count
image_id = var.image_id image_id = var.image_id
disk_size = var.state_disk_size disk_size = var.state_disk_size
disk_type = var.state_disk_type disk_type = var.state_disk_type
network = google_compute_network.vpc_network.id network = google_compute_network.vpc_network.id
subnetwork = google_compute_subnetwork.vpc_subnetwork.id subnetwork = google_compute_subnetwork.vpc_subnetwork.id
kube_env = local.kube_env kube_env = local.kube_env
debug = var.debug debug = var.debug
labels = local.labels labels = local.labels
init_secret_hash = local.initSecretHash
} }
resource "google_compute_global_address" "loadbalancer_ip" { resource "google_compute_global_address" "loadbalancer_ip" {

View File

@ -15,7 +15,7 @@ locals {
resource "google_compute_instance_template" "template" { resource "google_compute_instance_template" "template" {
name = local.name name = local.name
machine_type = var.instance_type 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 }) labels = merge(var.labels, { constellation-role = local.role_dashed })
confidential_instance_config { confidential_instance_config {
@ -41,8 +41,9 @@ resource "google_compute_instance_template" "template" {
} }
metadata = { metadata = {
kube-env = var.kube_env kube-env = var.kube_env
serial-port-enable = var.debug ? "TRUE" : "FALSE" constellation-init-secret-hash = var.init_secret_hash
serial-port-enable = var.debug ? "TRUE" : "FALSE"
} }
network_interface { network_interface {

View File

@ -59,6 +59,11 @@ variable "kube_env" {
description = "Kubernetes env." description = "Kubernetes env."
} }
variable "init_secret_hash" {
type = string
description = "Hash of the init secret."
}
variable "named_ports" { variable "named_ports" {
type = list(object({ name = string, port = number })) type = list(object({ name = string, port = number }))
default = [] default = []

View File

@ -1,3 +1,8 @@
output "ip" { output "ip" {
value = google_compute_global_address.loadbalancer_ip.address value = google_compute_global_address.loadbalancer_ip.address
} }
output "initSecret" {
value = random_password.initSecret.result
sensitive = true
}

View File

@ -24,6 +24,11 @@ provider "docker" {
} }
} }
resource "random_password" "initSecret" {
length = 32
special = true
override_special = "_%@"
}
resource "docker_image" "qemu_metadata" { resource "docker_image" "qemu_metadata" {
name = var.metadata_api_image name = var.metadata_api_image
keep_locally = true keep_locally = true
@ -39,6 +44,8 @@ resource "docker_container" "qemu_metadata" {
"${var.name}-network", "${var.name}-network",
"--libvirt-uri", "--libvirt-uri",
"${var.metadata_libvirt_uri}", "${var.metadata_libvirt_uri}",
"--initsecrethash",
"${random_password.initSecret.bcrypt_hash}",
] ]
mounts { mounts {
source = abspath(var.libvirt_socket_path) source = abspath(var.libvirt_socket_path)
@ -47,6 +54,8 @@ resource "docker_container" "qemu_metadata" {
} }
} }
module "control_plane" { module "control_plane" {
source = "./modules/instance_group" source = "./modules/instance_group"
role = "control-plane" role = "control-plane"

View File

@ -1,3 +1,8 @@
output "ip" { output "ip" {
value = module.control_plane.instance_ips[0] value = module.control_plane.instance_ips[0]
} }
output "initSecret" {
value = random_password.initSecret.result
sensitive = true
}

View File

@ -109,6 +109,9 @@ func TestCreateCluster(t *testing.T) {
"ip": { "ip": {
Value: "192.0.2.100", 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)) require.NoError(c.PrepareWorkspace(tc.provider, tc.vars))
ip, err := c.CreateCluster(context.Background()) ip, initSecret, err := c.CreateCluster(context.Background())
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
@ -210,6 +213,7 @@ func TestCreateCluster(t *testing.T) {
} }
assert.NoError(err) assert.NoError(err)
assert.Equal("192.0.2.100", ip) assert.Equal("192.0.2.100", ip)
assert.Equal("initSecret", initSecret)
}) })
} }
} }

View File

@ -21,6 +21,7 @@ func main() {
bindPort := flag.String("port", "8080", "Port to bind to") bindPort := flag.String("port", "8080", "Port to bind to")
targetNetwork := flag.String("network", "constellation-network", "Name of the network in QEMU to use") 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") libvirtURI := flag.String("libvirt-uri", "qemu:///system", "URI of the libvirt connection")
initSecretHash := flag.String("initsecrethash", "", "brcypt hash of the init secret")
flag.Parse() flag.Parse()
log := logger.New(logger.JSONLog, zapcore.InfoLevel) log := logger.New(logger.JSONLog, zapcore.InfoLevel)
@ -31,7 +32,7 @@ func main() {
} }
defer conn.Close() 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 { if err := serv.ListenAndServe(*bindPort); err != nil {
log.With(zap.Error(err)).Fatalf("Failed to serve") log.With(zap.Error(err)).Fatalf("Failed to serve")
} }

View File

@ -24,17 +24,19 @@ import (
// Server that provides QEMU metadata. // Server that provides QEMU metadata.
type Server struct { type Server struct {
log *logger.Logger log *logger.Logger
virt virConnect virt virConnect
network string network string
initSecretHashVal []byte
} }
// New creates a new Server. // 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{ return &Server{
log: log, log: log,
virt: conn, virt: conn,
network: network, 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("/log", http.HandlerFunc(s.postLog))
mux.Handle("/pcrs", http.HandlerFunc(s.exportPCRs)) mux.Handle("/pcrs", http.HandlerFunc(s.exportPCRs))
mux.Handle("/endpoint", http.HandlerFunc(s.getEndpoint)) mux.Handle("/endpoint", http.HandlerFunc(s.getEndpoint))
mux.Handle("/initsecrethash", http.HandlerFunc(s.initSecretHash))
server := http.Server{ server := http.Server{
Handler: mux, Handler: mux,
@ -115,6 +118,26 @@ func (s *Server) listPeers(w http.ResponseWriter, r *http.Request) {
log.Infof("Request successful") 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. // getEndpoint returns the IP address of the first control-plane instance.
// This allows us to fake a load balancer for QEMU instances. // This allows us to fake a load balancer for QEMU instances.
func (s *Server) getEndpoint(w http.ResponseWriter, r *http.Request) { func (s *Server) getEndpoint(w http.ResponseWriter, r *http.Request) {

View File

@ -72,7 +72,7 @@ func TestListAll(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(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() res, err := server.listAll()
@ -149,7 +149,7 @@ func TestListSelf(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.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) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://192.0.0.1/self", nil)
require.NoError(err) require.NoError(err)
@ -211,7 +211,7 @@ func TestListPeers(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.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) req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://192.0.0.1/peers", nil)
require.NoError(err) require.NoError(err)
@ -266,7 +266,7 @@ func TestPostLog(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.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) req, err := http.NewRequestWithContext(context.Background(), tc.method, "http://192.0.0.1/logs", tc.message)
require.NoError(err) require.NoError(err)
@ -346,7 +346,7 @@ func TestExportPCRs(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.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)) req, err := http.NewRequestWithContext(context.Background(), tc.method, "http://192.0.0.1/pcrs", strings.NewReader(tc.message))
require.NoError(err) 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 { func mustMarshal(t *testing.T, v any) string {
t.Helper() t.Helper()
b, err := json.Marshal(v) b, err := json.Marshal(v)

View File

@ -109,6 +109,15 @@ func (c *Cloud) UID(ctx context.Context) (string, error) {
return readInstanceTag(ctx, c.imds, cloud.TagUID) 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. // GetLoadBalancerEndpoint returns the endpoint of the load balancer.
func (c *Cloud) GetLoadBalancerEndpoint(ctx context.Context) (string, error) { func (c *Cloud) GetLoadBalancerEndpoint(ctx context.Context) (string, error) {
uid, err := readInstanceTag(ctx, c.imds, cloud.TagUID) uid, err := readInstanceTag(ctx, c.imds, cloud.TagUID)

View File

@ -67,7 +67,8 @@ func TestSelf(t *testing.T) {
}, },
}, },
tags: map[string]string{ tags: map[string]string{
cloud.TagRole: "worker", cloud.TagRole: "worker",
cloud.TagInitSecretHash: "initSecretHash",
}, },
}, },
wantSelf: metadata.InstanceMetadata{ wantSelf: metadata.InstanceMetadata{

View File

@ -250,6 +250,15 @@ func (c *Cloud) UID(ctx context.Context) (string, error) {
return uid, nil 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. // getLoadBalancer retrieves a load balancer from cloud provider metadata.
func (c *Cloud) getLoadBalancer(ctx context.Context, resourceGroup, uid string) (*armnetwork.LoadBalancer, error) { func (c *Cloud) getLoadBalancer(ctx context.Context, resourceGroup, uid string) (*armnetwork.LoadBalancer, error) {
pager := c.loadBalancerAPI.NewListPager(resourceGroup, nil) pager := c.loadBalancerAPI.NewListPager(resourceGroup, nil)
@ -283,8 +292,12 @@ func (c *Cloud) getInstance(ctx context.Context, providerID string) (metadata.In
if err != nil { if err != nil {
return metadata.InstanceMetadata{}, fmt.Errorf("retrieving VM network interfaces: %w", err) 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. // getNetworkSecurityGroupName returns the security group name of the resource group.

View File

@ -360,6 +360,7 @@ func TestGetInstance(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
scaleSetsVMAPI *stubVirtualMachineScaleSetVMsAPI scaleSetsVMAPI *stubVirtualMachineScaleSetVMsAPI
networkInterfacesAPI *stubNetworkInterfacesAPI networkInterfacesAPI *stubNetworkInterfacesAPI
IMDSAPI *stubIMDSAPI
providerID string providerID string
wantInstance metadata.InstanceMetadata wantInstance metadata.InstanceMetadata
wantErr bool wantErr bool
@ -368,6 +369,7 @@ func TestGetInstance(t *testing.T) {
scaleSetsVMAPI: successVMAPI, scaleSetsVMAPI: successVMAPI,
networkInterfacesAPI: successNetworkAPI, networkInterfacesAPI: successNetworkAPI,
providerID: sampleProviderID, providerID: sampleProviderID,
IMDSAPI: &stubIMDSAPI{},
wantInstance: metadata.InstanceMetadata{ wantInstance: metadata.InstanceMetadata{
Name: "scale-set-name-instance-id", Name: "scale-set-name-instance-id",
ProviderID: sampleProviderID, ProviderID: sampleProviderID,
@ -397,6 +399,7 @@ func TestGetInstance(t *testing.T) {
}, },
}, },
}, },
IMDSAPI: &stubIMDSAPI{},
networkInterfacesAPI: successNetworkAPI, networkInterfacesAPI: successNetworkAPI,
providerID: sampleProviderID, providerID: sampleProviderID,
wantInstance: metadata.InstanceMetadata{ wantInstance: metadata.InstanceMetadata{
@ -408,12 +411,14 @@ func TestGetInstance(t *testing.T) {
}, },
"invalid provider ID": { "invalid provider ID": {
scaleSetsVMAPI: successVMAPI, scaleSetsVMAPI: successVMAPI,
IMDSAPI: &stubIMDSAPI{},
networkInterfacesAPI: successNetworkAPI, networkInterfacesAPI: successNetworkAPI,
providerID: "invalid", providerID: "invalid",
wantErr: true, wantErr: true,
}, },
"vm API error": { "vm API error": {
scaleSetsVMAPI: &stubVirtualMachineScaleSetVMsAPI{getErr: someErr}, scaleSetsVMAPI: &stubVirtualMachineScaleSetVMsAPI{getErr: someErr},
IMDSAPI: &stubIMDSAPI{},
networkInterfacesAPI: successNetworkAPI, networkInterfacesAPI: successNetworkAPI,
providerID: sampleProviderID, providerID: sampleProviderID,
wantErr: true, wantErr: true,
@ -421,6 +426,7 @@ func TestGetInstance(t *testing.T) {
"network API error": { "network API error": {
scaleSetsVMAPI: successVMAPI, scaleSetsVMAPI: successVMAPI,
networkInterfacesAPI: &stubNetworkInterfacesAPI{getErr: someErr}, networkInterfacesAPI: &stubNetworkInterfacesAPI{getErr: someErr},
IMDSAPI: &stubIMDSAPI{},
providerID: sampleProviderID, providerID: sampleProviderID,
wantErr: true, wantErr: true,
}, },
@ -431,6 +437,7 @@ func TestGetInstance(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
metadata := Cloud{ metadata := Cloud{
imds: tc.IMDSAPI,
scaleSetsVMAPI: tc.scaleSetsVMAPI, scaleSetsVMAPI: tc.scaleSetsVMAPI,
netIfacAPI: tc.networkInterfacesAPI, 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) { func TestList(t *testing.T) {
someErr := errors.New("failed") someErr := errors.New("failed")
networkIfaceResponse := &stubNetworkInterfacesAPI{ networkIfaceResponse := &stubNetworkInterfacesAPI{
@ -978,6 +1021,8 @@ type stubIMDSAPI struct {
uidVal string uidVal string
nameErr error nameErr error
nameVal string nameVal string
initSecretHashVal string
initSecretHashErr error
} }
func (a *stubIMDSAPI) providerID(ctx context.Context) (string, 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 return a.nameVal, a.nameErr
} }
func (a *stubIMDSAPI) initSecretHash(ctx context.Context) (string, error) {
return a.initSecretHashVal, a.initSecretHashErr
}
type stubVirtualMachineScaleSetVMPager struct { type stubVirtualMachineScaleSetVMPager struct {
list []armcomputev2.VirtualMachineScaleSetVM list []armcomputev2.VirtualMachineScaleSetVM
fetchErr error fetchErr error

View File

@ -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) 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. // role returns the role of the instance the function is called from.
func (c *imdsClient) role(ctx context.Context) (role.Role, error) { func (c *imdsClient) role(ctx context.Context) (role.Role, error) {
if c.timeForUpdate() || len(c.cache.Compute.Tags) == 0 { if c.timeForUpdate() || len(c.cache.Compute.Tags) == 0 {

View File

@ -21,6 +21,7 @@ type imdsAPI interface {
resourceGroup(ctx context.Context) (string, error) resourceGroup(ctx context.Context) (string, error)
subscriptionID(ctx context.Context) (string, error) subscriptionID(ctx context.Context) (string, error)
uid(ctx context.Context) (string, error) uid(ctx context.Context) (string, error)
initSecretHash(ctx context.Context) (string, error)
} }
type virtualNetworksAPI interface { type virtualNetworksAPI interface {

View File

@ -11,4 +11,6 @@ const (
TagRole = "constellation-role" TagRole = "constellation-role"
// TagUID is the tag/label key used to identify the UID of a cluster. // TagUID is the tag/label key used to identify the UID of a cluster.
TagUID = "constellation-uid" TagUID = "constellation-uid"
// TagInitSecretHash is the tag/label key used to identify the hash of the init secret.
TagInitSecretHash = "constellation-init-secret-hash"
) )

View File

@ -181,6 +181,19 @@ func (c *Cloud) UID(ctx context.Context) (string, error) {
return c.uid(ctx, project, zone, instanceName) 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. // 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) { func (c *Cloud) getInstance(ctx context.Context, project, zone, instanceName string) (metadata.InstanceMetadata, error) {
gcpInstance, err := c.instanceAPI.Get(ctx, &computepb.GetInstanceRequest{ 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 { if err != nil {
return metadata.InstanceMetadata{}, fmt.Errorf("converting instance: %w", err) return metadata.InstanceMetadata{}, fmt.Errorf("converting instance: %w", err)
} }
instance.SecondaryIPRange = subnetCIDR instance.SecondaryIPRange = subnetCIDR
return instance, nil 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 { if instance == nil || instance.Labels == nil {
return "", errors.New("retrieving compute instance: received instance with invalid labels") 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. // convertToInstanceMetadata converts a *computepb.Instance to a metadata.InstanceMetadata.

View File

@ -36,8 +36,17 @@ func TestGetInstance(t *testing.T) {
Name: proto.String("someInstance"), Name: proto.String("someInstance"),
Zone: proto.String("someZone-west3-b"), Zone: proto.String("someZone-west3-b"),
Labels: map[string]string{ Labels: map[string]string{
cloud.TagUID: "1234", cloud.TagUID: "1234",
cloud.TagRole: role.ControlPlane.String(), 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{ 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 { type stubForwardingRulesAPI struct {
iterator forwardingRuleIterator iterator forwardingRuleIterator
} }

View File

@ -9,6 +9,7 @@ package qemu
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
@ -62,6 +63,15 @@ func (c *Cloud) GetLoadBalancerEndpoint(ctx context.Context) (string, error) {
return endpoint, err 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. // UID returns the UID of the constellation.
func (c *Cloud) UID(ctx context.Context) (string, error) { func (c *Cloud) UID(ctx context.Context) (string, error) {
// We expect only one constellation to be deployed in the same QEMU / libvirt environment. // We expect only one constellation to be deployed in the same QEMU / libvirt environment.