diff --git a/solutions/secure-hybrid-network/README.md b/solutions/secure-hybrid-network/README.md index efb31f4b..8cd2ec97 100644 --- a/solutions/secure-hybrid-network/README.md +++ b/solutions/secure-hybrid-network/README.md @@ -36,10 +36,21 @@ cd samples/solutions/secure-hybrid-network Run the following commands to initiate the deployment. When prompted, enter values for an admin username and password. These values are used to log into the included virtual machines. ```azurecli-interactive -# Resources will be created on deployment region +# Deploy the base infrastructure: hub, spoke, firewall, VPN, and VMSS workload az deployment sub create -n secure-hybrid-network --location eastus2 --template-file azuredeploy.bicep -p mocOnPremResourceGroup=rg-site-to-site-mock-prem-eastus2 azureNetworkResourceGroup=rg-site-to-site-azure-network-eastus2 ``` +Now that the on-premises site has joined the network, update the hub firewall with a DNAT rule so it can reach the spoke workloads: + +```azurecli-interactive +# Get the firewall and load balancer private IPs +FW_IP=$(az network firewall show -g rg-site-to-site-azure-network-eastus2 -n AzureFirewall --query "ipConfigurations[0].privateIPAddress" -o tsv) +LB_IP=$(az network lb frontend-ip list -g rg-site-to-site-azure-network-eastus2 --lb-name lb-internal --query "[0].privateIPAddress" -o tsv) + +# Add DNAT rules for on-premises to spoke traffic +az deployment group create -n firewallDnat -g rg-site-to-site-azure-network-eastus2 --template-file nestedtemplates/azure-network-azuredeploy-v2.bicep -p firewallName=AzureFirewall firewallPrivateIp=$FW_IP internalLoadBalancerPrivateIp=$LB_IP +``` + ## Solution deployment parameters **azuredeploy.bicep** @@ -105,6 +116,42 @@ az deployment sub create -n secure-hybrid-network --location eastus2 --template- | localNetworkGateway | string | Name of the mock on-prem local network gateway. | local-gateway-moc-prem | | location | string | Location to be used for all resources. | rg location | +**nestedtemplates/azure-network-azuredeploy-v2.bicep** + +| Parameter | Type | Description | Default | +|---|---|---|--| +| firewallName | string | Name of the Azure Firewall. | null | +| firewallPrivateIp | string | Private IP address of the firewall. | null | +| internalLoadBalancerPrivateIp | string | Private IP address of the internal load balancer. | null | +| location | string | Location for the resource. | rg location | + +## Validate deployment + +After the deployment completes, verify end-to-end connectivity by accessing the IIS web server from the mock on-premises VM through the VPN tunnel. Traffic flows through the Azure Firewall via a DNAT rule that translates requests to the internal load balancer. + +### Option 1: Azure Bastion + +Connect to the mock on-premises virtual machine using the included Azure Bastion host, open a web browser, and navigate to the Azure Firewall's private IP address (`http://`). The firewall translates the request to the application's internal load balancer. + +### Option 2: CLI + +```azurecli-interactive +# Get the Azure Firewall private IP (DNAT entry point) +FW_IP=$(az network firewall show \ + -g rg-site-to-site-azure-network-eastus2 \ + -n AzureFirewall \ + --query "ipConfigurations[0].privateIPAddress" -o tsv) + +# From the mock on-prem VM, reach IIS through the VPN tunnel via firewall DNAT +az vm run-command invoke \ + -g rg-site-to-site-mock-prem-eastus2 \ + -n vm-windows \ + --command-id RunPowerShellScript \ + --scripts "Invoke-WebRequest -Uri http://$FW_IP -UseBasicParsing | Select-Object -Property StatusCode" +``` + +A successful response returns `StatusCode: 200`, confirming the full path: on-prem VM → VPN → hub → firewall (DNAT) → spoke → load balancer → VMSS (IIS). + ## Clean Up ```azurecli-interactive diff --git a/solutions/secure-hybrid-network/nestedtemplates/azure-network-azuredeploy-v2.bicep b/solutions/secure-hybrid-network/nestedtemplates/azure-network-azuredeploy-v2.bicep new file mode 100644 index 00000000..26089079 --- /dev/null +++ b/solutions/secure-hybrid-network/nestedtemplates/azure-network-azuredeploy-v2.bicep @@ -0,0 +1,115 @@ +// Once an on-premises site joins the network, this template updates the hub +// firewall with a DNAT rule so inbound traffic can reach the spoke workloads. + +param location string = resourceGroup().location + +@description('Name of the Azure Firewall') +param firewallName string + +@description('Private IP address of the firewall') +param firewallPrivateIp string + +@description('Private IP address of the internal load balancer') +param internalLoadBalancerPrivateIp string + +@description('Name of the hub virtual network') +param hubVnetName string = 'vnet-hub' + +@description('Name of the firewall public IP') +param firewallPublicIpName string = 'pip-firewall' + +@description('Spoke network address prefix for source filtering') +param spokeAddressPrefix string = '10.100.0.0/16' + +resource hubVnet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { + name: hubVnetName +} + +resource firewallPublicIp 'Microsoft.Network/publicIPAddresses@2024-05-01' existing = { + name: firewallPublicIpName +} + +resource firewallDnat 'Microsoft.Network/azureFirewalls@2024-05-01' = { + name: firewallName + location: location + properties: { + sku: { + name: 'AZFW_VNet' + tier: 'Standard' + } + threatIntelMode: 'Alert' + ipConfigurations: [ + { + name: firewallName + properties: { + publicIPAddress: { + id: firewallPublicIp.id + } + subnet: { + id: resourceId('Microsoft.Network/virtualNetworks/subnets', hubVnet.name, 'AzureFirewallSubnet') + } + } + } + ] + applicationRuleCollections: [ + { + name: 'spoke-outbound' + properties: { + priority: 100 + action: { + type: 'Allow' + } + rules: [ + { + name: 'windows-update' + protocols: [ + { + protocolType: 'Https' + port: 443 + } + ] + targetFqdns: [ + '*.update.microsoft.com' + '*.windowsupdate.com' + '*.download.windowsupdate.com' + ] + sourceAddresses: [ + spokeAddressPrefix + ] + } + ] + } + } + ] + natRuleCollections: [ + { + name: 'dnat-onprem-to-spoke' + properties: { + priority: 100 + action: { + type: 'Dnat' + } + rules: [ + { + name: 'onprem-to-web' + protocols: [ + 'TCP' + ] + sourceAddresses: [ + '192.168.0.0/16' + ] + destinationAddresses: [ + firewallPrivateIp + ] + destinationPorts: [ + '80' + ] + translatedAddress: internalLoadBalancerPrivateIp + translatedPort: '80' + } + ] + } + } + ] + } +} diff --git a/solutions/secure-hybrid-network/nestedtemplates/azure-network-azuredeploy-v2.json b/solutions/secure-hybrid-network/nestedtemplates/azure-network-azuredeploy-v2.json new file mode 100644 index 00000000..3a105078 --- /dev/null +++ b/solutions/secure-hybrid-network/nestedtemplates/azure-network-azuredeploy-v2.json @@ -0,0 +1,144 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "11434533286408150086" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "firewallName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Firewall" + } + }, + "firewallPrivateIp": { + "type": "string", + "metadata": { + "description": "Private IP address of the firewall" + } + }, + "internalLoadBalancerPrivateIp": { + "type": "string", + "metadata": { + "description": "Private IP address of the internal load balancer" + } + }, + "hubVnetName": { + "type": "string", + "defaultValue": "vnet-hub", + "metadata": { + "description": "Name of the hub virtual network" + } + }, + "firewallPublicIpName": { + "type": "string", + "defaultValue": "pip-firewall", + "metadata": { + "description": "Name of the firewall public IP" + } + }, + "spokeAddressPrefix": { + "type": "string", + "defaultValue": "10.100.0.0/16", + "metadata": { + "description": "Spoke network address prefix for source filtering" + } + } + }, + "resources": [ + { + "type": "Microsoft.Network/azureFirewalls", + "apiVersion": "2024-05-01", + "name": "[parameters('firewallName')]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "AZFW_VNet", + "tier": "Standard" + }, + "threatIntelMode": "Alert", + "ipConfigurations": [ + { + "name": "[parameters('firewallName')]", + "properties": { + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('firewallPublicIpName'))]" + }, + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('hubVnetName'), 'AzureFirewallSubnet')]" + } + } + } + ], + "applicationRuleCollections": [ + { + "name": "spoke-outbound", + "properties": { + "priority": 100, + "action": { + "type": "Allow" + }, + "rules": [ + { + "name": "windows-update", + "protocols": [ + { + "protocolType": "Https", + "port": 443 + } + ], + "targetFqdns": [ + "*.update.microsoft.com", + "*.windowsupdate.com", + "*.download.windowsupdate.com" + ], + "sourceAddresses": [ + "[parameters('spokeAddressPrefix')]" + ] + } + ] + } + } + ], + "natRuleCollections": [ + { + "name": "dnat-onprem-to-spoke", + "properties": { + "priority": 100, + "action": { + "type": "Dnat" + }, + "rules": [ + { + "name": "onprem-to-web", + "protocols": [ + "TCP" + ], + "sourceAddresses": [ + "192.168.0.0/16" + ], + "destinationAddresses": [ + "[parameters('firewallPrivateIp')]" + ], + "destinationPorts": [ + "80" + ], + "translatedAddress": "[parameters('internalLoadBalancerPrivateIp')]", + "translatedPort": "80" + } + ] + } + } + ] + } + } + ] +} \ No newline at end of file