From 9f296aab01de5fe340923cc657bcf6f5ffb34988 Mon Sep 17 00:00:00 2001 From: Denis Korzunov Date: Mon, 16 Nov 2020 15:36:50 +1100 Subject: [PATCH] UPSTREAM: : openshift: add SecurityProfile.EncryptionAtHost parameter to enable host-based VM encryption --- .../azuremachineproviderconfig_types.go | 4 + pkg/apis/azureprovider/v1beta1/types.go | 9 ++ .../v1beta1/zz_generated.deepcopy.go | 26 ++++ .../azure/actuators/machine/reconciler.go | 23 ++-- .../virtualmachines/virtualmachines.go | 112 +++++++++++------ .../virtualmachines/virtualmachines_test.go | 114 ++++++++++++++++++ 6 files changed, 237 insertions(+), 51 deletions(-) diff --git a/pkg/apis/azureprovider/v1beta1/azuremachineproviderconfig_types.go b/pkg/apis/azureprovider/v1beta1/azuremachineproviderconfig_types.go index 09694e3f2ab..576adb6aef1 100644 --- a/pkg/apis/azureprovider/v1beta1/azuremachineproviderconfig_types.go +++ b/pkg/apis/azureprovider/v1beta1/azuremachineproviderconfig_types.go @@ -91,6 +91,10 @@ type AzureMachineProviderSpec struct { // SpotVMOptions allows the ability to specify the Machine should use a Spot VM SpotVMOptions *SpotVMOptions `json:"spotVMOptions,omitempty"` + + // SecurityProfile specifies the Security profile settings for a virtual machine. + // +optional + SecurityProfile *SecurityProfile `json:"securityProfile,omitempty"` } // SpotVMOptions defines the options relevant to running the Machine on Spot VMs diff --git a/pkg/apis/azureprovider/v1beta1/types.go b/pkg/apis/azureprovider/v1beta1/types.go index c8716bfa1e3..4ed20f3f296 100644 --- a/pkg/apis/azureprovider/v1beta1/types.go +++ b/pkg/apis/azureprovider/v1beta1/types.go @@ -414,3 +414,12 @@ type ManagedDiskParameters struct { type DiskEncryptionSetParameters struct { ID string `json:"id,omitempty"` } + +// SecurityProfile specifies the Security profile settings for a +// virtual machine or virtual machine scale set. +type SecurityProfile struct { + // This field indicates whether Host Encryption should be enabled + // or disabled for a virtual machine or virtual machine scale + // set. Default is disabled. + EncryptionAtHost *bool `json:"encryptionAtHost,omitempty"` +} diff --git a/pkg/apis/azureprovider/v1beta1/zz_generated.deepcopy.go b/pkg/apis/azureprovider/v1beta1/zz_generated.deepcopy.go index 3c1e85d9901..297fc48c6f4 100644 --- a/pkg/apis/azureprovider/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/azureprovider/v1beta1/zz_generated.deepcopy.go @@ -87,6 +87,11 @@ func (in *AzureMachineProviderSpec) DeepCopyInto(out *AzureMachineProviderSpec) *out = new(SpotVMOptions) (*in).DeepCopyInto(*out) } + if in.SecurityProfile != nil { + in, out := &in.SecurityProfile, &out.SecurityProfile + *out = new(SecurityProfile) + (*in).DeepCopyInto(*out) + } return } @@ -462,6 +467,27 @@ func (in *SecurityGroup) DeepCopy() *SecurityGroup { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityProfile) DeepCopyInto(out *SecurityProfile) { + *out = *in + if in.EncryptionAtHost != nil { + in, out := &in.EncryptionAtHost, &out.EncryptionAtHost + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityProfile. +func (in *SecurityProfile) DeepCopy() *SecurityProfile { + if in == nil { + return nil + } + out := new(SecurityProfile) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SpotVMOptions) DeepCopyInto(out *SpotVMOptions) { *out = *in diff --git a/pkg/cloud/azure/actuators/machine/reconciler.go b/pkg/cloud/azure/actuators/machine/reconciler.go index 852c5369409..6b116de535c 100644 --- a/pkg/cloud/azure/actuators/machine/reconciler.go +++ b/pkg/cloud/azure/actuators/machine/reconciler.go @@ -578,17 +578,18 @@ func (s *Reconciler) createVirtualMachine(ctx context.Context, nicName string) e } vmSpec = &virtualmachines.Spec{ - Name: s.scope.Machine.Name, - NICName: nicName, - SSHKeyData: string(decoded), - Size: s.scope.MachineConfig.VMSize, - OSDisk: s.scope.MachineConfig.OSDisk, - Image: s.scope.MachineConfig.Image, - Zone: zone, - Tags: s.scope.MachineConfig.Tags, - Priority: priority, - EvictionPolicy: evictionPolicy, - BillingProfile: billingProfile, + Name: s.scope.Machine.Name, + NICName: nicName, + SSHKeyData: string(decoded), + Size: s.scope.MachineConfig.VMSize, + OSDisk: s.scope.MachineConfig.OSDisk, + Image: s.scope.MachineConfig.Image, + Zone: zone, + Tags: s.scope.MachineConfig.Tags, + Priority: priority, + EvictionPolicy: evictionPolicy, + BillingProfile: billingProfile, + SecurityProfile: s.scope.MachineConfig.SecurityProfile, } if s.scope.MachineConfig.ManagedIdentity != "" { diff --git a/pkg/cloud/azure/services/virtualmachines/virtualmachines.go b/pkg/cloud/azure/services/virtualmachines/virtualmachines.go index 6317e3b594a..e3a4e8eae65 100644 --- a/pkg/cloud/azure/services/virtualmachines/virtualmachines.go +++ b/pkg/cloud/azure/services/virtualmachines/virtualmachines.go @@ -77,6 +77,7 @@ type Spec struct { Priority compute.VirtualMachinePriorityTypes EvictionPolicy compute.VirtualMachineEvictionPolicyTypes BillingProfile *compute.BillingProfile + SecurityProfile *v1beta1.SecurityProfile } // Get provides information about a virtual network. @@ -114,35 +115,49 @@ func (s *Service) CreateOrUpdate(ctx context.Context, spec azure.Spec) error { klog.V(2).Infof("creating vm %s ", vmSpec.Name) + virtualMachine, err := deriveVirtualMachineParameters(vmSpec, s.Scope.MachineConfig.Location, s.Scope.SubscriptionID, nic) + if err != nil { + return err + } + + future, err := s.Client.CreateOrUpdate( + ctx, + s.Scope.MachineConfig.ResourceGroup, + vmSpec.Name, + *virtualMachine) + if err != nil { + return fmt.Errorf("cannot create vm: %w", err) + } + + // Do not wait until the operation completes. Just check the result + // so the call to Create actuator operation is async. + _, err = future.Result(s.Client) + if err != nil { + return err + } + + klog.V(2).Infof("successfully created vm %s ", vmSpec.Name) + return err +} + +func generateOSProfile(vmSpec *Spec) (*compute.OSProfile, error) { sshKeyData := vmSpec.SSHKeyData if sshKeyData == "" && compute.OperatingSystemTypes(vmSpec.OSDisk.OSType) != compute.Windows { privateKey, perr := rsa.GenerateKey(rand.Reader, 2048) if perr != nil { - return fmt.Errorf("Failed to generate private key: %w", perr) + return nil, fmt.Errorf("Failed to generate private key: %w", perr) } publicRsaKey, perr := ssh.NewPublicKey(&privateKey.PublicKey) if perr != nil { - return fmt.Errorf("Failed to generate public key: %w", perr) + return nil, fmt.Errorf("Failed to generate public key: %w", perr) } sshKeyData = string(ssh.MarshalAuthorizedKey(publicRsaKey)) } randomPassword, err := GenerateRandomString(32) if err != nil { - return fmt.Errorf("failed to generate random string: %w", err) - } - - imageReference := &compute.ImageReference{ - Publisher: to.StringPtr(vmSpec.Image.Publisher), - Offer: to.StringPtr(vmSpec.Image.Offer), - Sku: to.StringPtr(vmSpec.Image.SKU), - Version: to.StringPtr(vmSpec.Image.Version), - } - if vmSpec.Image.ResourceID != "" { - imageReference = &compute.ImageReference{ - ID: to.StringPtr(fmt.Sprintf("/subscriptions/%s%s", s.Scope.SubscriptionID, vmSpec.Image.ResourceID)), - } + return nil, fmt.Errorf("failed to generate random string: %w", err) } osProfile := &compute.OSProfile{ @@ -186,8 +201,44 @@ func (s *Service) CreateOrUpdate(ctx context.Context, spec azure.Spec) error { osProfile.CustomData = to.StringPtr(vmSpec.CustomData) } - virtualMachine := compute.VirtualMachine{ - Location: to.StringPtr(s.Scope.MachineConfig.Location), + return osProfile, nil +} + +// Derive virtual machine parameters for CreateOrUpdate API call based +// on the provided virtual machine specification, resource location, +// subscription ID, and the network interface. +func deriveVirtualMachineParameters(vmSpec *Spec, location string, subscription string, nic network.Interface) (*compute.VirtualMachine, error) { + osProfile, err := generateOSProfile(vmSpec) + if err != nil { + return nil, err + } + + imageReference := &compute.ImageReference{ + Publisher: to.StringPtr(vmSpec.Image.Publisher), + Offer: to.StringPtr(vmSpec.Image.Offer), + Sku: to.StringPtr(vmSpec.Image.SKU), + Version: to.StringPtr(vmSpec.Image.Version), + } + if vmSpec.Image.ResourceID != "" { + imageReference = &compute.ImageReference{ + ID: to.StringPtr(fmt.Sprintf("/subscriptions/%s%s", subscription, vmSpec.Image.ResourceID)), + } + } + + var diskEncryptionSet *compute.DiskEncryptionSetParameters + if vmSpec.OSDisk.ManagedDisk.DiskEncryptionSet != nil { + diskEncryptionSet = &compute.DiskEncryptionSetParameters{ID: to.StringPtr(vmSpec.OSDisk.ManagedDisk.DiskEncryptionSet.ID)} + } + + var securityProfile *compute.SecurityProfile + if vmSpec.SecurityProfile != nil { + securityProfile = &compute.SecurityProfile{ + EncryptionAtHost: vmSpec.SecurityProfile.EncryptionAtHost, + } + } + + virtualMachine := &compute.VirtualMachine{ + Location: to.StringPtr(location), Tags: getTagListFromSpec(vmSpec), VirtualMachineProperties: &compute.VirtualMachineProperties{ HardwareProfile: &compute.HardwareProfile{ @@ -202,10 +253,12 @@ func (s *Service) CreateOrUpdate(ctx context.Context, spec azure.Spec) error { DiskSizeGB: to.Int32Ptr(vmSpec.OSDisk.DiskSizeGB), ManagedDisk: &compute.ManagedDiskParameters{ StorageAccountType: compute.StorageAccountTypes(vmSpec.OSDisk.ManagedDisk.StorageAccountType), + DiskEncryptionSet: diskEncryptionSet, }, }, }, - OsProfile: osProfile, + SecurityProfile: securityProfile, + OsProfile: osProfile, NetworkProfile: &compute.NetworkProfile{ NetworkInterfaces: &[]compute.NetworkInterfaceReference{ { @@ -231,33 +284,12 @@ func (s *Service) CreateOrUpdate(ctx context.Context, spec azure.Spec) error { } } - if vmSpec.OSDisk.ManagedDisk.DiskEncryptionSet != nil { - virtualMachine.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet = &compute.DiskEncryptionSetParameters{ID: to.StringPtr(vmSpec.OSDisk.ManagedDisk.DiskEncryptionSet.ID)} - } - if vmSpec.Zone != "" { zones := []string{vmSpec.Zone} virtualMachine.Zones = &zones } - future, err := s.Client.CreateOrUpdate( - ctx, - s.Scope.MachineConfig.ResourceGroup, - vmSpec.Name, - virtualMachine) - if err != nil { - return fmt.Errorf("cannot create vm: %w", err) - } - - // Do not wait until the operation completes. Just check the result - // so the call to Create actuator operation is async. - _, err = future.Result(s.Client) - if err != nil { - return err - } - - klog.V(2).Infof("successfully created vm %s ", vmSpec.Name) - return err + return virtualMachine, nil } func getTagListFromSpec(spec *Spec) map[string]*string { diff --git a/pkg/cloud/azure/services/virtualmachines/virtualmachines_test.go b/pkg/cloud/azure/services/virtualmachines/virtualmachines_test.go index 38eb644292b..42a79ee9469 100644 --- a/pkg/cloud/azure/services/virtualmachines/virtualmachines_test.go +++ b/pkg/cloud/azure/services/virtualmachines/virtualmachines_test.go @@ -1,10 +1,16 @@ package virtualmachines import ( + "fmt" "reflect" "testing" + . "github.com/onsi/gomega" + + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-30/compute" + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2020-06-01/network" "github.com/Azure/go-autorest/autorest/to" + "sigs.k8s.io/cluster-api-provider-azure/pkg/apis/azureprovider/v1beta1" ) func TestGetTagListFromSpec(t *testing.T) { @@ -38,3 +44,111 @@ func TestGetTagListFromSpec(t *testing.T) { } } } + +func TestDeriveVirtualMachineParameters(t *testing.T) { + testCases := []struct { + name string + updateSpec func(*Spec) + validate func(*WithT, *compute.VirtualMachine) + }{ + { + name: "Unspecified security profile", + updateSpec: nil, + validate: func(g *WithT, vm *compute.VirtualMachine) { + g.Expect(vm.SecurityProfile).To(BeNil()) + }, + }, + { + name: "Security profile with EncryptionAtHost set to true", + updateSpec: func(vmSpec *Spec) { + vmSpec.SecurityProfile = &v1beta1.SecurityProfile{EncryptionAtHost: to.BoolPtr(true)} + }, + validate: func(g *WithT, vm *compute.VirtualMachine) { + g.Expect(vm.SecurityProfile).ToNot(BeNil()) + g.Expect(vm.SecurityProfile.EncryptionAtHost).ToNot(BeNil()) + g.Expect(*vm.SecurityProfile.EncryptionAtHost).To(BeTrue()) + }, + }, + { + name: "Security profile with EncryptionAtHost set to false", + updateSpec: func(vmSpec *Spec) { + vmSpec.SecurityProfile = &v1beta1.SecurityProfile{EncryptionAtHost: to.BoolPtr(false)} + }, + validate: func(g *WithT, vm *compute.VirtualMachine) { + g.Expect(vm.SecurityProfile).ToNot(BeNil()) + g.Expect(vm.SecurityProfile.EncryptionAtHost).ToNot(BeNil()) + g.Expect(*vm.SecurityProfile.EncryptionAtHost).To(BeFalse()) + }, + }, + { + name: "Security profile with EncryptionAtHost unset", + updateSpec: func(vmSpec *Spec) { + vmSpec.SecurityProfile = &v1beta1.SecurityProfile{EncryptionAtHost: nil} + }, + validate: func(g *WithT, vm *compute.VirtualMachine) { + g.Expect(vm.SecurityProfile).ToNot(BeNil()) + g.Expect(vm.SecurityProfile.EncryptionAtHost).To(BeNil()) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + vmSpec := getTestVMSpec(tc.updateSpec) + subscription := "226e02ba-43d1-43d3-a02a-19e584a4ef67" + resourcegroup := "foobar" + location := "eastus" + nic := getTestNic(vmSpec, subscription, resourcegroup, location) + + vm, err := deriveVirtualMachineParameters(vmSpec, location, subscription, nic) + + g.Expect(err).ToNot(HaveOccurred()) + tc.validate(g, vm) + }) + } +} + +func getTestNic(vmSpec *Spec, subscription, resourcegroup, location string) network.Interface { + return network.Interface{ + Etag: to.StringPtr("foobar"), + ID: to.StringPtr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/networkInterfaces/%s", subscription, resourcegroup, vmSpec.NICName)), + Name: &vmSpec.NICName, + Type: to.StringPtr("awesome"), + Location: to.StringPtr("location"), + Tags: map[string]*string{}, + } +} + +func getTestVMSpec(updateSpec func(*Spec)) *Spec { + spec := &Spec{ + Name: "my-awesome-machine", + NICName: "gxqb-master-nic", + SSHKeyData: "", + Size: "Standard_D4s_v3", + Zone: "", + Image: v1beta1.Image{ + Publisher: "Red Hat Inc", + Offer: "ubi", + SKU: "ubi7", + Version: "latest", + }, + OSDisk: v1beta1.OSDisk{ + OSType: "Linux", + DiskSizeGB: 256, + }, + CustomData: "", + ManagedIdentity: "", + Tags: map[string]string{}, + Priority: compute.Regular, + EvictionPolicy: compute.Delete, + BillingProfile: nil, + SecurityProfile: nil, + } + + if updateSpec != nil { + updateSpec(spec) + } + + return spec +}