diff --git a/src/bicep/examples/README.md b/src/bicep/examples/README.md index 8b6e8bd95..640e57323 100644 --- a/src/bicep/examples/README.md +++ b/src/bicep/examples/README.md @@ -15,6 +15,7 @@ Example | Description [KeyVault](./key-vault/) | Deploys a premium Azure Key Vault with RBAC enabled to support secret, key, and certificate management. [Azure Sentinel](./sentinel) | A Terraform module that adds an Azure Sentinel solution to a Log Analytics Workspace. Sentinel can also be deployed via bicep and the base deployment of mlz.bicep by using the boolean param '-deploySentinel'. [Zero Trust (TIC3.0) Workbook](./zero-trust-workbook) | Deploys an Azure Sentinel Zero Trust (TIC3.0) Workbook +[IaaS DNS Forwarders](./iaas-dns-forwarders) | Deploys DNS Forwarder Virtual Machines in the HUB, for proper resolution of Private Endpoint and internal domains accross all Virtual Networks ## Shared Variable File Pattern (deploymentVariables.json) diff --git a/src/bicep/examples/iaas-dns-forwarders/README.md b/src/bicep/examples/iaas-dns-forwarders/README.md new file mode 100644 index 000000000..6fddafcc6 --- /dev/null +++ b/src/bicep/examples/iaas-dns-forwarders/README.md @@ -0,0 +1,78 @@ +# Azure IaaS DNS Forwarders example + +This example deploys DNS Forwarder Virtual Machines in the MLZ HUB, to enables proper resolution of Private Endpoint and internal domains accross all Virtual Networks. + +## What this example does + +### Follows best-practices + +This Infrastructure as Code deploys the components to follow best practices: [Private Link and DNS integration in hub and spoke network architectures](https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/private-link-and-dns-integration-at-scale#private-link-and-dns-integration-in-hub-and-spoke-network-architectures) + +### Configures proper DNS resolution in DoD Azure environments + +The two Windows DNS Servers are configured to act as DNS servers for all Virtual Networks, and then forward DNS requests three different ways, as depicted below. + +![DNS Forwarders diagram](diagram.png) + +1. Azure Private Endpoint-related DNS requests get forwarded to the Azure DNS server (168.63.129.16), which uses the Private DNS zones configured as part of MLZ. +2. Active Directory-related DNS requests get forwarded to the Domain Controllers in the Identity tier. +3. All other DNS requests (Internet...) get forwarded to the default server forwarder, typically DISA DNS servers. + +## Pre-requisites + +1. A Mission LZ deployment (a deployment of mlz.bicep) +2. The outputs from a deployment of mlz.bicep (./src/bicep/examples/deploymentVariables.json). + +See below for information on how to create the appropriate deployment variables file for use with this template. + +### Template Parameters + +Template Parameters Name | Description +--- | --- +vmNamePrefix | 3 to 12 characters VM name prefix. -01 and -02 will get appended to that prefix. +vmAdminPassword | local administrator password. +nicPrivateIPAddresses | array of two static IP addresses available in the HUB VNET subnet. +extensionsFilesContainerUri | uri to the storage account used to host the DSC configuration and custom script file (if not relying on the public repo) +extensionsFilesContainerSas | storage account account SAS token used to host the DSC configuration and custom script file (if not relying on the public repo) +dnsServerForwardersIpAddresses | default DNS server forwarders (for instance: DISA's). Defaults to Azure DNS. +conditionalDnsServerForwarders | array of conditional forwarders to create, including Azure Private DNS zones and Active Directory-related zones. Defaults to Azure US Government's private endpoint DNS zones. + +### Generate MLZ Variable File (deploymentVariables.json) + +For instructions on generating 'deploymentVariables.json' using both Azure PowerShell and Azure CLI, please see the [README at the root of the examples folder](..\README.md). + +Place the resulting 'deploymentVariables.json' file within the ./src/bicep/examples folder. + +### Deploying IaaS DNS Forwarders + +Connect to the appropriate Azure Environment and set appropriate context, see getting started with Azure PowerShell for help if needed. The commands below assume you are deploying in Azure Government and show the entire process from deploying MLZ and then adding DNS forwarders post-deployment. + +```PowerShell +cd .\src\bicep +Connect-AzAccount -Environment AzureUSGovernment +New-AzSubscriptionDeployment -Name contoso -TemplateFile .\mlz.bicep -resourcePrefix 'contoso' -Location 'USGovVirginia' +cd .\examples +(Get-AzSubscriptionDeployment -Name contoso).outputs | ConvertTo-Json | Out-File -FilePath .\deploymentVariables.json +cd .\keyVault +New-AzResourceGroupDeployment -DeploymentName IaaSDNSForwarders ` + -TemplateFile .\forwarderVm.bicep + -ResourceGroupName 'contoso-rg-hub-mlz' + -vmNamePrefix 'contoso-dnsfwd' + -nicPrivateIPAddresses "10.9.0.4", "10.9.0.5" + -dnsServerForwardersIpAddresses "2.2.2.2" +``` + +### Setting all Virtual Networks to use the Forwarders as DNS servers + +The PowerShell script below configures the (existing) Virtual Networks to use the DNS Forwarders as DNS servers. + +```PowerShell +$dnsForwarders = "10.9.0.4", "10.9.0.5" +$virtualNetworks = Get-AzVirtualNetwork +foreach($vnet in $virtualnetworks){ + Write-Output ("Changing VNET " + [char]34 + $vnet.Name + [char]34 + " DNS Servers...") + $vnet.DhcpOptions.DnsServers = $dnsForwarders + $vnetSave = $vnet | Set-AzVirtualNetwork +} +``` + diff --git a/src/bicep/examples/iaas-dns-forwarders/diagram.png b/src/bicep/examples/iaas-dns-forwarders/diagram.png new file mode 100644 index 000000000..527450e1b Binary files /dev/null and b/src/bicep/examples/iaas-dns-forwarders/diagram.png differ diff --git a/src/bicep/examples/iaas-dns-forwarders/extensions/Set-ConditionalDnsForwarders.ps1 b/src/bicep/examples/iaas-dns-forwarders/extensions/Set-ConditionalDnsForwarders.ps1 new file mode 100644 index 000000000..95b66f939 --- /dev/null +++ b/src/bicep/examples/iaas-dns-forwarders/extensions/Set-ConditionalDnsForwarders.ps1 @@ -0,0 +1,30 @@ +# Script to set Windows DNS Server Conditional DNS Forwarders +[CmdletBinding()] +param ( + [Parameter(Position = 0, + HelpMessage = "JSON Array of DNS Conditional Forwarders to configure", + Mandatory = $true)] + [String] + $ConditionalDnsForwardersJSON +) + +# Convert from JSON +$conditionalDnsForwarders = $ConditionalDnsForwardersJSON | ConvertFrom-Json +if (!($conditionalDnsForwarders)) { throw("Could not convert from JSON:`r`n" + $ConditionalDnsForwardersJSON) } + +# Loop through conditional forwarders +foreach ($conditionalDnsForwarder in $conditionalDnsForwarders) { + # Get existing zone + $existingZone = Get-DnsServerZone -Name $conditionalDnsForwarder.Name -ErrorAction:SilentlyContinue + if ($existingZone) {Write-Output ("Found existing zone for " + [char]34 + $conditionalDnsForwarder.Name + [char]34 + "...")} + # Create conditional forwarder + else { + Add-DnsServerConditionalForwarderZone -Name $conditionalDnsForwarder.Name -MasterServers $conditionalDnsForwarder.Forwarders + # Verify forwarder was created + $fwdGet = Get-DnsServerZone -Name $conditionalDnsForwarder.Name -ErrorAction:SilentlyContinue + if ($fwdGet) { + Write-Output ([char]34 + $conditionalDnsForwarder.Name + [char]34 + " was successfully created...") + } + else{throw("Could not create Conditional DNS Forwarder for name " + [char]34 + $conditionalDnsForwarder.Name + [char]34 + ".")} + } +} \ No newline at end of file diff --git a/src/bicep/examples/iaas-dns-forwarders/extensions/dnsForwarding.ps1.zip b/src/bicep/examples/iaas-dns-forwarders/extensions/dnsForwarding.ps1.zip new file mode 100644 index 000000000..0bec15525 Binary files /dev/null and b/src/bicep/examples/iaas-dns-forwarders/extensions/dnsForwarding.ps1.zip differ diff --git a/src/bicep/examples/iaas-dns-forwarders/forwarderVm.bicep b/src/bicep/examples/iaas-dns-forwarders/forwarderVm.bicep new file mode 100644 index 000000000..a74bc7fa2 --- /dev/null +++ b/src/bicep/examples/iaas-dns-forwarders/forwarderVm.bicep @@ -0,0 +1,318 @@ +@description('MLZ Deployment output variables in json format. It defaults to the deploymentVariables.json.') +param mlzDeploymentVariables object = json(loadTextContent('../deploymentVariables.json')) +param hubVirtualNetworkSubnetId string = mlzDeploymentVariables.hub.Value.subnetResourceId +param logAnalyticsWorkspaceResourceId string = mlzDeploymentVariables.logAnalyticsWorkspaceResourceId.Value + +@description('The region to deploy resources into. It defaults to the deployment location.') +param location string = resourceGroup().location + +@description('A string dictionary of tags to add to deployed resources. See https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources?tabs=json#arm-templates for valid settings.') +param tags object = {} + +@description('Prefix the VM names will start with.') +param vmNamePrefix string + +@description('Number of VM to build.') +param vmCount int = 2 + +@description('The administrator username for the Virtual Machine to remote into.') +param vmAdminUsername string = 'azureuser' + +@description('The administrator password the Virtual Machine to remote into. It must be > 12 characters in length. See https://docs.microsoft.com/en-us/azure/virtual-machines/windows/faq#what-are-the-password-requirements-when-creating-a-vm- for password requirements.') +@secure() +@minLength(12) +param vmAdminPassword string + +@description('The size of the Virtual Machine. It defaults to "Standard_DS1_v2".') +param vmSize string = 'Standard_DS1_v2' + +@description('The publisher of the Virtual Machine. It defaults to "MicrosoftWindowsServer".') +param vmPublisher string = 'MicrosoftWindowsServer' + +@description('The offer of the Virtual Machine. It defaults to "WindowsServer".') +param vmOffer string = 'WindowsServer' + +@description('The SKU of the Virtual Machine. It defaults to "2022-datacenter".') +param vmSku string = '2022-datacenter' + +@description('The version of the Virtual Machine. It defaults to "latest".') +param vmVersion string = 'latest' + +@description('The disk creation option of the Virtual Machine. It defaults to "FromImage".') +param vmCreateOption string = 'FromImage' + +@description('The storage account type of the Virtual Machine. It defaults to "StandardSSD_LRS".') +param vmStorageAccountType string = 'StandardSSD_LRS' + +@allowed([ + 'Static' + 'Dynamic' +]) +@description('[Static/Dynamic] The private IP Address allocation method for the Virtual Machine. It defaults to "Dynamic".') +param nicPrivateIPAddressAllocationMethod string = 'Dynamic' + +@description('Array of static private IP addresses for the VM Network Interface Cards') +param nicPrivateIPAddresses array = [] + +@description('Uri to the container that contains the DSC configuration and the Custom Script') +param extensionsFilesContainerUri string = 'https://raw.githubusercontent.com/Azure/missionlz/main/src/bicep/examples/iaas-dns-forwarders/extensions' + +@description('SAS Token to access the container that contains the DSC configuration and the Custom Script. Defaults to none for a public container') +@secure() +param extensionsFilesContainerSas string = '' + +@description('DSC Configurations Name') +param dscConfigName string = 'dnsForwarding' + +@description('DNS Server Forwarders IP Addresses that get configured in the Windows DNS server. Defaults to Azure DNS servers.') +param dnsServerForwardersIpAddresses array = ['168.63.129.16'] + +@description('Custom Script file name') +param customScriptName string ='Set-ConditionalDnsForwarders.ps1' + +@description('Custom Conditional DNS Forwarders that get configured in the Windows DNS server. Defaults to AzureUSGov Private Endpoint DNS Zones.') +param conditionalDnsServerForwarders array = [ + { + Name: 'azure-automation.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'database.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'blob.core.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'table.core.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'queue.core.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'file.core.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'web.core.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'documents.azure.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'batch.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'service.batch.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'postgres.database.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'mysql.database.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'mariadb.database.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'vault.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'vaultcore.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'search.windows.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'azconfig.azure.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'backup.windowsazure.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'siterecovery.windowsazure.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'servicebus.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'servicebus.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'azure-devices.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'servicebus.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'azurewebsites.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'adx.monitor.azure.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'oms.opinsights.azure.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'ods.opinsights.azure.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'agentsvc.azure-automation.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'cognitiveservices.azure.us' + Forwarders: ['168.63.129.16'] + } + { + Name: 'redis.cache.usgovcloudapi.net' + Forwarders: ['168.63.129.16'] + } + { + Name: 'azurehdinsight.us' + Forwarders: ['168.63.129.16'] + } +] + +var vmAvSetName = '${vmNamePrefix}-avset-01' +var NetworkInterfaceIpConfigurationName = 'ipConfiguration1' +var sasToken = ((extensionsFilesContainerSas != '') ? '?${extensionsFilesContainerSas}' : null) + +resource vmAvSet 'Microsoft.Compute/availabilitySets@2022-03-01' = { + name: vmAvSetName + location: location + tags: tags + sku: { + name: 'Aligned' + } + properties: { + platformUpdateDomainCount: 2 + platformFaultDomainCount: 2 + } +} + +resource networkInterface 'Microsoft.Network/networkInterfaces@2021-02-01' = [for i in range(0, vmCount): { + name: '${vmNamePrefix}-0${(i + 1)}-nic-01' + location: location + tags: tags + + properties: { + ipConfigurations: [ + { + name: NetworkInterfaceIpConfigurationName + properties: { + subnet: { + id: hubVirtualNetworkSubnetId + } + privateIPAllocationMethod: nicPrivateIPAddressAllocationMethod + privateIPAddress: ((nicPrivateIPAddressAllocationMethod == 'Static') ? nicPrivateIPAddresses[i] : null) + } + } + ] + } +}] + +module dnsForwarderVirtualMachine '../../modules/windows-virtual-machine.bicep' = [for vmi in range(0, vmCount): { + name: 'dnsForwarderVirtualMachines${(vmi + 1)}' + params: { + name: '${vmNamePrefix}-0${(vmi + 1)}' + location: location + tags: tags + + size: vmSize + adminUsername: vmAdminUsername + adminPassword: vmAdminPassword + publisher: vmPublisher + offer: vmOffer + sku: vmSku + version: vmVersion + createOption: vmCreateOption + storageAccountType: vmStorageAccountType + networkInterfaceName: '${vmNamePrefix}-0${(vmi + 1)}-nic-01' + logAnalyticsWorkspaceId: logAnalyticsWorkspaceResourceId + availabilitySet: { + id: vmAvSet.id + } + } + dependsOn: [ + networkInterface + ] +}] + +resource dnsForwarderVirtualMachines 'Microsoft.Compute/virtualMachines@2022-03-01' existing = [for i in range(0, vmCount): { + name: '${vmNamePrefix}-0${(i + 1)}' +}] + +resource DSC 'Microsoft.Compute/virtualMachines/extensions@2021-03-01' = [for i in range(0, vmCount): { + name: 'Microsoft.Powershell.DSC' + parent: dnsForwarderVirtualMachines[i] + location: location + properties: { + publisher: 'Microsoft.Powershell' + type: 'DSC' + typeHandlerVersion: '2.24' + autoUpgradeMinorVersion: true + settings: { + wmfVersion: 'latest' + configuration: { + url: '${extensionsFilesContainerUri}/${dscConfigName}.ps1.zip${sasToken}' + script: '${dscConfigName}.ps1' + function: dscConfigName + } + configurationArguments: { + dnsServerForwarders: dnsServerForwardersIpAddresses + } + } + } + dependsOn: [ + dnsForwarderVirtualMachine + ] +}] + +resource customScript 'Microsoft.Compute/virtualMachines/extensions@2022-03-01' = [for i in range(0, vmCount): if(conditionalDnsServerForwarders != []){ + name: 'CustomScriptExt' + location: location + parent: dnsForwarderVirtualMachines[i] + dependsOn: [ + DSC + ] + properties: { + autoUpgradeMinorVersion: true + enableAutomaticUpgrade: false + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + settings: { + commandToExecute: 'Powershell.exe -ExecutionPolicy Unrestricted -File ${customScriptName} -conditionalDnsForwardersJSON ${conditionalDnsServerForwarders}' + } + protectedSettings: { + fileUris: [ + '${extensionsFilesContainerUri}/${customScriptName}${sasToken}' + ] + } + } +}] diff --git a/src/bicep/modules/windows-virtual-machine.bicep b/src/bicep/modules/windows-virtual-machine.bicep index 9f221a3d7..14bec0036 100644 --- a/src/bicep/modules/windows-virtual-machine.bicep +++ b/src/bicep/modules/windows-virtual-machine.bicep @@ -21,6 +21,7 @@ param version string param createOption string param storageAccountType string param logAnalyticsWorkspaceId string +param availabilitySet object = {} resource networkInterface 'Microsoft.Network/networkInterfaces@2021-02-01' existing = { name: networkInterfaceName @@ -32,6 +33,7 @@ resource windowsVirtualMachine 'Microsoft.Compute/virtualMachines@2021-04-01' = tags: tags properties: { + availabilitySet: ((availabilitySet != {}) ? availabilitySet : null) hardwareProfile: { vmSize: size }