diff --git a/solutions/secure-hybrid-network/README.md b/solutions/secure-hybrid-network/README.md index f2e7f625..38f5e7e0 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, password, and VPN shared key. These values are used to log into the included virtual machines and establish the site-to-site VPN connection. ```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 mockOnPremResourceGroup=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** @@ -107,6 +118,42 @@ az deployment sub create -n secure-hybrid-network --location eastus2 --template- | sharedKey | securestring | The shared key for the VPN connection. | null | | 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..37fc6b9c --- /dev/null +++ b/solutions/secure-hybrid-network/nestedtemplates/azure-network-azuredeploy-v2.bicep @@ -0,0 +1,137 @@ +// 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: 'all-internet' + protocols: [ + { + protocolType: 'Http' + port: 80 + } + { + protocolType: 'Https' + port: 443 + } + ] + targetFqdns: [ + '*' + ] + sourceAddresses: [ + '*' + ] + } + ] + } + } + { + name: 'spoke-windows-update' + properties: { + priority: 200 + action: { + type: 'Allow' + } + rules: [ + { + name: 'windows-update' + fqdnTags: [ + 'WindowsUpdate' + ] + 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..0bf4c942 --- /dev/null +++ b/solutions/secure-hybrid-network/nestedtemplates/azure-network-azuredeploy-v2.json @@ -0,0 +1,166 @@ +{ + "$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": "all-internet", + "protocols": [ + { + "protocolType": "Http", + "port": 80 + }, + { + "protocolType": "Https", + "port": 443 + } + ], + "targetFqdns": [ + "*" + ], + "sourceAddresses": [ + "*" + ] + } + ] + } + }, + { + "name": "spoke-windows-update", + "properties": { + "priority": 200, + "action": { + "type": "Allow" + }, + "rules": [ + { + "name": "windows-update", + "fqdnTags": [ + "WindowsUpdate" + ], + "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