diff --git a/docs/changelog.md b/docs/changelog.md index f867b2aea2e..fa55e3701aa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -32,6 +32,10 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers What's changed since pre-release v1.48.0-B0228: +- New rules: + - Azure Fleet: + - Added `Azure.Fleet.SecureBoot` to require Trusted Launch/Confidential VM with Secure Boot for fleet VM profiles by @l46983284-cpu. + [#3729](https://github.com/Azure/PSRule.Rules.Azure/issues/3729) - General improvements: - Added support for resolving filtered subnet IDs from existing virtual networks during Bicep expansion. [#2159](https://github.com/Azure/PSRule.Rules.Azure/issues/2159) diff --git a/docs/en/rules/Azure.Fleet.SecureBoot.md b/docs/en/rules/Azure.Fleet.SecureBoot.md new file mode 100644 index 00000000000..0f3e7dabc2d --- /dev/null +++ b/docs/en/rules/Azure.Fleet.SecureBoot.md @@ -0,0 +1,187 @@ +--- +reviewed: 2026-07-14 +severity: Important +pillar: Security +category: SE:08 Hardening resources +resource: Azure Fleet +resourceType: Microsoft.AzureFleet/fleets +online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.Fleet.SecureBoot/ +--- + +# Azure Fleet Secure Boot is not enabled + +## SYNOPSIS + +Operating systems or drivers may be maliciously modified or injected if an actor gains access to VM/ OS storage or build media. + +## DESCRIPTION + +Azure Fleet virtual machine profiles are able to run a wide range of operating systems including many distributions of Windows and Linux. +A malicious actor may attempt to tamper or inject operating system and driver components to gain access to resources and persist between reboots. + +When a fleet VM instance is started, Azure is able to verify if: + +1. The operating system and drivers originate from a trusted source. +2. These components are in their original unaltered state. + +Azure is able to perform this verification by Secure Boot and Trusted Launch features. +These features verify the cryptographic signatures of early boot components before they start. + +Secure Boot and Trusted Launch are on by default for many configurations. +However, if you are running an older configuration these features may need to be enabled. + +Setting the security type to `ConfidentialVM` is also acceptable. + +## RECOMMENDATION + +Consider enabling Trusted Launch or Confidential VM with Secure Boot for Azure Fleet VM profiles to protect against boot-level attacks. + +## EXAMPLES + +### Configure with Bicep + +To deploy an Azure Fleet that passes this rule: + +- Set the `properties.computeProfile.baseVirtualMachineProfile.securityProfile.securityType` property to `TrustedLaunch` or `ConfidentialVM`. +- Set the `properties.computeProfile.baseVirtualMachineProfile.securityProfile.uefiSettings.secureBootEnabled` property to `true`. + +For example: + +```bicep +resource windows_fleet 'Microsoft.AzureFleet/fleets@2024-11-01' = { + name: name + location: location + properties: { + computeProfile: { + baseVirtualMachineProfile: { + securityProfile: { + securityType: 'TrustedLaunch' + encryptionAtHost: true + uefiSettings: { + secureBootEnabled: true + vTpmEnabled: true + } + } + osProfile: { + computerNamePrefix: 'fleet' + adminUsername: adminUsername + adminPassword: secret + } + networkProfile: { + networkInterfaceConfigurations: [ + { + name: 'netconfig' + properties: { + ipConfigurations: [ + { + name: 'ipconfig' + properties: { + primary: true + subnet: { + id: subnetId + } + } + } + ] + } + } + ] + } + } + } + vmSizesProfile: [ + { + name: 'Standard_D8ds_v6' + rank: 0 + } + ] + regularPriorityProfile: { + minCapacity: 1 + capacity: 5 + allocationStrategy: 'Prioritized' + } + } +} +``` + +### Configure with Azure template + +To deploy an Azure Fleet that passes this rule: + +- Set the `properties.computeProfile.baseVirtualMachineProfile.securityProfile.securityType` property to `TrustedLaunch` or `ConfidentialVM`. +- Set the `properties.computeProfile.baseVirtualMachineProfile.securityProfile.uefiSettings.secureBootEnabled` property to `true`. + +For example: + +```json +{ + "type": "Microsoft.AzureFleet/fleets", + "apiVersion": "2024-11-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "properties": { + "computeProfile": { + "baseVirtualMachineProfile": { + "securityProfile": { + "securityType": "TrustedLaunch", + "encryptionAtHost": true, + "uefiSettings": { + "secureBootEnabled": true, + "vTpmEnabled": true + } + }, + "osProfile": { + "computerNamePrefix": "fleet", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('secret')]" + }, + "networkProfile": { + "networkInterfaceConfigurations": [ + { + "name": "netconfig", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "primary": true, + "subnet": { + "id": "[parameters('subnetId')]" + } + } + } + ] + } + } + ] + } + } + }, + "vmSizesProfile": [ + { + "name": "Standard_D8ds_v6", + "rank": 0 + } + ], + "regularPriorityProfile": { + "minCapacity": 1, + "capacity": 5, + "allocationStrategy": "Prioritized" + } + } +} +``` + +## NOTES + +Currently there are a few limitations (see documentation for up to date details), including: + +- A supported VM SKU and operating system is required. +- Secure Boot and Trusted Launch is only supported on Generation 2 VM images. + +## LINKS + +- [SE:08 Hardening resources](https://learn.microsoft.com/azure/well-architected/security/harden-resources) +- [Security: Level 2](https://learn.microsoft.com/azure/well-architected/security/maturity-model?tabs=level2) +- [Trusted Launch for Azure virtual machines](https://learn.microsoft.com/azure/virtual-machines/trusted-launch) +- [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.azurefleet/fleets#securityprofile) diff --git a/src/PSRule.Rules.Azure/en/PSRule-rules.psd1 b/src/PSRule.Rules.Azure/en/PSRule-rules.psd1 index 890758609f7..06db5cdc5b5 100644 --- a/src/PSRule.Rules.Azure/en/PSRule-rules.psd1 +++ b/src/PSRule.Rules.Azure/en/PSRule-rules.psd1 @@ -79,6 +79,8 @@ ReplicaInSecondaryNotFound = "A replica in a secondary region was not found." VMSSPublicKey = "The virtual machine scale set '{0}' should have password authentication disabled." FleetPublicKey = "The Azure Fleet '{0}' should have password authentication disabled." + FleetSecureBoot = "The Azure Fleet '{0}' should set the 'securityType' property to 'TrustedLaunch' or 'ConfidentialVM'." + FleetSecureBootEnabled = "The Azure Fleet '{0}' should have Secure Boot enabled." ACRSoftDeletePolicy = "The container registry '{0}' should have soft delete policy enabled." ACRSoftDeletePolicyRetention = "The container registry '{0}' should have retention period value between one to 90 days for the soft delete policy." ContainerRegistryAuditDiagnosticSetting = "Minimum one diagnostic setting should have ({0}) configured or category group ({1}) configured." diff --git a/src/PSRule.Rules.Azure/rules/Azure.Fleet.Rule.ps1 b/src/PSRule.Rules.Azure/rules/Azure.Fleet.Rule.ps1 index b0f8c37903a..e6d9678b1bf 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.Fleet.Rule.ps1 +++ b/src/PSRule.Rules.Azure/rules/Azure.Fleet.Rule.ps1 @@ -13,4 +13,13 @@ Rule 'Azure.Fleet.PublicKey' -Ref 'AZR-000541' -Type 'Microsoft.AzureFleet/fleet Reason($LocalizedData.FleetPublicKey, $PSRule.TargetName) } + +# Synopsis: Azure Fleet VM profiles should use Trusted Launch with Secure Boot enabled. +Rule 'Azure.Fleet.SecureBoot' -Ref 'AZR-000545' -Type 'Microsoft.AzureFleet/fleets' -Tag @{ release = 'GA'; ruleSet = '2026_09'; 'Azure.WAF/pillar' = 'Security'; } -Labels @{ 'Azure.WAF/maturity' = 'L2' } { + $Assert.In($TargetObject, 'properties.computeProfile.baseVirtualMachineProfile.securityProfile.securityType', @('TrustedLaunch', 'ConfidentialVM')). + Reason($LocalizedData.FleetSecureBoot, $PSRule.TargetName) + $Assert.HasFieldValue($TargetObject, 'properties.computeProfile.baseVirtualMachineProfile.securityProfile.uefiSettings.secureBootEnabled', $True). + Reason($LocalizedData.FleetSecureBootEnabled, $PSRule.TargetName) +} + #endregion Rules diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.Fleet.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.Fleet.Tests.ps1 index 2a73bdcdec8..8d21ee27a54 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.Fleet.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.Fleet.Tests.ps1 @@ -53,8 +53,24 @@ Describe 'Azure.Fleet' -Tag 'Fleet' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 1; - $ruleResult.TargetName | Should -Be 'fleet-001'; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'fleet-001', 'fleet-005'; + } + + It 'Azure.Fleet.SecureBoot' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.Fleet.SecureBoot' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 5; + $ruleResult.TargetName | Should -BeIn 'fleet-001', 'fleet-002', 'fleet-003', 'fleet-006', 'fleet-007'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult | Should -Not -BeNullOrEmpty; + $ruleResult.Length | Should -Be 3; + $ruleResult.TargetName | Should -BeIn 'fleet-004', 'fleet-005', 'fleet-008'; } } } diff --git a/tests/PSRule.Rules.Azure.Tests/Resources.Fleet.json b/tests/PSRule.Rules.Azure.Tests/Resources.Fleet.json index a5b25f8e378..14d9f77e4d0 100644 --- a/tests/PSRule.Rules.Azure.Tests/Resources.Fleet.json +++ b/tests/PSRule.Rules.Azure.Tests/Resources.Fleet.json @@ -321,5 +321,348 @@ "ResourceGroupName": "test-rg", "Type": "Microsoft.AzureFleet/fleets", "Tags": {} + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.AzureFleet/fleets/fleet-005", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.AzureFleet/fleets/fleet-005", + "Identity": null, + "Kind": null, + "Location": "eastus", + "ManagedBy": null, + "ResourceName": "fleet-005", + "Name": "fleet-005", + "ExtensionResourceName": null, + "ParentResource": null, + "Plan": null, + "Properties": { + "computeProfile": { + "baseVirtualMachineProfile": { + "securityProfile": { + "securityType": "TrustedLaunch", + "uefiSettings": { + "secureBootEnabled": true, + "vTpmEnabled": true + } + }, + "osProfile": { + "computerNamePrefix": "fleet", + "adminUsername": "azureuser", + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "/home/azureuser/.ssh/authorized_keys", + "keyData": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC" + } + ] + }, + "provisionVMAgent": true + } + }, + "storageProfile": { + "osDisk": { + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Premium_LRS" + } + }, + "imageReference": { + "publisher": "MicrosoftCblMariner", + "offer": "Cbl-Mariner", + "sku": "cbl-mariner-2-gen2", + "version": "latest" + } + }, + "networkProfile": { + "networkInterfaceConfigurations": [ + { + "name": "netconfig", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "primary": true, + "subnet": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-001/subnets/subnet-001" + } + } + } + ] + } + } + ] + } + } + }, + "vmSizesProfile": [ + { + "name": "Standard_D8ds_v6", + "rank": 0 + } + ], + "regularPriorityProfile": { + "minCapacity": 1, + "capacity": 5, + "allocationStrategy": "Prioritized" + } + }, + "ResourceType": "Microsoft.AzureFleet/fleets", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "ResourceGroupName": "test-rg", + "Type": "Microsoft.AzureFleet/fleets", + "Tags": {} + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.AzureFleet/fleets/fleet-006", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.AzureFleet/fleets/fleet-006", + "Identity": null, + "Kind": null, + "Location": "eastus", + "ManagedBy": null, + "ResourceName": "fleet-006", + "Name": "fleet-006", + "ExtensionResourceName": null, + "ParentResource": null, + "Plan": null, + "Properties": { + "computeProfile": { + "baseVirtualMachineProfile": { + "securityProfile": { + "securityType": "TrustedLaunch", + "encryptionAtHost": true, + "uefiSettings": { + "secureBootEnabled": false, + "vTpmEnabled": true + } + }, + "osProfile": { + "computerNamePrefix": "fleet", + "adminUsername": "azureuser", + "adminPassword": "P@ssword123!" + }, + "storageProfile": { + "osDisk": { + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Premium_LRS" + }, + "osType": "Windows" + }, + "imageReference": { + "publisher": "MicrosoftWindowsServer", + "offer": "WindowsServer", + "sku": "2022-datacenter-g2", + "version": "latest" + } + }, + "networkProfile": { + "networkInterfaceConfigurations": [ + { + "name": "netconfig", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "primary": true, + "subnet": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-001/subnets/subnet-001" + } + } + } + ] + } + } + ] + } + } + }, + "vmSizesProfile": [ + { + "name": "Standard_D8ds_v6", + "rank": 0 + } + ], + "regularPriorityProfile": { + "minCapacity": 1, + "capacity": 5, + "allocationStrategy": "Prioritized" + } + }, + "ResourceType": "Microsoft.AzureFleet/fleets", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "ResourceGroupName": "test-rg", + "Type": "Microsoft.AzureFleet/fleets", + "Tags": {} + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.AzureFleet/fleets/fleet-007", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.AzureFleet/fleets/fleet-007", + "Identity": null, + "Kind": null, + "Location": "eastus", + "ManagedBy": null, + "ResourceName": "fleet-007", + "Name": "fleet-007", + "ExtensionResourceName": null, + "ParentResource": null, + "Plan": null, + "Properties": { + "computeProfile": { + "baseVirtualMachineProfile": { + "securityProfile": { + "uefiSettings": { + "secureBootEnabled": true, + "vTpmEnabled": true + } + }, + "osProfile": { + "computerNamePrefix": "fleet", + "adminUsername": "azureuser", + "adminPassword": "P@ssword123!" + }, + "storageProfile": { + "osDisk": { + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Premium_LRS" + }, + "osType": "Windows" + }, + "imageReference": { + "publisher": "MicrosoftWindowsServer", + "offer": "WindowsServer", + "sku": "2022-datacenter-g2", + "version": "latest" + } + }, + "networkProfile": { + "networkInterfaceConfigurations": [ + { + "name": "netconfig", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "primary": true, + "subnet": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-001/subnets/subnet-001" + } + } + } + ] + } + } + ] + } + } + }, + "vmSizesProfile": [ + { + "name": "Standard_D8ds_v6", + "rank": 0 + } + ], + "regularPriorityProfile": { + "minCapacity": 1, + "capacity": 5, + "allocationStrategy": "Prioritized" + } + }, + "ResourceType": "Microsoft.AzureFleet/fleets", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "ResourceGroupName": "test-rg", + "Type": "Microsoft.AzureFleet/fleets", + "Tags": {} + }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.AzureFleet/fleets/fleet-008", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.AzureFleet/fleets/fleet-008", + "Identity": null, + "Kind": null, + "Location": "eastus", + "ManagedBy": null, + "ResourceName": "fleet-008", + "Name": "fleet-008", + "ExtensionResourceName": null, + "ParentResource": null, + "Plan": null, + "Properties": { + "computeProfile": { + "baseVirtualMachineProfile": { + "securityProfile": { + "securityType": "ConfidentialVM", + "encryptionAtHost": true, + "uefiSettings": { + "secureBootEnabled": true, + "vTpmEnabled": true + } + }, + "osProfile": { + "computerNamePrefix": "fleet", + "adminUsername": "azureuser", + "adminPassword": "P@ssword123!" + }, + "storageProfile": { + "osDisk": { + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Premium_LRS" + }, + "osType": "Windows" + }, + "imageReference": { + "publisher": "MicrosoftWindowsServer", + "offer": "WindowsServer", + "sku": "2022-datacenter-g2", + "version": "latest" + } + }, + "networkProfile": { + "networkInterfaceConfigurations": [ + { + "name": "netconfig", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "primary": true, + "subnet": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-001/subnets/subnet-001" + } + } + } + ] + } + } + ] + } + } + }, + "vmSizesProfile": [ + { + "name": "Standard_D8ds_v6", + "rank": 0 + } + ], + "regularPriorityProfile": { + "minCapacity": 1, + "capacity": 5, + "allocationStrategy": "Prioritized" + } + }, + "ResourceType": "Microsoft.AzureFleet/fleets", + "SubscriptionId": "00000000-0000-0000-0000-000000000000", + "ResourceGroupName": "test-rg", + "Type": "Microsoft.AzureFleet/fleets", + "Tags": {} } ]