diff --git a/README.md b/README.md index 01f1ed1..361562e 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,10 @@ Inspect the following files for instructions to test PSRule for Azure rules by c - [deployments/contoso/landing-zones/subscription-1/rg-app-001/dev.bicepparam](deployments/contoso/landing-zones/subscription-1/rg-app-001/dev.bicepparam) - [deployments/contoso/landing-zones/subscription-1/rg-app-002/deploy.bicep](deployments/contoso/landing-zones/subscription-1/rg-app-002/deploy.bicep) +For examples of how to reference resources that require secrets to be passed in, see: + +- [deployments/contoso/landing-zones/subscription-1/rg-vm-001/deploy.bicep](deployments/contoso/landing-zones/subscription-1/rg-vm-001/deploy.bicep) + ## Support This project uses GitHub Issues to track bugs and feature requests. diff --git a/deployments/contoso/landing-zones/subscription-1/rg-vm-001/deploy.bicep b/deployments/contoso/landing-zones/subscription-1/rg-vm-001/deploy.bicep new file mode 100644 index 0000000..700b837 --- /dev/null +++ b/deployments/contoso/landing-zones/subscription-1/rg-vm-001/deploy.bicep @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Note: +// This Azure Bicep code demonstrates a deployment of a VM that uses a password with two common +// deployment options. 1. Using a password from the pipeline. 2. Using a password from a Key Vault secret. + +// --------------------------------------------------------------- +// OPTION 1: A VM deployment using the password from the pipeline. +// --------------------------------------------------------------- + +// If your pipeline passes a password in as a parameter to the deployment script, use this option. +// For expansion with PSRule, a dummy value for `adminPassword` is used set in `ps-rule.yaml` with +// the `AZURE_PARAMETER_DEFAULTS` configuration option. This allows PSRule to expand the deployment, +// without exposing your secret in the code or PSRule. + +@secure() +@description('Load the admin password from the pipeline.') +param adminPassword string + +@description('A VM deployment using a password from the pipeline.') +module vm001 '../../../../../modules/virtual-machine-windows/v1/main.bicep' = { + params: { + name: 'vm-001' + adminPassword: adminPassword + adminUsername: 'vm-admin' + imageSKU: '2022-Datacenter' + size: 'Standard_D4ds_v4' + subnetId: vnet.id + tags: { + env: 'dev' + } + } +} + +// --------------------------------------------------------------------- +// OPTION 2: A VM deployment using the password from a Key Vault secret. +// --------------------------------------------------------------------- + +// If your VM deployment is able to use a Key Vault secret that is already deployed to Azure, use this option. +// When you reference a Key Vault secret, PSRule will automatically substitute a placeholder for the secret value +// during expansion. So you can use the secret in your deployment without exposing it as a deployment parameter. + +// NB: PSRule never actually attempts to retrieve the secret value, so it does not need access to the secret. + +@description('An existing Key Vault to use for the VM deployment.') +resource vault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { + name: 'kv-001' +} + +@description('Load the admin password from a Key Vault secret.') +module vm002 '../../../../../modules/virtual-machine-windows/v1/main.bicep' = { + params: { + name: 'vm-002' + adminPassword: vault.getSecret('vm002-admin-password') + adminUsername: 'vm-admin' + imageSKU: '2022-Datacenter' + size: 'Standard_D4ds_v4' + subnetId: vnet.id + tags: { + env: 'dev' + } + } +} + +// --------------- +// Other resources +// --------------- + +// An existing virtual network and subnet to connect the VM. +resource vnet 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' existing = { + name: 'vnet-001/subnet-001' +} diff --git a/modules/virtual-machine-windows/v1/.bicep/rbac.bicep b/modules/virtual-machine-windows/v1/.bicep/rbac.bicep new file mode 100644 index 0000000..0c3e21e --- /dev/null +++ b/modules/virtual-machine-windows/v1/.bicep/rbac.bicep @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Configure role assignments for the Virtual Machine + +// ---------- +// PARAMETERS +// ---------- + +@sys.description('The display name of the role to assign or the GUID.') +param role string + +@sys.description('The GUID of the identity object to assign.') +param principalId string + +@sys.description('A description of the assignment.') +param description string = '' + +@allowed([ + 'ServicePrincipal' + 'Group' + 'User' + 'ForeignGroup' + 'Device' +]) +@sys.description('The principal type to assign.') +param principalType string = 'ServicePrincipal' + +@sys.description('The name of the Virtual Machine name.') +param resource string + +// --------- +// VARIABLES +// --------- + +// Map of common RBAC role names to their IDs. +// Azure uses specific GUIDs for built-in roles however it is easier to reference them by name. +var roles = { + Owner: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635') + Contributor: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + Reader: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') + 'User Access Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' + ) + 'Virtual Machine Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '9980e02c-c2be-4d73-94e8-173b1dc7cf3c' + ) +} + +var roleDefinitionId = roles[?role] ?? subscriptionResourceId('Microsoft.Authorization/roleDefinitions', role) + +// --------- +// RESOURCES +// --------- + +resource vm 'Microsoft.Compute/virtualMachines@2023-03-01' existing = { + name: resource +} + +@sys.description('Assign permissions to an Azure AD principal.') +resource rbac 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(vm.id, principalId, roleDefinitionId) + scope: vm + properties: { + principalId: principalId + roleDefinitionId: roleDefinitionId + principalType: principalType + description: description + } +} diff --git a/modules/virtual-machine-windows/v1/.bicep/vm.bicep b/modules/virtual-machine-windows/v1/.bicep/vm.bicep new file mode 100644 index 0000000..a3fd49a --- /dev/null +++ b/modules/virtual-machine-windows/v1/.bicep/vm.bicep @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Configure the Virtual Machine resource + +// ---------- +// PARAMETERS +// ---------- + +@sys.description('The name of the virtual machine.') +param name string + +@sys.description('The Azure region to deploy to.') +param location string + +@sys.description('The SKU of the virtual machine.') +param size string + +@sys.description('The publisher of the VM image to deploy.') +param imagePublisher string + +@sys.description('The name of the offer of the VM image to deploy.') +param imageOffer string + +@sys.description('The VM image SKU to deploy.') +param imageSKU string + +@sys.description('A User Assigned Managed Identity to configure.') +param identityId string + +@sys.description('The subnet to connect the VM.') +param subnetId string + +@sys.description('The workspace to store monitoring data.') +param workspaceId string + +@sys.description('The maintenance configuration to assign.') +param maintenanceConfigurationId string + +@secure() +@sys.description('The username of the local administrator account.') +param adminUsername string + +@secure() +@sys.description('The default password assigned to the local administrator account.') +param adminPassword string + +@sys.description('Attach existing disks or create empty disks.') +param diskCreateOption string + +@sys.description('An array of managed disks to create or attach.') +param dataDisks array + +@sys.description('Determines if accelerated networking is enabled for the VM.') +param useAcceleratedNetworking bool + +@sys.description('Determines if Network Watcher extension is configured for the VM.') +param useNetworkWatcher bool + +@sys.description('Determines if Azure Policy Guest Configuration extension is configured for the VM.') +param useAzurePolicy bool + +@sys.description('Determines if SQL Server IaaS extension is configured for the VM.') +param useSqlServer bool + +@sys.description('Tags to apply to the resource.') +param tags object + +// --------- +// VARIABLES +// --------- + +var nicName = toUpper('${name}-NIC-01') +var osDiskName = '${toUpper(name)}-os' + +// Configure VM availablity +var useAvailabilitySet = true +var availabilitySetName = toUpper('${vmNamePrefix}-AVSET01') +var faultDomains = 2 +var updateDomains = 5 + +// Map managed disks for VM +var managedDisks = [ + for (disk, index) in dataDisks: { + name: disk.?name ?? '${name}-Data${index}' + sku: disk.?sku ?? 'Standard_LRS' + properties: { + creationData: { + createOption: 'Empty' + } + diskSizeGB: disk.diskSizeGB + } + } +] + +// Map data disks for VM +var vmDisks = [ + for (disk, index) in dataDisks: { + lun: index + createOption: 'Attach' + caching: disk.?caching ?? (startsWith(disk.?sku ?? 'Standard_LRS', 'Standard_') ? 'None' : 'ReadOnly') + deleteOption: disk.?deleteOption ?? 'Detach' + managedDisk: { + id: md[index].id + } + writeAcceleratorEnabled: disk.?writeAcceleratorEnabled ?? false + } +] + +var storageProfileOption = { + Empty: { + imageReference: { + publisher: imagePublisher + offer: imageOffer + sku: imageSKU + version: 'latest' + } + osDisk: { + name: osDiskName + caching: 'ReadWrite' + createOption: 'FromImage' + } + dataDisks: vmDisks + } + Attach: { + osDisk: { + osType: 'Windows' + name: osDiskName + managedDisk: { + id: '${resourceGroup().id}/providers/Microsoft.Compute/disks/${osDiskName}' + } + caching: 'ReadWrite' + createOption: 'Attach' + } + dataDisks: vmDisks + } +} +var storageProfile = storageProfileOption[diskCreateOption] + +// VM naming +var vmNamePrefix = take(name, (length(name) - 2)) + +// Configure tags +var vmTags = tags + +// --------- +// RESOURCES +// --------- + +@sys.description('Create or update an Availability Set for the VM deployment.') +resource availabilitySet 'Microsoft.Compute/availabilitySets@2023-03-01' = if (useAvailabilitySet) { + name: availabilitySetName + location: location + tags: tags + sku: { + name: 'Aligned' + } + properties: { + platformFaultDomainCount: faultDomains + platformUpdateDomainCount: updateDomains + } +} + +@sys.description('Create or update a Network Interface using dynamic addressing.') +resource nic 'Microsoft.Network/networkInterfaces@2023-02-01' = { + name: nicName + location: location + tags: tags + properties: { + enableIPForwarding: false + enableAcceleratedNetworking: useAcceleratedNetworking + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + privateIPAllocationMethod: 'Dynamic' + subnet: { + id: subnetId + } + } + } + ] + } +} + +@sys.description('Create or update Managed Disks for the VM.') +resource md 'Microsoft.Compute/disks@2023-01-02' = [ + for item in managedDisks: if (length(managedDisks) > 0) { + name: item.name + location: location + tags: tags + sku: { + name: item.sku + } + properties: item.properties + } +] + +@sys.description('Create or update a Virtual Machine (VM).') +resource vm 'Microsoft.Compute/virtualMachines@2023-03-01' = { + name: name + location: location + tags: vmTags + identity: empty(identityId) + ? { + type: 'SystemAssigned' + } + : { + type: 'SystemAssigned, UserAssigned' + userAssignedIdentities: { + '${identityId}': {} + } + } + properties: { + hardwareProfile: { + vmSize: size + } + networkProfile: { + networkInterfaces: [ + { + id: nic.id + } + ] + } + availabilitySet: { + id: availabilitySet.id + } + osProfile: diskCreateOption == 'Empty' + ? { + adminUsername: adminUsername + adminPassword: adminPassword + computerName: name + } + : null + storageProfile: storageProfile + } +} + +@sys.description('Configure a maintenance configuration for the VM.') +resource config 'Microsoft.Maintenance/configurationAssignments@2022-11-01-preview' = if (!empty(maintenanceConfigurationId)) { + name: 'default' + location: location + scope: vm + properties: { + maintenanceConfigurationId: maintenanceConfigurationId + } +} + +@sys.description('Configure the Microsoft Monitoring Agent extension for the VM.') +resource monitoringExtension 'Microsoft.Compute/virtualMachines/extensions@2023-03-01' = { + parent: vm + name: 'Microsoft.EnterpriseCloud.Monitoring' + location: location + properties: { + publisher: 'Microsoft.EnterpriseCloud.Monitoring' + type: 'MicrosoftMonitoringAgent' + typeHandlerVersion: '1.0' + autoUpgradeMinorVersion: true + settings: { + workspaceId: reference(workspaceId, '2021-06-01').customerId + stopOnMultipleConnections: false + } + protectedSettings: { + workspaceKey: listKeys(workspaceId, '2021-06-01').primarySharedKey + } + } +} + +@sys.description('Configure the Dependency Agent for the VM.') +resource dependencyAgentWindows 'Microsoft.Compute/virtualMachines/extensions@2023-03-01' = { + parent: vm + name: 'DependencyAgentWindows' + location: location + properties: { + autoUpgradeMinorVersion: true + enableAutomaticUpgrade: true + publisher: 'Microsoft.Azure.Monitoring.DependencyAgent' + type: 'DependencyAgentWindows' + typeHandlerVersion: '9.10' + } +} + +@sys.description('Configure the Network Watcher Agent for the VM.') +resource networkWatcherExtension 'Microsoft.Compute/virtualMachines/extensions@2023-03-01' = if (useNetworkWatcher) { + parent: vm + name: 'AzureNetworkWatcherExtension' + location: location + properties: { + autoUpgradeMinorVersion: true + publisher: 'Microsoft.Azure.NetworkWatcher' + type: 'NetworkWatcherAgentWindows' + typeHandlerVersion: '1.4' + } +} + +@sys.description('Configure the Azure Policy Guest Configuration Agent for the VM.') +resource azurePolicyExtension 'Microsoft.Compute/virtualMachines/extensions@2023-03-01' = if (useAzurePolicy) { + parent: vm + name: 'AzurePolicyforWindows' + location: location + properties: { + autoUpgradeMinorVersion: true + enableAutomaticUpgrade: true + publisher: 'Microsoft.GuestConfiguration' + type: 'ConfigurationforWindows' + typeHandlerVersion: '1.1' + } +} + +@sys.description('Configure the SQL IaaS Agent for the VM.') +resource sqlServerExtension 'Microsoft.Compute/virtualMachines/extensions@2023-03-01' = if (useSqlServer) { + parent: vm + name: 'SqlIaasExtension' + location: location + properties: { + autoUpgradeMinorVersion: true + publisher: 'Microsoft.SqlServer.Management' + type: 'SqlIaaSAgent' + typeHandlerVersion: '2.0' + settings: { + sqlServerLicenseType: 'PAYG' + } + } +} + +// ------- +// OUTPUTS +// ------- + +@sys.description('A unique identifier for the VM.') +output id string = vm.id + +@sys.description('The name of the VM.') +output name string = vm.name diff --git a/modules/virtual-machine-windows/v1/.tests/main.tests.bicep b/modules/virtual-machine-windows/v1/.tests/main.tests.bicep new file mode 100644 index 0000000..b65fbd4 --- /dev/null +++ b/modules/virtual-machine-windows/v1/.tests/main.tests.bicep @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Test Windows Virtual Machine module +targetScope = 'resourceGroup' + +// ---------- +// REFERENCES +// ---------- + +resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = { + name: 'kv-001' +} + +resource vnet 'Microsoft.Network/virtualNetworks@2021-05-01' existing = { + name: 'vnet-001' +} + +resource subnet 'Microsoft.Network/virtualNetworks/subnets@2021-05-01' existing = { + parent: vnet + name: 'subnet-001' +} + +// --------- +// RESOURCES +// --------- + +// Test a basic VM +module test_vm_with_no_disks '../main.bicep' = { + name: 'test_vm_with_no_disks' + params: { + name: 'vm001' + adminUsername: kv.getSecret('vm-username') + adminPassword: kv.getSecret('vm-password') + subnetId: subnet.id + size: 'Standard_D2s_v3' + imageSKU: '2022-Datacenter-Core' + tags: { + env: 'dev' + } + } +} + +// Test a VM with two data disks +module test_vm_with_data_disks '../main.bicep' = { + name: 'test_vm_with_data_disks' + params: { + name: 'vm002' + adminUsername: kv.getSecret('vm-username') + adminPassword: kv.getSecret('vm-password') + subnetId: subnet.id + size: 'Standard_D2s_v3' + imageSKU: '2022-Datacenter-Core' + dataDisks: [ + { + diskSizeGB: 32 + sku: 'Standard_LRS' + } + { + diskSizeGB: 64 + sku: 'Standard_LRS' + caching: 'None' + } + ] + tags: { + env: 'dev' + } + } +} diff --git a/modules/virtual-machine-windows/v1/main.bicep b/modules/virtual-machine-windows/v1/main.bicep new file mode 100644 index 0000000..89774c0 --- /dev/null +++ b/modules/virtual-machine-windows/v1/main.bicep @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Create or update a Virtual Machine +targetScope = 'resourceGroup' + +metadata name = 'Virtual Machine - Windows Server' +metadata description = 'Deploys and configures a Windows Server Virtual Machine. VM will automatically domain join and configure monitoring.' +metadata version = '1.0.0' + +// ---------- +// PARAMETERS +// ---------- + +@sys.description('The name of the virtual machine.') +param name string + +@metadata({ + strongType: 'location' + example: 'eastus' + ignore: true +}) +@sys.description('The Azure region to deploy to.') +param location string = resourceGroup().location + +@allowed([ + '2022-Datacenter-Core' + '2022-Datacenter' +]) +@sys.description('The operating system image to deploy.') +param imageSKU string + +@allowed([ + 'Standard_D2s_v3' + 'Standard_D2as_v4' + 'Standard_D2ds_v4' + 'Standard_D4s_v3' + 'Standard_D4as_v4' + 'Standard_D4ds_v4' + 'Standard_D8s_v3' + 'Standard_D8as_v4' + 'Standard_D8ds_v4' + 'Standard_E2s_v3' + 'Standard_E2as_v4' + 'Standard_E2ds_v4' + 'Standard_E4s_v3' + 'Standard_E4as_v4' + 'Standard_E4ds_v4' + 'Standard_E8s_v3' + 'Standard_E8as_v4' + 'Standard_E8ds_v4' + 'Standard_F2s_v2' + 'Standard_F4s_v2' + 'Standard_F8s_v2' + 'Standard_F16s_v2' + 'Standard_L8s_v2' + 'Standard_L16s_v2' +]) +@sys.description('The SKU of the virtual machine.') +param size string + +@metadata({ + strongType: 'Microsoft.ManagedIdentity/userAssignedIdentities' +}) +@sys.description('A User Assigned Managed Identity to configure.') +param identityId string = '' + +@metadata({ + strongType: 'Microsoft.Network/virtualNetworks/subnets' +}) +@sys.description('The subnet to connect the VM.') +param subnetId string + +@metadata({ + strongType: 'Microsoft.OperationalInsights/workspaces' + example: '/subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.OperationalInsights/workspaces/WORKSPACE_NAME' +}) +@sys.description('The workspace to store monitoring data.') +param workspaceId string = '' + +@sys.description('The maintenance configuration to assign.') +param maintenanceConfigurationId string = '' + +@secure() +@sys.description('The default administrator username for the VM.') +param adminUsername string + +@secure() +@sys.description('The default administrator password for the VM.') +param adminPassword string + +@allowed([ + 'Empty' + 'Attach' +]) +@sys.description('Attach existing disks or create empty disks.') +param diskCreateOption string = 'Empty' + +@metadata({ + example: [ + { + diskSizeGB: 32 + sku: 'Standard_LRS' + caching: 'ReadOnly' + } + ] +}) +@sys.description('An array of managed disks to create or attach.') +param dataDisks array = [] + +@sys.description('Determines if accelerated networking is enabled for the VM.') +param useAcceleratedNetworking bool = false + +@sys.description('Determines if Network Watcher extension is configured for the VM.') +param useNetworkWatcher bool = false + +@sys.description('Determines if Azure Policy Guest Configuration extension is configured for the VM.') +param useAzurePolicy bool = false + +@sys.description('Determines if SQL Server IaaS extension is configured for the VM.') +param useSqlServer bool = false + +@metadata({ + example: [ + { + principalId: 'OBJECT_ID' + description: 'DESCRIPTION' + principalType: 'Group' + role: 'Contributor' + } + ] +}) +@sys.description('A list of additional role assignments for the Virtual Machine.') +param assignments array = [] + +@sys.description('Tags to apply to the resource.') +param tags object = resourceGroup().tags + +// --------- +// VARIABLES +// --------- + +// Get the publisher and offer details for the specific VM image to use +var imagePublisher = 'MicrosoftWindowsServer' +var imageOffer = 'WindowsServer' + +// --------- +// RESOURCES +// --------- + +@sys.description('Create or update a Virtual Machine (VM).') +module vm '.bicep/vm.bicep' = { + name: 'vm-${name}' + params: { + name: name + location: location + size: size + imagePublisher: imagePublisher + imageOffer: imageOffer + imageSKU: imageSKU + identityId: identityId + subnetId: subnetId + workspaceId: workspaceId + maintenanceConfigurationId: maintenanceConfigurationId + adminUsername: adminUsername + adminPassword: adminPassword + diskCreateOption: diskCreateOption + dataDisks: dataDisks + useAcceleratedNetworking: useAcceleratedNetworking + useNetworkWatcher: useNetworkWatcher + useAzurePolicy: useAzurePolicy + useSqlServer: useSqlServer + tags: tags + } +} + +@sys.description('Create or update role assignments for the Virtual Machine.') +module rbac '.bicep/rbac.bicep' = [ + for assignment in assignments: { + name: 'assignment-${uniqueString(resourceId('Microsoft.Compute/virtualMachines', name), assignment.principalId, assignment.role)}' + params: { + principalId: assignment.principalId + description: assignment.?description ?? '' + principalType: assignment.principalType + role: assignment.role + resource: vm.outputs.name + } + } +] + +// ------- +// OUTPUTS +// ------- + +@sys.description('A unique identifier for the VM.') +output id string = vm.outputs.id + +@sys.description('The name of the VM.') +output name string = name + +@sys.description('The name of the Resource Group where the VM is deployed.') +output resourceGroupName string = resourceGroup().name + +@sys.description('The guid for the subscription where the VM is deployed.') +output subscriptionId string = subscription().subscriptionId diff --git a/ps-rule.yaml b/ps-rule.yaml index 29d47fc..a42c99d 100644 --- a/ps-rule.yaml +++ b/ps-rule.yaml @@ -73,12 +73,25 @@ configuration: # Configure the minimum version of the Bicep CLI. AZURE_BICEP_MINIMUM_VERSION: '0.25.53' + AZURE_PARAMETER_DEFAULTS: + adminPassword: $CREDENTIAL_PLACEHOLDER$ + AZURE_DEPLOYMENT_NONSENSITIVE_PARAMETER_NAMES: - keys + AZURE_RESOURCE_GROUP: + tags: {} + # Suppression ignores rules for a specific Azure resource by name. suppression: Azure.KeyVault.Logs: - kvtest001 Azure.Storage.BlobPublicAccess: - sttest001 + +# Disable the following rules by name. +rule: + exclude: + - Azure.VM.AMA + - Azure.VM.MaintenanceConfig + - Azure.VM.MigrateAMA