From a37666c9c037efad226e61f93f13f93207f80da4 Mon Sep 17 00:00:00 2001 From: amccullough84 Date: Fri, 24 Feb 2023 20:00:39 +0000 Subject: [PATCH] Enable SecurityProfile (TrustedLaunch) for Virtual Machines (#257) * Add Trusted Launch feature * Simplify logic * Remove if condtions all together * Added Test for Trusted Launch === RUN TestTrustedLaunch01 --- PASS: TestTrustedLaunch01 (0.00s) PASS ok github.com/hashicorp/packer-plugin-azure/builder/azure/arm 1.721s * Fixed Failing Tests Updated approved JSON following rebase * Added ParameterSet check for Managed Image with vTPM or SecureBoot enabled * I told you the wrong field for managed images in the PR review, I'm committing directly to your fork, will describe changes in PR comment * And managed image check should be or, neither of these fields are valid when secure boot is set --------- Co-authored-by: Max Lampert Co-authored-by: Jenna Goldstrich --- builder/azure/arm/config.go | 9 + builder/azure/arm/config.hcl2spec.go | 4 + builder/azure/arm/config_test.go | 53 +++++ builder/azure/arm/template_factory.go | 7 + ...ory_test.TestTrustedLaunch01.approved.json | 211 ++++++++++++++++++ builder/azure/arm/template_factory_test.go | 18 ++ builder/azure/common/template/template.go | 1 + .../azure/common/template/template_builder.go | 16 ++ .../builder/azure/arm/Config-not-required.mdx | 4 + 9 files changed, 323 insertions(+) create mode 100644 builder/azure/arm/template_factory_test.TestTrustedLaunch01.approved.json diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go index 8b025ff3..6633bf77 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -535,6 +535,11 @@ type Config struct { // or // [Linux](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/azure-hybrid-benefit-linux) LicenseType string `mapstructure:"license_type" required:"false"` + // Specifies if Secure Boot and Trusted Launch is enabled for the Virtual Machine. + SecureBootEnabled bool `mapstructure:"secure_boot_enabled" required:"false"` + + // Specifies if vTPM (virtual Trusted Platform Module) and Trusted Launch is enabled for the Virtual Machine. + VTpmEnabled bool `mapstructure:"vtpm_enabled" required:"false"` // Runtime Values UserName string `mapstructure-to-hcl2:",skip"` @@ -1063,6 +1068,10 @@ func assertRequiredParametersSet(c *Config, errs *packersdk.MultiError) { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("A managed image must be created from a managed image, it cannot be created from a VHD.")) } + if (c.SecureBootEnabled || c.VTpmEnabled) && (c.ManagedImageName != "" || c.ManagedImageResourceGroupName != "") { + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("A managed image (managed_image_name, managed_image_resource_group_name) can not set SecureBoot or VTpm, these features are only supported when directly publishing to a Shared Image Gallery")) + } + if (c.CaptureContainerName != "" || c.CaptureNamePrefix != "" || c.ManagedImageName != "") && c.DiskEncryptionSetId != "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("Setting a disk encryption set ID is not allowed when building a VHD or creating a Managed Image, only when publishing directly to Shared Image Gallery")) } diff --git a/builder/azure/arm/config.hcl2spec.go b/builder/azure/arm/config.hcl2spec.go index 2b68f22f..c2fb1df0 100644 --- a/builder/azure/arm/config.hcl2spec.go +++ b/builder/azure/arm/config.hcl2spec.go @@ -90,6 +90,8 @@ type FlatConfig struct { BootDiagSTGAccount *string `mapstructure:"boot_diag_storage_account" required:"false" cty:"boot_diag_storage_account" hcl:"boot_diag_storage_account"` CustomResourcePrefix *string `mapstructure:"custom_resource_build_prefix" required:"false" cty:"custom_resource_build_prefix" hcl:"custom_resource_build_prefix"` LicenseType *string `mapstructure:"license_type" required:"false" cty:"license_type" hcl:"license_type"` + SecureBootEnabled *bool `mapstructure:"secure_boot_enabled" required:"false" cty:"secure_boot_enabled" hcl:"secure_boot_enabled"` + VTpmEnabled *bool `mapstructure:"vtpm_enabled" required:"false" cty:"vtpm_enabled" hcl:"vtpm_enabled"` Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"` PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"` SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"` @@ -232,6 +234,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "boot_diag_storage_account": &hcldec.AttrSpec{Name: "boot_diag_storage_account", Type: cty.String, Required: false}, "custom_resource_build_prefix": &hcldec.AttrSpec{Name: "custom_resource_build_prefix", Type: cty.String, Required: false}, "license_type": &hcldec.AttrSpec{Name: "license_type", Type: cty.String, Required: false}, + "secure_boot_enabled": &hcldec.AttrSpec{Name: "secure_boot_enabled", Type: cty.Bool, Required: false}, + "vtpm_enabled": &hcldec.AttrSpec{Name: "vtpm_enabled", Type: cty.Bool, Required: false}, "communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false}, "pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false}, "ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false}, diff --git a/builder/azure/arm/config_test.go b/builder/azure/arm/config_test.go index 15a68b80..eb25ba61 100644 --- a/builder/azure/arm/config_test.go +++ b/builder/azure/arm/config_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "strings" "testing" "time" @@ -1324,6 +1325,58 @@ func TestConfigShouldRejectManagedImageOSDiskSnapshotNameAndManagedImageDataDisk } } +func TestConfigShouldRejectSecureBootWhenPublishingToAManagedImage(t *testing.T) { + expectedErrorMessage := "A managed image (managed_image_name, managed_image_resource_group_name) can not set SecureBoot or VTpm, these features are only supported when directly publishing to a Shared Image Gallery" + config := map[string]interface{}{ + "image_offer": "ignore", + "image_publisher": "ignore", + "image_sku": "ignore", + "location": "ignore", + "subscription_id": "ignore", + "communicator": "none", + "managed_image_resource_group_name": "ignore", + "managed_image_name": "ignore", + "secure_boot_enabled": "true", + + // Does not matter for this test case, just pick one. + "os_type": constants.Target_Linux, + } + + var c Config + _, err := c.Prepare(config, getPackerConfiguration()) + if err == nil { + t.Fatal("expected config to reject managed image with secure boot, secure boot is only allowed when direct publishing to SIG") + } else if !strings.Contains(err.Error(), expectedErrorMessage) { + t.Fatalf("unexpected rejection reason, expected %s to contain %s", err.Error(), expectedErrorMessage) + } +} + +func TestConfigShouldRejectVTPMWhenPublishingToAManagedImage(t *testing.T) { + expectedErrorMessage := "A managed image (managed_image_name, managed_image_resource_group_name) can not set SecureBoot or VTpm, these features are only supported when directly publishing to a Shared Image Gallery" + config := map[string]interface{}{ + "image_offer": "ignore", + "image_publisher": "ignore", + "image_sku": "ignore", + "location": "ignore", + "subscription_id": "ignore", + "communicator": "none", + "managed_image_resource_group_name": "ignore", + "managed_image_name": "ignore", + "vtpm_enabled": "true", + + // Does not matter for this test case, just pick one. + "os_type": constants.Target_Linux, + } + + var c Config + _, err := c.Prepare(config, getPackerConfiguration()) + if err == nil { + t.Fatal("expected config to reject managed image with secure boot, secure boot is only allowed when direct publishing to SIG") + } else if !strings.Contains(err.Error(), expectedErrorMessage) { + t.Fatalf("unexpected rejection reason, expected %s to contain %s", err.Error(), expectedErrorMessage) + } +} + func TestConfigShouldAcceptPlatformManagedImageBuild(t *testing.T) { config := map[string]interface{}{ "image_offer": "ignore", diff --git a/builder/azure/arm/template_factory.go b/builder/azure/arm/template_factory.go index e90a7418..ceee40f6 100644 --- a/builder/azure/arm/template_factory.go +++ b/builder/azure/arm/template_factory.go @@ -238,6 +238,13 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error) } } + if config.SecureBootEnabled || config.VTpmEnabled { + err = builder.SetSecurityProfile(config.SecureBootEnabled, config.VTpmEnabled) + if err != nil { + return nil, err + } + } + err = builder.SetTags(&config.AzureTags) if err != nil { return nil, err diff --git a/builder/azure/arm/template_factory_test.TestTrustedLaunch01.approved.json b/builder/azure/arm/template_factory_test.TestTrustedLaunch01.approved.json new file mode 100644 index 00000000..7495c74f --- /dev/null +++ b/builder/azure/arm/template_factory_test.TestTrustedLaunch01.approved.json @@ -0,0 +1,211 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "adminPassword": { + "type": "securestring" + }, + "adminUsername": { + "type": "string" + }, + "commandToExecute": { + "type": "string" + }, + "dataDiskName": { + "type": "string" + }, + "dnsNameForPublicIP": { + "type": "string" + }, + "nicName": { + "type": "string" + }, + "nsgName": { + "type": "string" + }, + "osDiskName": { + "type": "string" + }, + "publicIPAddressName": { + "type": "string" + }, + "storageAccountBlobEndpoint": { + "type": "string" + }, + "subnetName": { + "type": "string" + }, + "virtualNetworkName": { + "type": "string" + }, + "vmName": { + "type": "string" + }, + "vmSize": { + "type": "string" + } + }, + "resources": [ + { + "apiVersion": "[variables('publicIPAddressApiVersion')]", + "location": "[variables('location')]", + "name": "[parameters('publicIPAddressName')]", + "properties": { + "dnsSettings": { + "domainNameLabel": "[parameters('dnsNameForPublicIP')]" + }, + "publicIPAllocationMethod": "[variables('publicIPAddressType')]" + }, + "type": "Microsoft.Network/publicIPAddresses" + }, + { + "apiVersion": "[variables('virtualNetworksApiVersion')]", + "location": "[variables('location')]", + "name": "[variables('virtualNetworkName')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('addressPrefix')]" + ] + }, + "subnets": [ + { + "name": "[variables('subnetName')]", + "properties": { + "addressPrefix": "[variables('subnetAddressPrefix')]" + } + } + ] + }, + "type": "Microsoft.Network/virtualNetworks" + }, + { + "apiVersion": "[variables('networkInterfacesApiVersion')]", + "dependsOn": [ + "[concat('Microsoft.Network/publicIPAddresses/', parameters('publicIPAddressName'))]", + "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" + ], + "location": "[variables('location')]", + "name": "[parameters('nicName')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('publicIPAddressName'))]" + }, + "subnet": { + "id": "[variables('subnetRef')]" + } + } + } + ] + }, + "type": "Microsoft.Network/networkInterfaces" + }, + { + "apiVersion": "[variables('apiVersion')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', parameters('nicName'))]" + ], + "location": "[variables('location')]", + "name": "[parameters('vmName')]", + "properties": { + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": false + } + }, + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('nicName'))]" + } + ] + }, + "osProfile": { + "adminPassword": "[parameters('adminPassword')]", + "adminUsername": "[parameters('adminUsername')]", + "computerName": "[parameters('vmName')]", + "linuxConfiguration": { + "ssh": { + "publicKeys": [ + { + "keyData": "", + "path": "[variables('sshKeyPath')]" + } + ] + } + } + }, + "securityProfile": { + "securityType": "TrustedLaunch", + "uefiSettings": { + "secureBootEnabled": true, + "vTpmEnabled": true + } + }, + "storageProfile": { + "imageReference": { + "offer": "ignored00", + "publisher": "ignored00", + "sku": "ignored00", + "version": "latest" + }, + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "name": "[parameters('osDiskName')]", + "vhd": { + "uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]" + } + } + } + }, + "type": "Microsoft.Compute/virtualMachines" + }, + { + "apiVersion": "2022-08-01", + "condition": "[not(empty(parameters('commandToExecute')))]", + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines/', parameters('vmName'))]" + ], + "location": "[variables('location')]", + "name": "[concat(parameters('vmName'), '/extension-customscript')]", + "properties": { + "autoUpgradeMinorVersion": true, + "publisher": "Microsoft.Compute", + "settings": { + "commandToExecute": "[parameters('commandToExecute')]" + }, + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.8" + }, + "type": "Microsoft.Compute/virtualMachines/extensions" + } + ], + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2020-12-01", + "location": "[resourceGroup().location]", + "managedDiskApiVersion": "2017-03-30", + "networkInterfacesApiVersion": "2017-04-01", + "networkSecurityGroupsApiVersion": "2019-04-01", + "publicIPAddressApiVersion": "2017-04-01", + "publicIPAddressType": "Dynamic", + "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", + "subnetAddressPrefix": "10.0.0.0/24", + "subnetName": "[parameters('subnetName')]", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "[parameters('virtualNetworkName')]", + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "virtualNetworksApiVersion": "2017-04-01", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } +} \ No newline at end of file diff --git a/builder/azure/arm/template_factory_test.go b/builder/azure/arm/template_factory_test.go index 06e156f4..d0f3ca24 100644 --- a/builder/azure/arm/template_factory_test.go +++ b/builder/azure/arm/template_factory_test.go @@ -690,3 +690,21 @@ func TestPlanInfo02(t *testing.T) { approvaltests.VerifyJSONStruct(t, deployment.Properties.Template) } + +func TestTrustedLaunch01(t *testing.T) { + m := getArmBuilderConfiguration() + m["secure_boot_enabled"] = "true" + m["vtpm_enabled"] = "true" + + var c Config + _, err := c.Prepare(m, getPackerConfiguration(), getPackerSSHPasswordCommunicatorConfiguration()) + if err != nil { + t.Fatal(err) + } + deployment, err := GetVirtualMachineDeployment(&c) + if err != nil { + t.Fatal(err) + } + + approvaltests.VerifyJSONStruct(t, deployment.Properties.Template) +} diff --git a/builder/azure/common/template/template.go b/builder/azure/common/template/template.go index a6478969..884ae1b3 100644 --- a/builder/azure/common/template/template.go +++ b/builder/azure/common/template/template.go @@ -104,6 +104,7 @@ type Properties struct { Sku *Sku `json:"sku,omitempty"` UserData *string `json:"userData,omitempty"` StorageProfile *StorageProfileUnion `json:"storageProfile,omitempty"` + SecurityProfile *compute.SecurityProfile `json:"securityProfile,omitempty"` Subnets *[]network.Subnet `json:"subnets,omitempty"` SecurityRules *[]network.SecurityRule `json:"securityRules,omitempty"` TenantId *string `json:"tenantId,omitempty"` diff --git a/builder/azure/common/template/template_builder.go b/builder/azure/common/template/template_builder.go index c29e9890..59ecedc8 100644 --- a/builder/azure/common/template/template_builder.go +++ b/builder/azure/common/template/template_builder.go @@ -498,6 +498,22 @@ func (s *TemplateBuilder) SetLicenseType(licenseType string) error { return nil } +func (s *TemplateBuilder) SetSecurityProfile(secureBootEnabled bool, vtpmEnabled bool) error { + s.setVariable("apiVersion", "2020-12-01") // Required for Trusted Launch + resource, err := s.getResourceByType(resourceVirtualMachine) + if err != nil { + return err + } + + resource.Properties.SecurityProfile = &compute.SecurityProfile{} + resource.Properties.SecurityProfile.UefiSettings = &compute.UefiSettings{} + resource.Properties.SecurityProfile.SecurityType = compute.SecurityTypesTrustedLaunch + resource.Properties.SecurityProfile.UefiSettings.SecureBootEnabled = to.BoolPtr(secureBootEnabled) + resource.Properties.SecurityProfile.UefiSettings.VTpmEnabled = to.BoolPtr(vtpmEnabled) + + return nil +} + func (s *TemplateBuilder) ToJSON() (*string, error) { bs, err := json.MarshalIndent(s.template, jsonPrefix, jsonIndent) diff --git a/docs-partials/builder/azure/arm/Config-not-required.mdx b/docs-partials/builder/azure/arm/Config-not-required.mdx index c39939d3..5d521069 100644 --- a/docs-partials/builder/azure/arm/Config-not-required.mdx +++ b/docs-partials/builder/azure/arm/Config-not-required.mdx @@ -358,6 +358,10 @@ [Windows](https://learn.microsoft.com/en-us/azure/virtual-machines/windows/hybrid-use-benefit-licensing) or [Linux](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/azure-hybrid-benefit-linux) + +- `secure_boot_enabled` (bool) - Specifies if Secure Boot and Trusted Launch is enabled for the Virtual Machine. + +- `vtpm_enabled` (bool) - Specifies if vTPM (virtual Trusted Platform Module) and Trusted Launch is enabled for the Virtual Machine. - `async_resourcegroup_delete` (bool) - If you want packer to delete the temporary resource group asynchronously set this value. It's a boolean