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]
### 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
<!-- 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.
## [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))

View File

@ -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)
}

View File

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

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"`
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 (

View File

@ -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 {

View File

@ -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)
}

View File

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

View File

@ -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()

View File

@ -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 {

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

View File

@ -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"`
}

View File

@ -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()

View File

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

View File

@ -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 }
)
}

View File

@ -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 {

View File

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

View File

@ -1,3 +1,8 @@
output "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 {
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

View File

@ -1,3 +1,8 @@
output "ip" {
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 {
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" {

View File

@ -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 {

View File

@ -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 = []

View File

@ -1,3 +1,8 @@
output "ip" {
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" {
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"

View File

@ -1,3 +1,8 @@
output "ip" {
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": {
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)
})
}
}

View File

@ -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")
}

View File

@ -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) {

View File

@ -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)

View File

@ -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)

View File

@ -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{

View File

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

View File

@ -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

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)
}
// 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 {

View File

@ -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 {

View File

@ -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"
)

View File

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

View File

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

View File

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