diff --git a/035-HubAndSpoke/Coach/README.md b/035-HubAndSpoke/Coach/README.md index 7ab9ebfbed..ad42ff9e59 100644 --- a/035-HubAndSpoke/Coach/README.md +++ b/035-HubAndSpoke/Coach/README.md @@ -26,3 +26,17 @@ These topics are not covered, and you might want to introduce them along the way - Add an Application Gateway to the mix - Challenge 5: **[PaaS Networking](05-Paas.md)** - Integrate Azure Web Apps and Azure SQL Databases with your hub and spoke design + +## Deployment using Infrastructure-as-Code +The coaches solutions for this hack includes a deployment of the challenges written in Bicep. + +For coaches, the infrastructure deployed in the solution can provide a quick reference architecture/lab for an approach to implementing the challenges. + +For students, the automation presents a solution, which the students are generally expected to figure out on their own. However, there are a number of scenarios where providing a student with the Bicep files could be helpful: + +* Bringing a student up to speed with the rest of the cohort +* Enabling students to focus on the network aspects of the hack, versus manual infrastructure deployment (especially when they are struggling with a less-relevant aspect) +* Quickly bringing a cohort of students up to a specific challenge (for example, enabling data-focused students to work with PaaS services and Private Endpoints, without having had to manually deploy the underlying infrastructure) +* Providing examples to students looking to implement the hack with IaC + +**See [Bicep Solution Readme](./Solutions/bicep/README.md) for detail deployment process.** \ No newline at end of file diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/01-00-resourceGroups.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/01-00-resourceGroups.bicep new file mode 100644 index 0000000000..dba60c6b27 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/01-00-resourceGroups.bicep @@ -0,0 +1,23 @@ +param location string = 'eastus2' + +targetScope = 'subscription' +//hub resources +resource wthrghub 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: 'wth-rg-hub' + location: location +} + +resource wthrgspoke01 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: 'wth-rg-spoke1' + location: location +} + +resource wthrgspoke02 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: 'wth-rg-spoke2' + location: location +} + +resource wthrgonprem 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: 'wth-rg-onprem' + location: location +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/01-01-hub.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/01-01-hub.bicep new file mode 100644 index 0000000000..3bf55cffa0 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/01-01-hub.bicep @@ -0,0 +1,281 @@ +param location string = 'eastus2' +param hubVMUsername string = 'admin-wth' +@secure() +param vmPassword string + +targetScope = 'resourceGroup' +//hub resources + +resource wthhubvnet 'Microsoft.Network/virtualNetworks@2021-08-01' = { + name: 'wth-vnet-hub01' + location: location + properties: { + addressSpace: { + addressPrefixes: [ + '10.0.0.0/16' + ] + } + subnets: [ + { + name: 'GatewaySubnet' + properties: { + addressPrefix: '10.0.0.0/24' + routeTable: { + id: rtvnetgw.id + } + } + } + { + name: 'subnet-hubvms' + properties: { + addressPrefix: '10.0.10.0/24' + routeTable: { + id: rthubvms.id + } + networkSecurityGroup: { + id: nsghubvms.id + } + } + } + { + name: 'AzureFirewallSubnet' + properties: { + addressPrefix: '10.0.1.0/24' + } + } + ] + } +} + +resource wthhubgwpip01 'Microsoft.Network/publicIPAddresses@2022-01-01' = { + name: 'wth-pip-gw01' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +output pipgw1 string = wthhubgwpip01.properties.ipAddress + +resource wthhubgwpip02 'Microsoft.Network/publicIPAddresses@2022-01-01' = { + name: 'wth-pip-gw02' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +output pipgw2 string = wthhubgwpip02.properties.ipAddress + +resource wthhubvnetgw 'Microsoft.Network/virtualNetworkGateways@2022-01-01' = { + name: 'wth-vngw-hub01' + location: location + properties: { + activeActive: true + bgpSettings: { + asn: 65515 + } + enableBgp: true + gatewayType: 'Vpn' + vpnType: 'RouteBased' + vpnGatewayGeneration: 'Generation1' + sku: { + name: 'VpnGw1' + tier: 'VpnGw1' + } + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + publicIPAddress: { + id: wthhubgwpip01.id + } + subnet: { + id: '${wthhubvnet.id}/subnets/GatewaySubnet' + } + } + } + { + name: 'ipconfig2' + properties: { + publicIPAddress: { + id: wthhubgwpip02.id + } + subnet: { + id: '${wthhubvnet.id}/subnets/GatewaySubnet' + } + } + } + ] + } +} + +output wthhubvnetgwasn int = wthhubvnetgw.properties.bgpSettings.asn +output wthhubvnetgwprivateip1 string = wthhubvnetgw.properties.bgpSettings.bgpPeeringAddresses[0].defaultBgpIpAddresses[0] +output wthhubvnetgwprivateip2 string = wthhubvnetgw.properties.bgpSettings.bgpPeeringAddresses[1].defaultBgpIpAddresses[0] + +resource changerdpport 'Microsoft.Compute/virtualMachines/extensions@2022-03-01' = { + name: '${wthhubvm01.name}/wth-vmextn-changerdpport33899' + location: location + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + settings: { + /* + To generate encoded command in PowerShell: + + $s = @' + Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp\" -Name PortNumber -Value 33899 + New-NetFirewallRule -DisplayName "RDP 33899 TCP" -Direction Inbound -LocalPort 33899 -Protocol TCP -Action Allow + New-NetFirewallRule -DisplayName "RDP 33899 UDP" -Direction Inbound -LocalPort 33899 -Protocol UDP -Action Allow + Restart-Service -Name TermService -Force + + New-NetFirewallRule -DisplayName 'ICMPv4' -Direction Inbound -Action Allow -Protocol icmpv4 -Enabled True + '@ + $bytes = [System.Text.Encoding]::Unicode.GetBytes($s) + [convert]::ToBase64String($bytes) */ + commandToExecute: 'powershell.exe -ep bypass -encodedcommand UwBlAHQALQBJAHQAZQBtAFAAcgBvAHAAZQByAHQAeQAgAC0AUABhAHQAaAAgACIASABLAEwATQA6AFwAUwB5AHMAdABlAG0AXABDAHUAcgByAGUAbgB0AEMAbwBuAHQAcgBvAGwAUwBlAHQAXABDAG8AbgB0AHIAbwBsAFwAVABlAHIAbQBpAG4AYQBsACAAUwBlAHIAdgBlAHIAXABXAGkAbgBTAHQAYQB0AGkAbwBuAHMAXABSAEQAUAAtAFQAYwBwAFwAIgAgAC0ATgBhAG0AZQAgAFAAbwByAHQATgB1AG0AYgBlAHIAIAAtAFYAYQBsAHUAZQAgADMAMwA4ADkAOQANAAoATgBlAHcALQBOAGUAdABGAGkAcgBlAHcAYQBsAGwAUgB1AGwAZQAgAC0ARABpAHMAcABsAGEAeQBOAGEAbQBlACAAIgBSAEQAUAAgADMAMwA4ADkAOQAgAFQAQwBQACIAIAAtAEQAaQByAGUAYwB0AGkAbwBuACAASQBuAGIAbwB1AG4AZAAgAC0ATABvAGMAYQBsAFAAbwByAHQAIAAzADMAOAA5ADkAIAAtAFAAcgBvAHQAbwBjAG8AbAAgAFQAQwBQACAALQBBAGMAdABpAG8AbgAgAEEAbABsAG8AdwANAAoATgBlAHcALQBOAGUAdABGAGkAcgBlAHcAYQBsAGwAUgB1AGwAZQAgAC0ARABpAHMAcABsAGEAeQBOAGEAbQBlACAAIgBSAEQAUAAgADMAMwA4ADkAOQAgAFUARABQACIAIAAtAEQAaQByAGUAYwB0AGkAbwBuACAASQBuAGIAbwB1AG4AZAAgAC0ATABvAGMAYQBsAFAAbwByAHQAIAAzADMAOAA5ADkAIAAtAFAAcgBvAHQAbwBjAG8AbAAgAFUARABQACAALQBBAGMAdABpAG8AbgAgAEEAbABsAG8AdwANAAoAUgBlAHMAdABhAHIAdAAtAFMAZQByAHYAaQBjAGUAIAAtAE4AYQBtAGUAIABUAGUAcgBtAFMAZQByAHYAaQBjAGUAIAAtAEYAbwByAGMAZQANAAoADQAKAE4AZQB3AC0ATgBlAHQARgBpAHIAZQB3AGEAbABsAFIAdQBsAGUAIAAtAEQAaQBzAHAAbABhAHkATgBhAG0AZQAgACcASQBDAE0AUAB2ADQAJwAgAC0ARABpAHIAZQBjAHQAaQBvAG4AIABJAG4AYgBvAHUAbgBkACAALQBBAGMAdABpAG8AbgAgAEEAbABsAG8AdwAgAC0AUAByAG8AdABvAGMAbwBsACAAaQBjAG0AcAB2ADQAIAAtAEUAbgBhAGIAbABlAGQAIABUAHIAdQBlAA==' + } + } +} + +resource wthhubvmpip01 'Microsoft.Network/publicIPAddresses@2022-01-01' = { + name: 'wth-pip-hubvm01' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource wthhubvmnic 'Microsoft.Network/networkInterfaces@2022-01-01' = { + name: 'wth-nic-hubvm01' + location: location + properties: { + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + subnet: { + id: '${wthhubvnet.id}/subnets/subnet-hubvms' + } + privateIPAddress: '10.0.10.4' + publicIPAddress: { + id: wthhubvmpip01.id + } + } + } + ] + } +} + +resource wthhubvm01 'Microsoft.Compute/virtualMachines@2022-03-01' = { + name: 'wth-vm-hub01' + location: location + properties: { + hardwareProfile: { + vmSize: 'Standard_B2s' + } + storageProfile: { + imageReference: { + publisher: 'MicrosoftWindowsServer' + offer: 'WindowsServer' + sku: '2022-datacenter-azure-edition' + version: 'latest' + } + osDisk: { + osType: 'Windows' + name: 'wth-disk-vmhubos01' + createOption: 'FromImage' + caching: 'ReadWrite' + } + } + osProfile: { + computerName: 'vm-hub01' + adminUsername: hubVMUsername + adminPassword: vmPassword + windowsConfiguration: { + provisionVMAgent: true + enableAutomaticUpdates: true + } + } + networkProfile: { + networkInterfaces: [ + { + id: wthhubvmnic.id + } + ] + } + diagnosticsProfile: { + bootDiagnostics: { + enabled: true + } + } + licenseType: 'Windows_Server' + } +} + +resource rtvnetgw 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-hubgwsubnet' + location: location + properties: { + routes: [] + disableBgpRoutePropagation: false + } +} + +resource rthubvms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-hubvmssubnet' + location: location + properties: { + routes: [] + disableBgpRoutePropagation: false + } +} + +resource nsghubvms 'Microsoft.Network/networkSecurityGroups@2022-01-01' = { + name: 'wth-nsg-hubvmssubnet' + location: location + properties: { + securityRules: [ + { + name: 'allow-altrdp-to-vmssubnet-from-any' + properties: { + priority: 1000 + access: 'Allow' + direction: 'Inbound' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '33899-33899' + sourceAddressPrefix: '*' + destinationAddressPrefix: '10.0.10.0/24' + } + } + { + name: 'allow-altssh-to-vmssubnet-from-any' + properties: { + priority: 1001 + access: 'Allow' + direction: 'Inbound' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '22222-22222' + sourceAddressPrefix: '*' + destinationAddressPrefix: '10.0.10.0/24' + } + } + ] + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/01-01-spoke1.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/01-01-spoke1.bicep new file mode 100644 index 0000000000..a29c457abe --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/01-01-spoke1.bicep @@ -0,0 +1,169 @@ +param location string = 'eastus2' +param spoke1VMUsername string = 'admin-wth' +@secure() +param vmPassword string + +targetScope = 'resourceGroup' +//spoke1 resources + +resource wthspoke1vnet 'Microsoft.Network/virtualNetworks@2021-08-01' = { + name: 'wth-vnet-spoke101' + location: location + properties: { + addressSpace: { + addressPrefixes: [ + '10.1.0.0/16' + ] + } + subnets: [ + { + name: 'subnet-spoke1vms' + properties: { + addressPrefix: '10.1.10.0/24' + networkSecurityGroup: { + id: nsgspoke1vms.id + } + routeTable: { + id: rtspoke1vms.id + } + } + } + ] + } +} + +resource wthspoke1vmpip01 'Microsoft.Network/publicIPAddresses@2022-01-01' = { + name: 'wth-pip-spoke1vm01' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource wthspoke1vmnic 'Microsoft.Network/networkInterfaces@2022-01-01' = { + name: 'wth-nic-spoke1vm01' + location: location + properties: { + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + subnet: { + id: '${wthspoke1vnet.id}/subnets/subnet-spoke1vms' + } + privateIPAddress: '10.1.10.4' + publicIPAddress: { + id: wthspoke1vmpip01.id + } + } + } + ] + } +} + +resource wthspoke1vm01 'Microsoft.Compute/virtualMachines@2022-03-01' = { + name: 'wth-vm-spoke101' + location: location + properties: { + hardwareProfile: { + vmSize: 'Standard_B2s' + } + storageProfile: { + imageReference: { + publisher: 'MicrosoftWindowsServer' + offer: 'WindowsServer' + sku: '2022-datacenter-azure-edition' + version: 'latest' + } + osDisk: { + osType: 'Windows' + name: 'wth-disk-vmspoke1os01' + createOption: 'FromImage' + caching: 'ReadWrite' + } + } + osProfile: { + computerName: 'vm-spoke101' + adminUsername: spoke1VMUsername + adminPassword: vmPassword + windowsConfiguration: { + provisionVMAgent: true + enableAutomaticUpdates: true + } + } + networkProfile: { + networkInterfaces: [ + { + id: wthspoke1vmnic.id + } + ] + } + diagnosticsProfile: { + bootDiagnostics: { + enabled: true + } + } + licenseType: 'Windows_Server' + } +} + +resource changerdpport 'Microsoft.Compute/virtualMachines/extensions@2022-03-01' = { + name: '${wthspoke1vm01.name}/wth-vmextn-changerdpport33899' + location: location + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + settings: { + commandToExecute: 'powershell.exe -ep bypass -encodedcommand UwBlAHQALQBJAHQAZQBtAFAAcgBvAHAAZQByAHQAeQAgAC0AUABhAHQAaAAgACIASABLAEwATQA6AFwAUwB5AHMAdABlAG0AXABDAHUAcgByAGUAbgB0AEMAbwBuAHQAcgBvAGwAUwBlAHQAXABDAG8AbgB0AHIAbwBsAFwAVABlAHIAbQBpAG4AYQBsACAAUwBlAHIAdgBlAHIAXABXAGkAbgBTAHQAYQB0AGkAbwBuAHMAXABSAEQAUAAtAFQAYwBwAFwAIgAgAC0ATgBhAG0AZQAgAFAAbwByAHQATgB1AG0AYgBlAHIAIAAtAFYAYQBsAHUAZQAgADMAMwA4ADkAOQANAAoATgBlAHcALQBOAGUAdABGAGkAcgBlAHcAYQBsAGwAUgB1AGwAZQAgAC0ARABpAHMAcABsAGEAeQBOAGEAbQBlACAAIgBSAEQAUAAgADMAMwA4ADkAOQAgAFQAQwBQACIAIAAtAEQAaQByAGUAYwB0AGkAbwBuACAASQBuAGIAbwB1AG4AZAAgAC0ATABvAGMAYQBsAFAAbwByAHQAIAAzADMAOAA5ADkAIAAtAFAAcgBvAHQAbwBjAG8AbAAgAFQAQwBQACAALQBBAGMAdABpAG8AbgAgAEEAbABsAG8AdwANAAoATgBlAHcALQBOAGUAdABGAGkAcgBlAHcAYQBsAGwAUgB1AGwAZQAgAC0ARABpAHMAcABsAGEAeQBOAGEAbQBlACAAIgBSAEQAUAAgADMAMwA4ADkAOQAgAFUARABQACIAIAAtAEQAaQByAGUAYwB0AGkAbwBuACAASQBuAGIAbwB1AG4AZAAgAC0ATABvAGMAYQBsAFAAbwByAHQAIAAzADMAOAA5ADkAIAAtAFAAcgBvAHQAbwBjAG8AbAAgAFUARABQACAALQBBAGMAdABpAG8AbgAgAEEAbABsAG8AdwANAAoAUgBlAHMAdABhAHIAdAAtAFMAZQByAHYAaQBjAGUAIAAtAE4AYQBtAGUAIABUAGUAcgBtAFMAZQByAHYAaQBjAGUAIAAtAEYAbwByAGMAZQANAAoADQAKAE4AZQB3AC0ATgBlAHQARgBpAHIAZQB3AGEAbABsAFIAdQBsAGUAIAAtAEQAaQBzAHAAbABhAHkATgBhAG0AZQAgACcASQBDAE0AUAB2ADQAJwAgAC0ARABpAHIAZQBjAHQAaQBvAG4AIABJAG4AYgBvAHUAbgBkACAALQBBAGMAdABpAG8AbgAgAEEAbABsAG8AdwAgAC0AUAByAG8AdABvAGMAbwBsACAAaQBjAG0AcAB2ADQAIAAtAEUAbgBhAGIAbABlAGQAIABUAHIAdQBlAA==' + } + } +} + +resource rtspoke1vms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-spoke1vmssubnet' + location: location + properties: { + routes: [] + disableBgpRoutePropagation: false + } +} + +resource nsgspoke1vms 'Microsoft.Network/networkSecurityGroups@2022-01-01' = { + name: 'wth-nsg-spoke1vmssubnet' + location: location + properties: { + securityRules: [ + { + name: 'allow-altrdp-to-vmssubnet-from-any' + properties: { + priority: 1000 + access: 'Allow' + direction: 'Inbound' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '33899-33899' + sourceAddressPrefix: '*' + destinationAddressPrefix: '10.1.10.0/24' + } + } + { + name: 'allow-altssh-to-vmssubnet-from-any' + properties: { + priority: 1001 + access: 'Allow' + direction: 'Inbound' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '22222-22222' + sourceAddressPrefix: '*' + destinationAddressPrefix: '10.1.10.0/24' + } + } + ] + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/01-01-spoke2.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/01-01-spoke2.bicep new file mode 100644 index 0000000000..bc959ce86e --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/01-01-spoke2.bicep @@ -0,0 +1,169 @@ +param location string = 'eastus2' +param spoke2VMUsername string = 'admin-wth' +@secure() +param vmPassword string + +targetScope = 'resourceGroup' +//spoke2 resources + +resource wthspoke2vnet 'Microsoft.Network/virtualNetworks@2021-08-01' = { + name: 'wth-vnet-spoke201' + location: location + properties: { + addressSpace: { + addressPrefixes: [ + '10.2.0.0/16' + ] + } + subnets: [ + { + name: 'subnet-spoke2vms' + properties: { + addressPrefix: '10.2.10.0/24' + networkSecurityGroup: { + id: nsgspoke2vms.id + } + routeTable: { + id: rtspoke2vms.id + } + } + } + ] + } +} + +resource wthspoke2vmpip01 'Microsoft.Network/publicIPAddresses@2022-01-01' = { + name: 'wth-pip-spoke2vm01' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource wthspoke2vmnic 'Microsoft.Network/networkInterfaces@2022-01-01' = { + name: 'wth-nic-spoke2vm01' + location: location + properties: { + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + subnet: { + id: '${wthspoke2vnet.id}/subnets/subnet-spoke2vms' + } + privateIPAddress: '10.2.10.4' + publicIPAddress: { + id: wthspoke2vmpip01.id + } + } + } + ] + } +} + +resource wthspoke2vm01 'Microsoft.Compute/virtualMachines@2022-03-01' = { + name: 'wth-vm-spoke201' + location: location + properties: { + hardwareProfile: { + vmSize: 'Standard_B2s' + } + storageProfile: { + imageReference: { + publisher: 'MicrosoftWindowsServer' + offer: 'WindowsServer' + sku: '2022-datacenter-azure-edition' + version: 'latest' + } + osDisk: { + osType: 'Windows' + name: 'wth-disk-vmspoke2os01' + createOption: 'FromImage' + caching: 'ReadWrite' + } + } + osProfile: { + computerName: 'vm-spoke201' + adminUsername: spoke2VMUsername + adminPassword: vmPassword + windowsConfiguration: { + provisionVMAgent: true + enableAutomaticUpdates: true + } + } + networkProfile: { + networkInterfaces: [ + { + id: wthspoke2vmnic.id + } + ] + } + diagnosticsProfile: { + bootDiagnostics: { + enabled: true + } + } + licenseType: 'Windows_Server' + } +} + +resource changerdpport 'Microsoft.Compute/virtualMachines/extensions@2022-03-01' = { + name: '${wthspoke2vm01.name}/wth-vmextn-changerdpport33899' + location: location + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + settings: { + commandToExecute: 'powershell.exe -ep bypass -encodedcommand UwBlAHQALQBJAHQAZQBtAFAAcgBvAHAAZQByAHQAeQAgAC0AUABhAHQAaAAgACIASABLAEwATQA6AFwAUwB5AHMAdABlAG0AXABDAHUAcgByAGUAbgB0AEMAbwBuAHQAcgBvAGwAUwBlAHQAXABDAG8AbgB0AHIAbwBsAFwAVABlAHIAbQBpAG4AYQBsACAAUwBlAHIAdgBlAHIAXABXAGkAbgBTAHQAYQB0AGkAbwBuAHMAXABSAEQAUAAtAFQAYwBwAFwAIgAgAC0ATgBhAG0AZQAgAFAAbwByAHQATgB1AG0AYgBlAHIAIAAtAFYAYQBsAHUAZQAgADMAMwA4ADkAOQANAAoATgBlAHcALQBOAGUAdABGAGkAcgBlAHcAYQBsAGwAUgB1AGwAZQAgAC0ARABpAHMAcABsAGEAeQBOAGEAbQBlACAAIgBSAEQAUAAgADMAMwA4ADkAOQAgAFQAQwBQACIAIAAtAEQAaQByAGUAYwB0AGkAbwBuACAASQBuAGIAbwB1AG4AZAAgAC0ATABvAGMAYQBsAFAAbwByAHQAIAAzADMAOAA5ADkAIAAtAFAAcgBvAHQAbwBjAG8AbAAgAFQAQwBQACAALQBBAGMAdABpAG8AbgAgAEEAbABsAG8AdwANAAoATgBlAHcALQBOAGUAdABGAGkAcgBlAHcAYQBsAGwAUgB1AGwAZQAgAC0ARABpAHMAcABsAGEAeQBOAGEAbQBlACAAIgBSAEQAUAAgADMAMwA4ADkAOQAgAFUARABQACIAIAAtAEQAaQByAGUAYwB0AGkAbwBuACAASQBuAGIAbwB1AG4AZAAgAC0ATABvAGMAYQBsAFAAbwByAHQAIAAzADMAOAA5ADkAIAAtAFAAcgBvAHQAbwBjAG8AbAAgAFUARABQACAALQBBAGMAdABpAG8AbgAgAEEAbABsAG8AdwANAAoAUgBlAHMAdABhAHIAdAAtAFMAZQByAHYAaQBjAGUAIAAtAE4AYQBtAGUAIABUAGUAcgBtAFMAZQByAHYAaQBjAGUAIAAtAEYAbwByAGMAZQANAAoADQAKAE4AZQB3AC0ATgBlAHQARgBpAHIAZQB3AGEAbABsAFIAdQBsAGUAIAAtAEQAaQBzAHAAbABhAHkATgBhAG0AZQAgACcASQBDAE0AUAB2ADQAJwAgAC0ARABpAHIAZQBjAHQAaQBvAG4AIABJAG4AYgBvAHUAbgBkACAALQBBAGMAdABpAG8AbgAgAEEAbABsAG8AdwAgAC0AUAByAG8AdABvAGMAbwBsACAAaQBjAG0AcAB2ADQAIAAtAEUAbgBhAGIAbABlAGQAIABUAHIAdQBlAA==' + } + } +} + +resource rtspoke2vms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-spoke2vmssubnet' + location: location + properties: { + routes: [] + disableBgpRoutePropagation: false + } +} + +resource nsgspoke2vms 'Microsoft.Network/networkSecurityGroups@2022-01-01' = { + name: 'wth-nsg-spoke2vmssubnet' + location: location + properties: { + securityRules: [ + { + name: 'allow-altrdp-to-vmssubnet-from-any' + properties: { + priority: 1000 + access: 'Allow' + direction: 'Inbound' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '33899-33899' + sourceAddressPrefix: '*' + destinationAddressPrefix: '10.2.10.0/24' + } + } + { + name: 'allow-altssh-to-vmssubnet-from-any' + properties: { + priority: 1001 + access: 'Allow' + direction: 'Inbound' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '22222-22222' + sourceAddressPrefix: '*' + destinationAddressPrefix: '10.2.10.0/24' + } + } + ] + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/01-02-vnetpeeringhub.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/01-02-vnetpeeringhub.bicep new file mode 100644 index 0000000000..cc274cc671 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/01-02-vnetpeeringhub.bicep @@ -0,0 +1,40 @@ +resource hubvnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource spoke1vnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-spoke101' + scope: resourceGroup('wth-rg-spoke1') +} + +resource spoke2vnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-spoke201' + scope: resourceGroup('wth-rg-spoke2') +} + +resource hubtospoke1 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2022-01-01' = { + name: '${hubvnet.name}/wth-peering-hubtospoke1' + properties: { + allowGatewayTransit: true + allowForwardedTraffic: true + allowVirtualNetworkAccess: true + useRemoteGateways: false + remoteVirtualNetwork: { + id: spoke1vnet.id + } + } +} + +resource hubtospoke2 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2022-01-01' = { + name: '${hubvnet.name}/wth-peering-hubtospoke2' + properties: { + allowGatewayTransit: true + allowForwardedTraffic: true + allowVirtualNetworkAccess: true + useRemoteGateways: false + remoteVirtualNetwork: { + id: spoke2vnet.id + } + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/01-02-vnetpeeringspoke1.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/01-02-vnetpeeringspoke1.bicep new file mode 100644 index 0000000000..6531902b6a --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/01-02-vnetpeeringspoke1.bicep @@ -0,0 +1,27 @@ +resource hubvnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource spoke1vnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-spoke101' + scope: resourceGroup('wth-rg-spoke1') +} + +resource spoke2vnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-spoke201' + scope: resourceGroup('wth-rg-spoke2') +} + +resource spoke1tohub 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2022-01-01' = { + name: '${spoke1vnet.name}/wth-peering-spoke1tohub' + properties: { + allowGatewayTransit: false + allowForwardedTraffic: false + allowVirtualNetworkAccess: true + useRemoteGateways: true + remoteVirtualNetwork: { + id: hubvnet.id + } + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/01-02-vnetpeeringspoke2.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/01-02-vnetpeeringspoke2.bicep new file mode 100644 index 0000000000..d837f6e6b5 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/01-02-vnetpeeringspoke2.bicep @@ -0,0 +1,27 @@ +resource hubvnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource spoke1vnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-spoke101' + scope: resourceGroup('wth-rg-spoke1') +} + +resource spoke2vnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-spoke201' + scope: resourceGroup('wth-rg-spoke2') +} + +resource spoke2tohub 'Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2022-01-01' = { + name: '${spoke2vnet.name}/wth-peering-spoke1tohub' + properties: { + allowGatewayTransit: false + allowForwardedTraffic: false + allowVirtualNetworkAccess: true + useRemoteGateways: true + remoteVirtualNetwork: { + id: hubvnet.id + } + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/01-03-onprem.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/01-03-onprem.bicep new file mode 100644 index 0000000000..64464639fe --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/01-03-onprem.bicep @@ -0,0 +1,321 @@ +param location string = 'eastus2' +param onpremVMUsername string = 'admin-wth' +@secure() +param vmPassword string + +targetScope = 'resourceGroup' +//onprem resources + +resource hubvnet 'Microsoft.Network/virtualNetworks@2022-07-01' existing = { + name: 'wth-vnet-hub01' + scope: resourceGroup('wth-rg-hub') + + resource gwsubnet 'subnets' existing = { + name: 'GatewaySubnet' + } +} + +resource wthonpremvnet 'Microsoft.Network/virtualNetworks@2021-08-01' = { + name: 'wth-vnet-onprem01' + location: location + properties: { + addressSpace: { + addressPrefixes: [ + '172.16.0.0/16' + ] + } + subnets: [ + { + name: 'subnet-vpn' + properties: { + addressPrefix: '172.16.0.0/24' + networkSecurityGroup: { + id: nsgonpremvpn.id + } + } + } + { + name: 'subnet-onpremvms' + properties: { + addressPrefix: '172.16.10.0/24' + networkSecurityGroup: { + id: nsgonpremvms.id + } + routeTable: { + id: rtonpremvms.id + } + } + } + ] + } +} + +resource wthonpremvmpip01 'Microsoft.Network/publicIPAddresses@2022-01-01' = { + name: 'wth-pip-onpremvm01' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource wthonpremvmnic 'Microsoft.Network/networkInterfaces@2022-01-01' = { + name: 'wth-nic-onpremvm01' + location: location + properties: { + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + subnet: { + id: '${wthonpremvnet.id}/subnets/subnet-onpremvms' + } + privateIPAddress: '172.16.10.4' + publicIPAddress: { + id: wthonpremvmpip01.id + } + } + } + ] + } +} + +resource wthonpremvm01 'Microsoft.Compute/virtualMachines@2022-03-01' = { + name: 'wth-vm-onprem01' + location: location + properties: { + hardwareProfile: { + vmSize: 'Standard_B2s' + } + storageProfile: { + imageReference: { + publisher: 'MicrosoftWindowsServer' + offer: 'WindowsServer' + sku: '2022-datacenter-azure-edition' + version: 'latest' + } + osDisk: { + osType: 'Windows' + name: 'wth-disk-vmonpremos01' + createOption: 'FromImage' + caching: 'ReadWrite' + } + } + osProfile: { + computerName: 'vm-onprem01' + adminUsername: onpremVMUsername + adminPassword: vmPassword + windowsConfiguration: { + provisionVMAgent: true + enableAutomaticUpdates: true + } + } + networkProfile: { + networkInterfaces: [ + { + id: wthonpremvmnic.id + } + ] + } + diagnosticsProfile: { + bootDiagnostics: { + enabled: true + } + } + licenseType: 'Windows_Server' + } +} + +resource changerdpport 'Microsoft.Compute/virtualMachines/extensions@2022-03-01' = { + name: '${wthonpremvm01.name}/wth-vmextn-changerdpport33899' + location: location + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + settings: { + commandToExecute: 'powershell.exe -ep bypass -encodedcommand UwBlAHQALQBJAHQAZQBtAFAAcgBvAHAAZQByAHQAeQAgAC0AUABhAHQAaAAgACIASABLAEwATQA6AFwAUwB5AHMAdABlAG0AXABDAHUAcgByAGUAbgB0AEMAbwBuAHQAcgBvAGwAUwBlAHQAXABDAG8AbgB0AHIAbwBsAFwAVABlAHIAbQBpAG4AYQBsACAAUwBlAHIAdgBlAHIAXABXAGkAbgBTAHQAYQB0AGkAbwBuAHMAXABSAEQAUAAtAFQAYwBwAFwAIgAgAC0ATgBhAG0AZQAgAFAAbwByAHQATgB1AG0AYgBlAHIAIAAtAFYAYQBsAHUAZQAgADMAMwA4ADkAOQANAAoATgBlAHcALQBOAGUAdABGAGkAcgBlAHcAYQBsAGwAUgB1AGwAZQAgAC0ARABpAHMAcABsAGEAeQBOAGEAbQBlACAAIgBSAEQAUAAgADMAMwA4ADkAOQAgAFQAQwBQACIAIAAtAEQAaQByAGUAYwB0AGkAbwBuACAASQBuAGIAbwB1AG4AZAAgAC0ATABvAGMAYQBsAFAAbwByAHQAIAAzADMAOAA5ADkAIAAtAFAAcgBvAHQAbwBjAG8AbAAgAFQAQwBQACAALQBBAGMAdABpAG8AbgAgAEEAbABsAG8AdwANAAoATgBlAHcALQBOAGUAdABGAGkAcgBlAHcAYQBsAGwAUgB1AGwAZQAgAC0ARABpAHMAcABsAGEAeQBOAGEAbQBlACAAIgBSAEQAUAAgADMAMwA4ADkAOQAgAFUARABQACIAIAAtAEQAaQByAGUAYwB0AGkAbwBuACAASQBuAGIAbwB1AG4AZAAgAC0ATABvAGMAYQBsAFAAbwByAHQAIAAzADMAOAA5ADkAIAAtAFAAcgBvAHQAbwBjAG8AbAAgAFUARABQACAALQBBAGMAdABpAG8AbgAgAEEAbABsAG8AdwANAAoAUgBlAHMAdABhAHIAdAAtAFMAZQByAHYAaQBjAGUAIAAtAE4AYQBtAGUAIABUAGUAcgBtAFMAZQByAHYAaQBjAGUAIAAtAEYAbwByAGMAZQANAAoADQAKAE4AZQB3AC0ATgBlAHQARgBpAHIAZQB3AGEAbABsAFIAdQBsAGUAIAAtAEQAaQBzAHAAbABhAHkATgBhAG0AZQAgACcASQBDAE0AUAB2ADQAJwAgAC0ARABpAHIAZQBjAHQAaQBvAG4AIABJAG4AYgBvAHUAbgBkACAALQBBAGMAdABpAG8AbgAgAEEAbABsAG8AdwAgAC0AUAByAG8AdABvAGMAbwBsACAAaQBjAG0AcAB2ADQAIAAtAEUAbgBhAGIAbABlAGQAIABUAHIAdQBlAA==' + } + } +} + +resource rtonpremvms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-onpremvmssubnet' + location: location + properties: { + routes: [ + { + name: 'route-hub' + properties: { + addressPrefix: '10.0.0.0/16' + nextHopIpAddress: '172.16.0.4' + nextHopType: 'VirtualAppliance' + } + } + { + name: 'route-spoke1' + properties: { + addressPrefix: '10.1.0.0/16' + nextHopIpAddress: '172.16.0.4' + nextHopType: 'VirtualAppliance' + } + } + { + name: 'route-spoke2' + properties: { + addressPrefix: '10.2.0.0/16' + nextHopIpAddress: '172.16.0.4' + nextHopType: 'VirtualAppliance' + } + } + ] + disableBgpRoutePropagation: false + } +} + +resource nsgonpremvms 'Microsoft.Network/networkSecurityGroups@2022-01-01' = { + name: 'wth-nsg-onpremvmssubnet' + location: location + properties: { + securityRules: [ + { + name: 'allow-altrdp-to-vmssubnet-from-any' + properties: { + priority: 1000 + access: 'Allow' + direction: 'Inbound' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '33899-33899' + sourceAddressPrefix: '*' + destinationAddressPrefix: '172.16.10.0/24' + } + } + { + name: 'allow-altssh-to-vmssubnet-from-any' + properties: { + priority: 1001 + access: 'Allow' + direction: 'Inbound' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '22222-22222' + sourceAddressPrefix: '*' + destinationAddressPrefix: '172.16.10.0/24' + } + } + ] + } +} + +resource nsgonpremvpn 'Microsoft.Network/networkSecurityGroups@2022-01-01' = { + name: 'wth-nsg-onpremvpnsubnet' + location: location + properties: { + securityRules: [ + { + name: 'allow-any-to-vpnsubnet-from-onprem' + properties: { + priority: 1000 + access: 'Allow' + direction: 'Inbound' + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '*' + sourceAddressPrefix: '172.16.10.0/24' + destinationAddressPrefix: '*' + } + } + { + name: 'allow-any-to-any-from-azurevpngw' + properties: { + priority: 1001 + access: 'Allow' + direction: 'Inbound' + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '*' + sourceAddressPrefix: hubvnet::gwsubnet.properties.addressPrefix + destinationAddressPrefix: '*' + } + } + ] + } +} + +resource wthonpremcsrpip01 'Microsoft.Network/publicIPAddresses@2022-01-01' = { + name: 'wth-pip-csr01' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource wthonpremcsrnic 'Microsoft.Network/networkInterfaces@2022-01-01' = { + name: 'wth-nic-csr01' + location: location + properties: { + enableIPForwarding: true + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + subnet: { + id: '${wthonpremvnet.id}/subnets/subnet-vpn' + } + privateIPAddress: '172.16.0.4' + publicIPAddress: { + id: wthonpremcsrpip01.id + } + } + } + ] + } +} + +resource ciscocsr 'Microsoft.Compute/virtualMachines@2022-03-01' = { + name: 'wth-vm-ciscocsr01' + location: location + plan: { + publisher: 'cisco' + product: 'cisco-csr-1000v' + name: '16_12-byol' + } + properties: { + hardwareProfile: { + vmSize: 'Standard_B2ms' + } + storageProfile: { + imageReference: { + publisher: 'cisco' + offer: 'cisco-csr-1000v' + sku: '16_12-byol' + version: 'latest' + } + osDisk: { + osType: 'Linux' + createOption: 'FromImage' + } + } + networkProfile: { + networkInterfaces: [ + { + id: wthonpremcsrnic.id + } + ] + } + osProfile: { + adminUsername: onpremVMUsername + adminPassword: vmPassword + computerName: 'wthcsr01' + customData: loadFileAsBase64('./csrScript.txt.tmp') + } + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/01-04-hubvpnconfig.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/01-04-hubvpnconfig.bicep new file mode 100644 index 0000000000..11883fa563 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/01-04-hubvpnconfig.bicep @@ -0,0 +1,48 @@ +param location string = 'eastus2' + +resource onpremcsrpip 'Microsoft.Network/publicIPAddresses@2022-01-01' existing = { + name: 'wth-pip-csr01' + scope: resourceGroup('wth-rg-onprem') +} + +resource onpremcsrnic 'Microsoft.Network/networkInterfaces@2022-01-01' existing = { + name: 'wth-nic-csr01' + scope: resourceGroup('wth-rg-onprem') +} + +resource wthhubvnetgw 'Microsoft.Network/virtualNetworkGateways@2022-01-01' existing = { + name: 'wth-vngw-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource wthhublocalgw 'Microsoft.Network/localNetworkGateways@2022-01-01' = { + name: 'wth-lgw-onprem01' + location: location + properties: { + gatewayIpAddress: onpremcsrpip.properties.ipAddress + localNetworkAddressSpace: { + addressPrefixes: [] + } + bgpSettings: { + bgpPeeringAddress: onpremcsrnic.properties.ipConfigurations[0].properties.privateIPAddress + asn: 65510 + } + } +} + +resource wthhubconnection 'Microsoft.Network/connections@2022-01-01' = { + name: 'wth-cxn-vpn01' + location: location + properties: { + sharedKey: '123mysecretkey' + connectionType: 'IPsec' + connectionMode: 'Default' + enableBgp: true + localNetworkGateway2: { + id: wthhublocalgw.id + } + virtualNetworkGateway1: { + id: wthhubvnetgw.id + } + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/02-00-afw.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/02-00-afw.bicep new file mode 100644 index 0000000000..f0e517a2f1 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/02-00-afw.bicep @@ -0,0 +1,175 @@ +param location string = 'eastus2' +param afwSku string = 'basic' + +resource hubvnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource afwsubnet 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' = { + name: '${hubvnet.name}/AzureFirewallSubnet' + properties: { + addressPrefix: '10.0.1.0/24' + } +} + +resource afwmgmtsubnet 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' = { + name: '${hubvnet.name}/AzureFirewallManagementSubnet' + properties: { + addressPrefix: '10.0.2.0/24' + } +} + +resource wthafwpip01 'Microsoft.Network/publicIPAddresses@2022-01-01' = { + name: 'wth-pip-afw01' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource wthafwmgmtpip01 'Microsoft.Network/publicIPAddresses@2022-01-01' = { + name: 'wth-pip-afwmgmt01' + location: location + sku: { + name: 'Standard' + tier: 'Regional' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource wthafwpolicy 'Microsoft.Network/firewallPolicies@2022-01-01' = { + name: 'wth-fwp-policy01' + location: location + properties: { + sku: { + tier: afwSku + } + } +} + +resource wthafw 'Microsoft.Network/azureFirewalls@2022-01-01' = { + name: 'wth-afw-hub01' + location: location + properties: { + sku: { + name: 'AZFW_VNet' + tier: afwSku + } + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + subnet: { + id: afwsubnet.id + } + publicIPAddress: { + id: wthafwpip01.id + } + } + } + ] + managementIpConfiguration: { + name: 'ipconfigMgmt1' + properties: { + publicIPAddress: { + id: wthafwmgmtpip01.id + } + subnet: { + id: afwmgmtsubnet.id + } + } + } + + firewallPolicy: { + id: wthafwpolicy.id + } + } +} + +resource wthlaw 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { + name: 'wth-law-default01' + location: location + properties: { + sku: { + name: 'PerGB2018' + } + } +} + +resource wthafwdiagsettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: 'diagSettingsAFW' + scope: wthafw + properties: { + workspaceId: wthlaw.id + logs: [ + { + enabled: true + categoryGroup: 'allLogs' + retentionPolicy: { + enabled: true + days: 14 + } + } + ] + logAnalyticsDestinationType: 'Dedicated' + } +} + +resource rthubvms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-hubvmssubnet' + location: location + properties: { + routes: [ + { + name: 'route-all-to-afw' + properties: { + addressPrefix: '0.0.0.0/0' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + } + } + ] + disableBgpRoutePropagation: false + } +} + +resource rtvnetgw 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-hubgwsubnet' + location: location + properties: { + routes: [ + { + name: 'route-spoke1-to-afw' + properties: { + addressPrefix: '10.1.0.0/16' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + } + } + { + name: 'route-spoke2-to-afw' + properties: { + addressPrefix: '10.2.0.0/16' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + } + } + { + name: 'route-hubvm-to-afw' + properties: { + addressPrefix: '10.0.10.0/24' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + } + } + ] + disableBgpRoutePropagation: false + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/02-01-fwpolicyrules.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/02-01-fwpolicyrules.bicep new file mode 100644 index 0000000000..2d3e52ad59 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/02-01-fwpolicyrules.bicep @@ -0,0 +1,227 @@ +resource wthafwpolicy 'Microsoft.Network/firewallPolicies@2022-01-01' existing = { + name: 'wth-fwp-policy01' + scope: resourceGroup('wth-rg-hub') +} + +resource wthafwpip01 'Microsoft.Network/publicIPAddresses@2022-01-01' existing = { + name: 'wth-pip-afw01' + scope: resourceGroup('wth-rg-hub') +} + +resource wthhubvmnic 'Microsoft.Network/networkInterfaces@2022-01-01' existing = { + name: 'wth-nic-hubvm01' + scope: resourceGroup('wth-rg-hub') +} + +resource wthspoke1vmnic 'Microsoft.Network/networkInterfaces@2022-01-01' existing = { + name: 'wth-nic-spoke1vm01' + scope: resourceGroup('wth-rg-spoke1') +} + +resource wthspoke2vmnic 'Microsoft.Network/networkInterfaces@2022-01-01' existing = { + name: 'wth-nic-spoke2vm01' + scope: resourceGroup('wth-rg-spoke2') +} + +resource wthafwrcgdnat 'Microsoft.Network/firewallPolicies/ruleCollectionGroups@2022-01-01' = { + name: '${wthafwpolicy.name}/WTH_DNATRulesCollectionGroup' + properties: { + priority: 100 + ruleCollections: [ + { + ruleCollectionType: 'FirewallPolicyNatRuleCollection' + action: { + type: 'DNAT' + } + name: 'dnat-webservers-http' + priority: 100 + rules: [ + { + name: 'dnat-tcp8080-to-hub-80' + ruleType: 'NatRule' + description: 'DNAT port 8080 to hub' + destinationAddresses: [ + wthafwpip01.properties.ipAddress + ] + destinationPorts: [ + '8080' + ] + ipProtocols: [ + 'tcp' + ] + sourceAddresses: [ + '*' + ] + translatedAddress: wthhubvmnic.properties.ipConfigurations[0].properties.privateIPAddress + translatedPort: '80' + } + { + name: 'dnat-tcp8081-to-spoke1-80' + ruleType: 'NatRule' + description: 'DNAT port 8081 to Spoke1' + destinationAddresses: [ + wthafwpip01.properties.ipAddress + ] + destinationPorts: [ + '8081' + ] + ipProtocols: [ + 'tcp' + ] + sourceAddresses: [ + '*' + ] + translatedAddress: wthspoke1vmnic.properties.ipConfigurations[0].properties.privateIPAddress + translatedPort: '80' + } + { + name: 'dnat-tcp8082-to-spoke1-80' + ruleType: 'NatRule' + description: 'DNAT port 8082 to Spoke2' + destinationAddresses: [ + wthafwpip01.properties.ipAddress + ] + destinationPorts: [ + '8082' + ] + ipProtocols: [ + 'tcp' + ] + sourceAddresses: [ + '*' + ] + translatedAddress: wthspoke2vmnic.properties.ipConfigurations[0].properties.privateIPAddress + translatedPort: '80' + } + ] + } + { + ruleCollectionType: 'FirewallPolicyNatRuleCollection' + action: { + type: 'DNAT' + } + name: 'dnat-rdp' + priority: 101 + rules: [ + { + name: 'dnat-tcp33890-to-hub-33899' + ruleType: 'NatRule' + description: 'DNAT port 33891 to hub' + destinationAddresses: [ + wthafwpip01.properties.ipAddress + ] + destinationPorts: [ + '33890' + ] + ipProtocols: [ + 'tcp' + ] + sourceAddresses: [ + '*' + ] + translatedAddress: wthhubvmnic.properties.ipConfigurations[0].properties.privateIPAddress + translatedPort: '33899' + } + { + name: 'dnat-tcp33891-to-spoke1-33899' + ruleType: 'NatRule' + description: 'DNAT port 33891 to Spoke1' + destinationAddresses: [ + wthafwpip01.properties.ipAddress + ] + destinationPorts: [ + '33891' + ] + ipProtocols: [ + 'tcp' + ] + sourceAddresses: [ + '*' + ] + translatedAddress: wthspoke1vmnic.properties.ipConfigurations[0].properties.privateIPAddress + translatedPort: '33899' + } + { + name: 'dnat-tcp33892-to-spoke1-33899' + ruleType: 'NatRule' + description: 'DNAT port 33892 to Spoke2' + destinationAddresses: [ + wthafwpip01.properties.ipAddress + ] + destinationPorts: [ + '33892' + ] + ipProtocols: [ + 'tcp' + ] + sourceAddresses: [ + '*' + ] + translatedAddress: wthspoke2vmnic.properties.ipConfigurations[0].properties.privateIPAddress + translatedPort: '33899' + } + ] + } + ] + } +} + +resource wthafwrcgnet 'Microsoft.Network/firewallPolicies/ruleCollectionGroups@2022-01-01' = { + name: '${wthafwpolicy.name}/WTH_NetworkRulesCollectionGroup' + properties: { + priority: 200 + ruleCollections: [ + { + ruleCollectionType: 'FirewallPolicyFilterRuleCollection' + action: { + type: 'Allow' + } + name: 'allow-network-rules' + priority: 200 + rules: [ + { + ruleType: 'NetworkRule' + name: 'allow-any-to-any' + description: 'Allow any traffic to any destination' + destinationAddresses: [ + '*' + ] + destinationPorts: [ + '*' + ] + sourceAddresses: [ + '*' + ] + ipProtocols: [ + 'Any' + ] + } + ] + } + ] + } + dependsOn: [ + wthafwrcgdnat + ] +} + +resource wthafwrcgapp 'Microsoft.Network/firewallPolicies/ruleCollectionGroups@2022-01-01' = { + name: '${wthafwpolicy.name}/WTH_AppRulesCollectionGroup' + properties: { + priority: 300 + ruleCollections: [ + { + ruleCollectionType: 'FirewallPolicyFilterRuleCollection' + action: { + type: 'Allow' + } + priority: 300 + name: 'allow-apprules' + rules: [] + } + ] + } + dependsOn: [ + wthafwrcgnet + ] +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/02-01-hub.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/02-01-hub.bicep new file mode 100644 index 0000000000..c68616e14b --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/02-01-hub.bicep @@ -0,0 +1,68 @@ +param location string = 'eastus2' + +resource wthafw 'Microsoft.Network/azureFirewalls@2022-01-01' existing = { + name: 'wth-afw-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource wthhubvm01 'Microsoft.Compute/virtualMachines@2022-03-01' existing = { + name: 'wth-vm-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource rthubvms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-hubvmssubnet' + location: location + properties: { + routes: [ + { + name: 'route-all-to-afw' + properties: { + addressPrefix: '0.0.0.0/0' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + + } + } + { + name: 'route-onprem-to-afw' + properties: { + addressPrefix: '172.16.0.0/16' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + } + } + { + name: 'route-afw-to-vnet' + properties: { + addressPrefix: '10.0.1.0/24' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + } + } + ] + disableBgpRoutePropagation: true + } +} + +resource installinspectorgadget 'Microsoft.Compute/virtualMachines/runCommands@2022-03-01' = { + name: '${wthhubvm01.name}/wth-runcmd-installinspectorgadget' + location: location + properties: { + asyncExecution: true + source: { + + /* + To generate encoded command in PowerShell: + + $s = @' + Install-WindowsFeature Web-Server,Web-Asp-Net45 -IncludeManagementTools + [System.Net.WebClient]::new().DownloadFile('https://raw.githubusercontent.com/jelledruyts/InspectorGadget/main/Page/default.aspx','c:\inetpub\wwwroot\default.aspx') + '@ + $bytes = [System.Text.Encoding]::Unicode.GetBytes($s) + [convert]::ToBase64String($bytes) */ + script: 'powershell.exe -ep bypass -encodedcommand IAAgACAAIAAgACAAIAAgAEkAbgBzAHQAYQBsAGwALQBXAGkAbgBkAG8AdwBzAEYAZQBhAHQAdQByAGUAIABXAGUAYgAtAFMAZQByAHYAZQByACwAVwBlAGIALQBBAHMAcAAtAE4AZQB0ADQANQAgAC0ASQBuAGMAbAB1AGQAZQBNAGEAbgBhAGcAZQBtAGUAbgB0AFQAbwBvAGwAcwAKACAAIAAgACAAIAAgACAAIABbAFMAeQBzAHQAZQBtAC4ATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdABdADoAOgBuAGUAdwAoACkALgBEAG8AdwBuAGwAbwBhAGQARgBpAGwAZQAoACcAaAB0AHQAcABzADoALwAvAHIAYQB3AC4AZwBpAHQAaAB1AGIAdQBzAGUAcgBjAG8AbgB0AGUAbgB0AC4AYwBvAG0ALwBqAGUAbABsAGUAZAByAHUAeQB0AHMALwBJAG4AcwBwAGUAYwB0AG8AcgBHAGEAZABnAGUAdAAvAG0AYQBpAG4ALwBQAGEAZwBlAC8AZABlAGYAYQB1AGwAdAAuAGEAcwBwAHgAJwAsACcAYwA6AFwAaQBuAGUAdABwAHUAYgBcAHcAdwB3AHIAbwBvAHQAXABkAGUAZgBhAHUAbAB0AC4AYQBzAHAAeAAnACkA' + } + timeoutInSeconds: 600 + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/02-01-onprem.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/02-01-onprem.bicep new file mode 100644 index 0000000000..397a33cff2 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/02-01-onprem.bicep @@ -0,0 +1,29 @@ +param location string = 'eastus2' + + +resource wthonpremvm01 'Microsoft.Compute/virtualMachines@2022-03-01' existing = { + name: 'wth-vm-onprem01' + scope: resourceGroup('wth-rg-onprem') +} + +resource installinspectorgadget 'Microsoft.Compute/virtualMachines/runCommands@2022-03-01' = { + name: '${wthonpremvm01.name}/wth-runcmd-installinspectorgadget' + location: location + properties: { + asyncExecution: true + source: { + + /* + To generate encoded command in PowerShell: + + $s = @' + Install-WindowsFeature Web-Server,Web-Asp-Net45 -IncludeManagementTools + [System.Net.WebClient]::new().DownloadFile('https://raw.githubusercontent.com/jelledruyts/InspectorGadget/main/Page/default.aspx','c:\inetpub\wwwroot\default.aspx') + '@ + $bytes = [System.Text.Encoding]::Unicode.GetBytes($s) + [convert]::ToBase64String($bytes) */ + script: 'powershell.exe -ep bypass -encodedcommand IAAgACAAIAAgACAAIAAgAEkAbgBzAHQAYQBsAGwALQBXAGkAbgBkAG8AdwBzAEYAZQBhAHQAdQByAGUAIABXAGUAYgAtAFMAZQByAHYAZQByACwAVwBlAGIALQBBAHMAcAAtAE4AZQB0ADQANQAgAC0ASQBuAGMAbAB1AGQAZQBNAGEAbgBhAGcAZQBtAGUAbgB0AFQAbwBvAGwAcwAKACAAIAAgACAAIAAgACAAIABbAFMAeQBzAHQAZQBtAC4ATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdABdADoAOgBuAGUAdwAoACkALgBEAG8AdwBuAGwAbwBhAGQARgBpAGwAZQAoACcAaAB0AHQAcABzADoALwAvAHIAYQB3AC4AZwBpAHQAaAB1AGIAdQBzAGUAcgBjAG8AbgB0AGUAbgB0AC4AYwBvAG0ALwBqAGUAbABsAGUAZAByAHUAeQB0AHMALwBJAG4AcwBwAGUAYwB0AG8AcgBHAGEAZABnAGUAdAAvAG0AYQBpAG4ALwBQAGEAZwBlAC8AZABlAGYAYQB1AGwAdAAuAGEAcwBwAHgAJwAsACcAYwA6AFwAaQBuAGUAdABwAHUAYgBcAHcAdwB3AHIAbwBvAHQAXABkAGUAZgBhAHUAbAB0AC4AYQBzAHAAeAAnACkA' + } + timeoutInSeconds: 600 + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/02-01-spoke1.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/02-01-spoke1.bicep new file mode 100644 index 0000000000..fab54341b9 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/02-01-spoke1.bicep @@ -0,0 +1,51 @@ +param location string = 'eastus2' + +resource wthafw 'Microsoft.Network/azureFirewalls@2022-01-01' existing = { + name: 'wth-afw-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource wthspoke1vm01 'Microsoft.Compute/virtualMachines@2022-03-01' existing = { + name: 'wth-vm-spoke101' + scope: resourceGroup('wth-rg-spoke1') +} + +resource rtspoke1vms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-spoke1vmssubnet' + location: location + properties: { + routes: [ + { + name: 'route-all-to-afw' + properties: { + addressPrefix: '0.0.0.0/0' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + } + } + ] + disableBgpRoutePropagation: true + } +} + +resource installinspectorgadget 'Microsoft.Compute/virtualMachines/runCommands@2022-03-01' = { + name: '${wthspoke1vm01.name}/wth-runcmd-installinspectorgadget' + location: location + properties: { + asyncExecution: true + source: { + + /* + To generate encoded command in PowerShell: + + $s = @' + Install-WindowsFeature Web-Server,Web-Asp-Net45 -IncludeManagementTools + [System.Net.WebClient]::new().DownloadFile('https://raw.githubusercontent.com/jelledruyts/InspectorGadget/main/Page/default.aspx','c:\inetpub\wwwroot\default.aspx') + '@ + $bytes = [System.Text.Encoding]::Unicode.GetBytes($s) + [convert]::ToBase64String($bytes) */ + script: 'powershell.exe -ep bypass -encodedcommand IAAgACAAIAAgACAAIAAgAEkAbgBzAHQAYQBsAGwALQBXAGkAbgBkAG8AdwBzAEYAZQBhAHQAdQByAGUAIABXAGUAYgAtAFMAZQByAHYAZQByACwAVwBlAGIALQBBAHMAcAAtAE4AZQB0ADQANQAgAC0ASQBuAGMAbAB1AGQAZQBNAGEAbgBhAGcAZQBtAGUAbgB0AFQAbwBvAGwAcwAKACAAIAAgACAAIAAgACAAIABbAFMAeQBzAHQAZQBtAC4ATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdABdADoAOgBuAGUAdwAoACkALgBEAG8AdwBuAGwAbwBhAGQARgBpAGwAZQAoACcAaAB0AHQAcABzADoALwAvAHIAYQB3AC4AZwBpAHQAaAB1AGIAdQBzAGUAcgBjAG8AbgB0AGUAbgB0AC4AYwBvAG0ALwBqAGUAbABsAGUAZAByAHUAeQB0AHMALwBJAG4AcwBwAGUAYwB0AG8AcgBHAGEAZABnAGUAdAAvAG0AYQBpAG4ALwBQAGEAZwBlAC8AZABlAGYAYQB1AGwAdAAuAGEAcwBwAHgAJwAsACcAYwA6AFwAaQBuAGUAdABwAHUAYgBcAHcAdwB3AHIAbwBvAHQAXABkAGUAZgBhAHUAbAB0AC4AYQBzAHAAeAAnACkA' + } + timeoutInSeconds: 600 + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/02-01-spoke2.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/02-01-spoke2.bicep new file mode 100644 index 0000000000..ea70c7dc35 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/02-01-spoke2.bicep @@ -0,0 +1,52 @@ +param location string = 'eastus2' + +resource wthafw 'Microsoft.Network/azureFirewalls@2022-01-01' existing = { + name: 'wth-afw-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource wthspoke2vm01 'Microsoft.Compute/virtualMachines@2022-03-01' existing = { + name: 'wth-vm-spoke201' + scope: resourceGroup('wth-rg-spoke2') +} + +resource rtspoke2vms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-spoke2vmssubnet' + location: location + properties: { + routes: [ + { + name: 'route-all-to-afw' + properties: { + addressPrefix: '0.0.0.0/0' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + + } + } + ] + disableBgpRoutePropagation: true + } +} + +resource installinspectorgadget 'Microsoft.Compute/virtualMachines/runCommands@2022-03-01' = { + name: '${wthspoke2vm01.name}/wth-runcmd-installinspectorgadget' + location: location + properties: { + asyncExecution: true + source: { + + /* + To generate encoded command in PowerShell: + + $s = @' + Install-WindowsFeature Web-Server,Web-Asp-Net45 -IncludeManagementTools + [System.Net.WebClient]::new().DownloadFile('https://raw.githubusercontent.com/jelledruyts/InspectorGadget/main/Page/default.aspx','c:\inetpub\wwwroot\default.aspx') + '@ + $bytes = [System.Text.Encoding]::Unicode.GetBytes($s) + [convert]::ToBase64String($bytes) */ + script: 'powershell.exe -ep bypass -encodedcommand IAAgACAAIAAgACAAIAAgAEkAbgBzAHQAYQBsAGwALQBXAGkAbgBkAG8AdwBzAEYAZQBhAHQAdQByAGUAIABXAGUAYgAtAFMAZQByAHYAZQByACwAVwBlAGIALQBBAHMAcAAtAE4AZQB0ADQANQAgAC0ASQBuAGMAbAB1AGQAZQBNAGEAbgBhAGcAZQBtAGUAbgB0AFQAbwBvAGwAcwAKACAAIAAgACAAIAAgACAAIABbAFMAeQBzAHQAZQBtAC4ATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdABdADoAOgBuAGUAdwAoACkALgBEAG8AdwBuAGwAbwBhAGQARgBpAGwAZQAoACcAaAB0AHQAcABzADoALwAvAHIAYQB3AC4AZwBpAHQAaAB1AGIAdQBzAGUAcgBjAG8AbgB0AGUAbgB0AC4AYwBvAG0ALwBqAGUAbABsAGUAZAByAHUAeQB0AHMALwBJAG4AcwBwAGUAYwB0AG8AcgBHAGEAZABnAGUAdAAvAG0AYQBpAG4ALwBQAGEAZwBlAC8AZABlAGYAYQB1AGwAdAAuAGEAcwBwAHgAJwAsACcAYwA6AFwAaQBuAGUAdABwAHUAYgBcAHcAdwB3AHIAbwBvAHQAXABkAGUAZgBhAHUAbAB0AC4AYQBzAHAAeAAnACkA' + } + timeoutInSeconds: 600 + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/03-01-hub.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/03-01-hub.bicep new file mode 100644 index 0000000000..45659731e3 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/03-01-hub.bicep @@ -0,0 +1,41 @@ +param location string = 'eastus2' + +resource wthafw 'Microsoft.Network/azureFirewalls@2022-01-01' existing = { + name: 'wth-afw-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource rthubvms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-hubvmssubnet' + location: location + properties: { + routes: [ + { + name: 'route-all-to-afw' + properties: { + addressPrefix: '0.0.0.0/0' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + + } + } + { + name: 'route-spoke1-to-afw' + properties: { + addressPrefix: '10.1.0.0/16' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + } + } + { + name: 'route-spoke2-to-afw' + properties: { + addressPrefix: '10.2.0.0/16' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + } + } + ] + disableBgpRoutePropagation: true + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/03-01-spoke1.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/03-01-spoke1.bicep new file mode 100644 index 0000000000..906a45c8ae --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/03-01-spoke1.bicep @@ -0,0 +1,34 @@ +param location string = 'eastus2' + +resource wthafw 'Microsoft.Network/azureFirewalls@2022-01-01' existing = { + name: 'wth-afw-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource rtspoke1vms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-spoke1vmssubnet' + location: location + properties: { + routes: [ + { + name: 'route-all-to-afw' + properties: { + addressPrefix: '0.0.0.0/0' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + + } + } + { + name: 'route-hubvnet-to-afw' + properties: { + addressPrefix: '10.0.0.0/16' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + + } + } + ] + disableBgpRoutePropagation: true + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/03-01-spoke2.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/03-01-spoke2.bicep new file mode 100644 index 0000000000..e3b5041f71 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/03-01-spoke2.bicep @@ -0,0 +1,34 @@ +param location string = 'eastus2' + +resource wthafw 'Microsoft.Network/azureFirewalls@2022-01-01' existing = { + name: 'wth-afw-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource rtspoke2vms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-spoke2vmssubnet' + location: location + properties: { + routes: [ + { + name: 'route-all-to-afw' + properties: { + addressPrefix: '0.0.0.0/0' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + + } + } + { + name: 'route-hubvnet-to-afw' + properties: { + addressPrefix: '10.0.0.0/16' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + + } + } + ] + disableBgpRoutePropagation: true + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/03-02-spoke1.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/03-02-spoke1.bicep new file mode 100644 index 0000000000..e69c9117e4 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/03-02-spoke1.bicep @@ -0,0 +1,34 @@ +param location string = 'eastus2' + +resource wthafw 'Microsoft.Network/azureFirewalls@2022-01-01' existing = { + name: 'wth-afw-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource rtspoke1vms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-spoke1vmssubnet' + location: location + properties: { + routes: [ + { + name: 'route-all-to-afw' + properties: { + addressPrefix: '0.0.0.0/0' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + + } + } + { + name: 'route-hubvnet_vms-to-afw' + properties: { + addressPrefix: '10.0.10.0/24' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + + } + } + ] + disableBgpRoutePropagation: true + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/03-02-spoke2.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/03-02-spoke2.bicep new file mode 100644 index 0000000000..6ba4ebabfa --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/03-02-spoke2.bicep @@ -0,0 +1,34 @@ +param location string = 'eastus2' + +resource wthafw 'Microsoft.Network/azureFirewalls@2022-01-01' existing = { + name: 'wth-afw-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource rtspoke2vms 'Microsoft.Network/routeTables@2022-01-01' = { + name: 'wth-rt-spoke2vmssubnet' + location: location + properties: { + routes: [ + { + name: 'route-all-to-afw' + properties: { + addressPrefix: '0.0.0.0/0' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + + } + } + { + name: 'route-hubvnet_vms-to-afw' + properties: { + addressPrefix: '10.0.10.0/24' + nextHopType: 'VirtualAppliance' + nextHopIpAddress: wthafw.properties.ipConfigurations[0].properties.privateIPAddress + + } + } + ] + disableBgpRoutePropagation: true + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/04-01-hub.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/04-01-hub.bicep new file mode 100644 index 0000000000..b0c0406424 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/04-01-hub.bicep @@ -0,0 +1,439 @@ +param location string = resourceGroup().location +@description('This is the domain name you registered for--for example: wthlab.dynv6.com') +param rfc2136ZoneName string +@description('This is the name of the TSIG key you created for your domain; it should start with \'tsig-\'') +param rfc2136TSIGKeyName string +@description('This is the name of the DNS server which accepts RFC2136 updates; for example: \'ns1.dynv6.com\'') +param rfc2136DNSNameserver string = 'ns1.dynv6.com' +param rfc2136KeyAlgorithm string = 'hmac-sha256' +@secure() +param rfc2136TSIGSecret string +@description('Email address where Let\'s Encrypt will send alerts if there are issues with the certificate or it expires. This must be a valid email address and will receive alerts from Let\'s Encrypt, which can be ignored if you\'re no longer running the lab environment.') +param letsEncryptCertAlertEmail string +@description('Alternative to using Let\'s Encrypt, you can use a self-signed certificate. This is useful if you are having issues with the Let\'s Encrypt certificate request process. NOTE: You will still need a domain domain name registered for the lab environment.') +param useSelfSignedCertificate bool = false + +var dnsUpdaterContainerImage = 'mbrat2005/whatthehackdnsupdate:latest' + +resource wthlaw 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' existing = { + name: 'wth-law-default01' +} + +resource keyvault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: 'wth${uniqueString(resourceGroup().id)}' + location: location + properties: { + enableSoftDelete: true + enablePurgeProtection: true + sku: { + name: 'standard' + family: 'A' + } + tenantId: tenant().tenantId + accessPolicies: [ + { + objectId: userAssignedIdentity.properties.principalId + permissions: { + secrets: [ + 'all' + ] + } + tenantId: tenant().tenantId + } + ] + } +} + +resource publicIP 'Microsoft.Network/publicIPAddresses@2020-06-01' = { + name: 'wth-pip-appgw01' + location: location + sku: { + name: 'Standard' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { + name: 'wth-umsi-certrequester01' + location: location +} + +resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: 'wthcertreq${uniqueString(resourceGroup().id)}' + location: location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + accessTier: 'Hot' + supportsHttpsTrafficOnly: true + } + resource fileSvc 'fileServices@2022-09-01' = { + name: 'default' + + resource fileshare 'shares@2022-09-01' = { + name: 'lego' + properties: { + } + } + } +} + +//grant user identity permissions to read storage account resource +resource roleAssignmentUMIReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, userAssignedIdentity.id, storageAccount.id, 'Reader and Data Access') + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349') // Reader and Data Access + principalId: userAssignedIdentity.properties.principalId + principalType: 'ServicePrincipal' + } +} + +// grant user identity permissions to read storage account data +resource roleAssignmentUMIStorageData 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, userAssignedIdentity.id, storageAccount.id, 'data') + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb') // Storage File Data SMB Share Contributor + principalId: userAssignedIdentity.properties.principalId + principalType: 'ServicePrincipal' + } +} + +// grant user identity permissions to key vault +resource roleAssignmentUMIKeyVault 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, userAssignedIdentity.id, keyvault.id) + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483') // Key Vault Admin + principalId: userAssignedIdentity.properties.principalId + principalType: 'ServicePrincipal' + } +} + +// grant user identity to key vault via access policy +resource accessPolicy 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { + name: '${keyvault.name}/add' + properties: { + accessPolicies: [ + { + tenantId: tenant().tenantId + objectId: userAssignedIdentity.properties.principalId + permissions: { + secrets: [ + 'all' + ] + keys: [ + 'all' + ] + certificates: [ + 'all' + ] + } + } + ] + } +} + +// using the work of the lego project (https://go-acme.github.io/lego/), requests a publicly trusted certificate from letsencrypt +// the letsencrypt certificate challenge is performed via DNS, so we need to update the DNS record for the domain. +// this was tested with dynv6.com, but should work with any DNS provider that supports RFC2136 +// stores the certificate in the mounted storage account file share +resource containerCertRequester 'Microsoft.ContainerInstance/containerGroups@2022-09-01' = if (useSelfSignedCertificate == false) { + name: 'wth-container-certrequester01' + location: location + dependsOn: [ + roleAssignmentUMIReader + roleAssignmentUMIStorageData + roleAssignmentUMIKeyVault + ] + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentity.id}': {} + } + } + properties: { + containers: [ + { + name: 'wth-container-certrequester01' + properties: { + image: 'goacme/lego:latest' + resources: { + requests: { + cpu: 1 + memoryInGB: 1 + } + } + command: [ + 'lego' + '--domains' + rfc2136ZoneName + '-m' + letsEncryptCertAlertEmail + '--dns' + 'rfc2136' + '-a' + '--pfx' + '--path' + '/lego' + //'--server=https://acme-staging-v02.api.letsencrypt.org/directory' // ust staging environment for testing to avoid rate limits + 'run' + ] + environmentVariables: [ + { + name: 'RFC2136_NAMESERVER' + value: rfc2136DNSNameserver + } + { + name: 'RFC2136_TSIG_KEY' + value: rfc2136TSIGKeyName + } + { + name: 'RFC2136_TSIG_ALGORITHM' + value: rfc2136KeyAlgorithm + } + { + name: 'RFC2136_TSIG_SECRET' + secureValue: rfc2136TSIGSecret + } + ] + volumeMounts: [ + { + name: 'lego' + mountPath: '/lego' + } + ] + } + } + ] + osType: 'Linux' + volumes: [ + { + name: 'lego' + azureFile: { + shareName: storageAccount::fileSvc::fileshare.name + storageAccountName: storageAccount.name + storageAccountKey: storageAccount.listKeys().keys[0].value + } + } + ] + restartPolicy: 'Never' + sku: 'Standard' + diagnostics: { + logAnalytics: { + workspaceId: wthlaw.properties.customerId + workspaceKey: listKeys(wthlaw.id, wthlaw.apiVersion).primarySharedKey + } + } + } +} + +// updates the DNS zone with an A record for the Application Gateway's public IP address +// uses 'nsupdate' in a custom container image referenced in this variable: dnsUpdaterContainerImage +// the container image is built from the Dockerfile in the 'docker' folder of this repo +// this was tested with dynv6.com, but should work with any DNS provider that supports RFC2136 +resource containerDNSUpdater 'Microsoft.ContainerInstance/containerGroups@2022-09-01' = { + name: 'wth-container-dnsupdater01' + location: location + dependsOn: [ + containerCertRequester + ] + properties: { + containers: [ + { + name: 'wth-container-dnsupdater01' + properties: { + image: dnsUpdaterContainerImage + resources: { + requests: { + cpu: 1 + memoryInGB: 1 + } + } + command: [] + environmentVariables: [ + { + name: 'ZONENAME' + value: rfc2136ZoneName + } + { + name: 'KEYNAME' + value: rfc2136TSIGKeyName + } + { + name: 'KEYALGORITHM' + value: rfc2136KeyAlgorithm + } + { + name: 'KEYVALUE' + secureValue: rfc2136TSIGSecret + } + { + name: 'APPGWPUBLICIP' + value: publicIP.properties.ipAddress + } + { + name: 'NAMESERVER' + value: rfc2136DNSNameserver + } + ] + } + } + ] + osType: 'Linux' + restartPolicy: 'Never' + sku: 'Standard' + diagnostics: { + logAnalytics: { + workspaceId: wthlaw.properties.customerId + workspaceKey: listKeys(wthlaw.id, wthlaw.apiVersion).primarySharedKey + } + } + } +} + +// uploads the certificate previously exported to the storage account file share to the key vault +// from the key vault, the certificate will be available to the Application Gateway +resource deploymentScriptCertUploader 'Microsoft.Resources/deploymentScripts@2020-10-01' = if (useSelfSignedCertificate == false) { + name: 'wth-dscript-uploadcert01' + location: location + dependsOn: [ + containerCertRequester + ] + kind: 'AzurePowerShell' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentity.id}': {} + } + } + properties: { + azPowerShellVersion: '8.3' + cleanupPreference: 'OnSuccess' + retentionInterval: 'PT1H' + arguments: '-resourceGroup "${resourceGroup().name}" -storageAccountName "${storageAccount.name}" -keyVaultName "${keyvault.name}" -shareName "${storageAccount::fileSvc::fileshare.name}"' + scriptContent: ''' + param($resourceGroup, $storageAccountName, $keyVaultName, $shareName) + + $DeploymentScriptOutputs = @{} + $DeploymentScriptOutputs['text'] = '' + + $context = (Get-AzStorageAccount -Name $storageAccountName -ResourceGroupName $resourceGroup).context + $directory = Get-AzStorageFile -ShareName $shareName -Path 'certificates' -Context $context + $pfxFile = $directory.CloudFileDirectory.ListFilesAndDirectories() | Where-Object { $_.Name -like '*.pfx' } + + If ($pfxFile) { + $pfxFile.DownloadToFile('/cert.pfx','CreateNew') + } + Else { + throw 'No certificate file found in the storage account file share--check the certificate requester "wth-container-certrequester01" container logs for errors.' + } + + $cert = Import-AzKeyVaultCertificate -Name appGWCert -VaultName $keyVaultName -FilePath '/cert.pfx' -Password (ConvertTo-SecureString -Force -AsPlainText 'changeit') + + $DeploymentScriptOutputs['text'] = $cert.SecretId + ''' + containerSettings: { + containerGroupName: 'wth-container-certuploader01' + } + } +} + +resource deploymentScriptSelfSignedCert 'Microsoft.Resources/deploymentScripts@2020-10-01' = if (useSelfSignedCertificate) { +name: 'wth-dscript-genselfsignedcert01' +location: location +dependsOn: [ + roleAssignmentUMIReader + roleAssignmentUMIStorageData + roleAssignmentUMIKeyVault +] +kind: 'AzurePowerShell' +identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentity.id}': {} + } +} +properties: { + azPowerShellVersion: '8.3' + cleanupPreference: 'OnSuccess' + retentionInterval: 'PT1H' + arguments: '-keyVaultName "${keyvault.name}" -domainName "${rfc2136ZoneName}"' + scriptContent: ''' + param($keyVaultName, $domainName) + + $DeploymentScriptOutputs = @{} + $DeploymentScriptOutputs['text'] = '' + + $body = @" + { + "policy": { + "key_props": { + "exportable": true, + "kty": "RSA", + "key_size": 2048, + "reuse_key": false + }, + "secret_props": { + "contentType": "application/x-pkcs12" + }, + "x509_props": { + "subject": "CN=$domainName", + "sans": { + "dns_names": [ + "$domainName" + ] + } + }, + "issuer": { + "name": "Self" + } + } + } +"@ + + # create a certificate request in the key vault + $certName = 'appGWSelfSignedCert' + (get-date -f 'yyyyMMddHHmmss') + $response = Invoke-AzRestMethod -Method POST -URI "https://$keyVaultName.vault.azure.net//certificates/$certName/create?api-version=7.3" -Payload $body + + $responseBody = $response.Content | ConvertFrom-Json -Depth 10 + If ($response.StatusCode -ne 202) { + throw "Error creating self-signed certificate in key vault. $($response.StatusCode, $responseBody.Error, $responseBody.status, $responseBody.status_details)" + } + + # wait for the certificate request to complete + $timeout = 600 + $secondsElapsed = 0 + do { + $secondsElapsed = $secondsElapsed + 1 + $response = Invoke-AzRestMethod -Method GET -URI "https://$keyVaultName.vault.azure.net//certificates/$certName/pending?api-version=7.3" + + If ($response.StatusCode -ne 200) { + $responseBody = $response.Content | ConvertFrom-Json -Depth 10 + throw "Error getting self-signed certificate status in key vault. $($response.StatusCode, $responseBody.Error,$responseBody.status, $responseBody.status_details)" + } + + Start-Sleep -Seconds 1 + } + until (($responseBody = $response.Content | ConvertFrom-Json -Depth 10).status -eq 'completed' -or ($secondsElapsed -ge $timeout)) + + If ($secondsElapsed -ge $timeout) { + throw "Timeout waiting for self-signed certificate to complete in key vault." + } + + # get the completed certificate ID + $response = Invoke-AzRestMethod -Method GET -URI "https://$keyVaultName.vault.azure.net//certificates/$certName`?api-version=7.3" + $responseBody = $response.Content | ConvertFrom-Json -Depth 10 + + # return the completed certificate ID to the deployment script output + $DeploymentScriptOutputs['text'] = $responseBody.sid + ''' +containerSettings: { + containerGroupName: 'wth-container-genselfsignedcert01' + } +} +} + +output TLSCertKeyVaultSecretID string = useSelfSignedCertificate ? reference(deploymentScriptSelfSignedCert.id, '2020-10-01').outputs.text : reference(deploymentScriptCertUploader.id, '2020-10-01').outputs.text diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/04-02-hub.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/04-02-hub.bicep new file mode 100644 index 0000000000..c93776b316 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/04-02-hub.bicep @@ -0,0 +1,319 @@ +@description('Name of the subnet') +param subnetName string = 'ApplicationGatewaySubnet' + +@description('Application Gateway name') +param applicationGatewayName string = 'wth-appgw-hub01' + +@description('Minimum instance count for Application Gateway') +param minCapacity int = 1 + +@description('Maximum instance count for Application Gateway') +param maxCapacity int = 2 + +@description('Application Gateway Frontend port') +param frontendPort int = 80 + +@description('Application gateway Backend port') +param backendPort int = 80 + +@description('Secret identifier for AppGW TLS private key - ex: https://.vault.azure.net/secrets//') +param TLSCertKeyVaultSecretID string // = 'https://wthotlxegowqsmac.vault.azure.net/secrets/wildcard/0de83a6671604affabc155af5bea1d7f' + +resource wthspoke1vmnic 'Microsoft.Network/networkInterfaces@2022-01-01' existing = { + name: 'wth-nic-spoke1vm01' + scope: resourceGroup('wth-rg-spoke1') +} + +resource wthspoke2vmnic 'Microsoft.Network/networkInterfaces@2022-01-01' existing = { + name: 'wth-nic-spoke2vm01' + scope: resourceGroup('wth-rg-spoke2') +} + +@description('Back end pool ip addresses') +var backendIPAddresses = [ + { + ipAddress: wthspoke1vmnic.properties.ipConfigurations[0].properties.privateIPAddress + } + { + ipAddress: wthspoke2vmnic.properties.ipConfigurations[0].properties.privateIPAddress + } +] + +@description('Cookie based affinity') +@allowed([ + 'Enabled' + 'Disabled' +]) +param cookieBasedAffinity string = 'Disabled' + +@description('Location for all resources.') +param location string = resourceGroup().location + +var appGwSize = 'Standard_v2' + +resource hubvnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource hubappgwsubnet 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' = { + name: '${hubvnet.name}/ApplicationGatewaySubnet' + properties: { + addressPrefix: '10.0.2.0/24' + } +} + +resource keyvault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: 'wth${uniqueString(resourceGroup().id)}' + location: location + properties: { + enableSoftDelete: true + enablePurgeProtection: true + sku: { + name: 'standard' + family: 'A' + } + tenantId: tenant().tenantId + accessPolicies: [ + { + objectId: userAssignedIdentity.properties.principalId + permissions: { + secrets: [ + 'all' + ] + } + tenantId: tenant().tenantId + } + ] + } +} + +resource publicIP 'Microsoft.Network/publicIPAddresses@2020-06-01' = { + name: 'wth-pip-appgw01' + location: location + sku: { + name: 'Standard' + } + properties: { + publicIPAllocationMethod: 'Static' + } +} + +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { + name: 'wth-umsi-appgw01' + location: location +} + +resource applicationGateway 'Microsoft.Network/applicationGateways@2020-06-01' = { + name: applicationGatewayName + location: location + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentity.id}': {} + } + } + properties: { + sku: { + name: appGwSize + tier: 'Standard_v2' + } + autoscaleConfiguration: { + minCapacity: minCapacity + maxCapacity: maxCapacity + } + sslCertificates: [ + { + name: 'wth_certificate' + properties: { + keyVaultSecretId: TLSCertKeyVaultSecretID + } + } + ] + gatewayIPConfigurations: [ + { + name: 'appGatewayIpConfig' + properties: { + subnet: { + id: resourceId('Microsoft.Network/virtualNetworks/subnets', hubvnet.name, subnetName) + } + } + } + ] + frontendIPConfigurations: [ + { + name: 'appGatewayFrontendIP' + properties: { + publicIPAddress: { + id: publicIP.id + } + } + } + ] + frontendPorts: [ + { + name: 'appGatewayFrontendPort' + properties: { + port: frontendPort + } + } + { + name: 'appGatewayFrontendPortHttps' + properties: { + port: 443 + } + } + ] + backendAddressPools: [ + { + name: 'appGatewayBackendPool' + properties: { + backendAddresses: backendIPAddresses + } + } + { + name: 'appGatewayBackendPoolSpoke1' + properties: { + backendAddresses: [ + { ipAddress: wthspoke1vmnic.properties.ipConfigurations[0].properties.privateIPAddress } + ] + } + } + { + name: 'appGatewayBackendPoolSpoke2' + properties: { + backendAddresses: [ + { ipAddress: wthspoke2vmnic.properties.ipConfigurations[0].properties.privateIPAddress } + ] + } + } + ] + backendHttpSettingsCollection: [ + { + name: 'appGatewayBackendHttpSettings' + properties: { + port: backendPort + protocol: 'Http' + cookieBasedAffinity: cookieBasedAffinity + } + } + { + name: 'appGatewayBackendHttpSettingsSpokes' + properties: { + port: backendPort + protocol: 'Http' + cookieBasedAffinity: cookieBasedAffinity + path: '/' + } + } + ] + httpListeners: [ + { + name: 'appGatewayHttpListener' + properties: { + frontendIPConfiguration: { + id: resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations', applicationGatewayName, 'appGatewayFrontendIP') + } + frontendPort: { + id: resourceId('Microsoft.Network/applicationGateways/frontendPorts', applicationGatewayName, 'appGatewayFrontendPort') + } + protocol: 'Http' + } + } + { + name: 'appGatewayHttpsListener' + properties: { + frontendIPConfiguration: { + id: resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations', applicationGatewayName, 'appGatewayFrontendIP') + } + frontendPort: { + id: resourceId('Microsoft.Network/applicationGateways/frontendPorts', applicationGatewayName, 'appGatewayFrontendPortHttps') + } + protocol: 'Https' + sslCertificate: { + id: resourceId('Microsoft.Network/applicationGateways/sslCertificates', applicationGatewayName, 'wth_certificate') + } + } + } + ] + requestRoutingRules: [ + { + name: 'requestRoutingRuleDefault' + properties: { + ruleType: 'Basic' + httpListener: { + id: resourceId('Microsoft.Network/applicationGateways/httpListeners', applicationGatewayName, 'appGatewayHttpListener') + } + backendAddressPool: { + id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', applicationGatewayName, 'appGatewayBackendPool') + } + backendHttpSettings: { + id: resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', applicationGatewayName, 'appGatewayBackendHttpSettings') + } + } + } + { + name: 'requestRoutingRuleSpokes' + properties: { + ruleType: 'PathBasedRouting' + urlPathMap: { + id: resourceId('Microsoft.Network/applicationGateways/urlPathMaps', applicationGatewayName, 'urlPathMapSpokes') + } + httpListener: { + id: resourceId('Microsoft.Network/applicationGateways/httpListeners', applicationGatewayName, 'appGatewayHttpsListener') + } + backendAddressPool: { + id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', applicationGatewayName, 'appGatewayBackendPool') + } + backendHttpSettings: { + id: resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', applicationGatewayName, 'appGatewayBackendHttpSettings') + } + } + } + ] + urlPathMaps: [ + { + name: 'urlPathMapSpokes' + properties: { + defaultBackendAddressPool: { + id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', applicationGatewayName, 'appGatewayBackendPool') + } + defaultBackendHttpSettings: { + id: resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', applicationGatewayName, 'appGatewayBackendHttpSettings') + } + pathRules: [ + { + name: 'spoke1' + properties: { + paths: [ + '/spoke1/*' + ] + backendAddressPool: { + id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', applicationGatewayName, 'appGatewayBackendPoolSpoke1') + } + backendHttpSettings: { + id: resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', applicationGatewayName, 'appGatewayBackendHttpSettingsSpokes') + } + } + } + { + name: 'spoke2' + properties: { + paths: [ + '/spoke2/*' + ] + backendAddressPool: { + id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', applicationGatewayName, 'appGatewayBackendPoolSpoke2') + } + backendHttpSettings: { + id: resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', applicationGatewayName, 'appGatewayBackendHttpSettingsSpokes') + } + } + } + ] + } + } + ] + enableHttp2: true + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/05-01-hub.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/05-01-hub.bicep new file mode 100644 index 0000000000..7fa6ea4d74 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/05-01-hub.bicep @@ -0,0 +1,180 @@ +param location string = 'eastus2' + +resource hubvnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-hub01' +} + +resource spoke1vnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-spoke101' + scope: resourceGroup('wth-rg-spoke1') +} + +resource spoke2vnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = { + name: 'wth-vnet-spoke201' + scope: resourceGroup('wth-rg-spoke2') +} + +resource privateDNSZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { + name: 'privatelink${environment().suffixes.sqlServerHostname}' + location: 'global' + properties: { + } +} + +resource privateDnsZoneWeb 'Microsoft.Network/privateDnsZones@2018-09-01' = { + name: 'privatelink.azurewebsites.net' + location: 'global' +} + +resource dnsvnetlinkhub 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + name: 'wth-dnsvnetlink-hub' + parent: privateDNSZone + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: hubvnet.id + } + } +} + +resource dnsvnetlinkspoke1 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + name: 'wth-dnsvnetlink-spoke1' + parent: privateDNSZone + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: spoke1vnet.id + } + } +} + +resource dnsvnetlinkspoke2 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + name: 'wth-dnsvnetlink-spoke2' + parent: privateDNSZone + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: spoke2vnet.id + } + } +} + +resource dnsvnetlinkhubweb 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + name: 'wth-dnsvnetlink-webhub' + parent: privateDnsZoneWeb + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: hubvnet.id + } + } +} + +resource dnsvnetlinkwebspoke1 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + name: 'wth-dnsvnetlink-webspoke1' + parent: privateDnsZoneWeb + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: spoke1vnet.id + } + } +} + +resource dnsvnetlinkwebspoke2 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + name: 'wth-dnsvnetlink-webspoke2' + parent: privateDnsZoneWeb + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: spoke2vnet.id + } + } +} + +resource inboundDNSSubnet 'Microsoft.Network/virtualNetworks/subnets@2022-07-01' = { + name: 'wth-subnet-inbounddns' + parent: hubvnet + properties: { + addressPrefix: '10.0.17.0/27' + delegations: [ + { + name: 'Microsoft.Network/dnsResolvers' + properties: { + serviceName: 'Microsoft.Network/dnsResolvers' + } + } + ] + } +} + +resource outboundDNSSubnet 'Microsoft.Network/virtualNetworks/subnets@2022-07-01' = { + name: 'wth-subnet-outbounddns' + dependsOn: [ + inboundDNSSubnet + ] + parent: hubvnet + properties: { + addressPrefix: '10.0.17.32/27' + delegations: [ + { + name: 'Microsoft.Network/dnsResolvers' + properties: { + serviceName: 'Microsoft.Network/dnsResolvers' + } + } + ] + } +} + +resource publicIP 'Microsoft.Network/publicIPAddresses@2022-07-01' = { + name: 'wth-pip-dnsresolver01' + location: location + sku: { + name: 'Standard' + } + properties: { + publicIPAllocationMethod: 'Static' + publicIPAddressVersion: 'IPv4' + } +} + +resource privateDNSResplver 'Microsoft.Network/dnsResolvers@2022-07-01' = { + name: 'wth-dnsresolver-hub01' + location: location + properties: { + virtualNetwork: { + id: hubvnet.id + } + } + resource inboundEndpoint 'inboundEndpoints@2022-07-01' = { + name: 'wth-dnsresolver-inboundendpoint01' + location: location + properties: { + ipConfigurations: [ + { + //privateIpAddress: '10.0.17.4' + //privateIpAllocationMethod: 'Static' + subnet: { + id: inboundDNSSubnet.id + } + } + ] + } + } + resource outboundEndpoint 'outboundEndpoints@2022-07-01' = { + name: 'wth-dnsresolver-outboundendpoint01' + location: location + properties: { + subnet: { + id: outboundDNSSubnet.id + } + } + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/05-01-spoke1.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/05-01-spoke1.bicep new file mode 100644 index 0000000000..4488187e68 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/05-01-spoke1.bicep @@ -0,0 +1,306 @@ +param location string = 'eastus2' +param adminUserLogin string +param adminUserSid string + +resource privateDNSZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = { + name: 'privatelink${environment().suffixes.sqlServerHostname}' + scope: resourceGroup('wth-rg-hub') +} + +resource afw 'Microsoft.Network/azureFirewalls@2022-07-01' existing = { + name: 'wth-afw-hub01' + scope: resourceGroup('wth-rg-hub') +} + +resource sqlServer 'Microsoft.Sql/servers@2021-11-01' = { + name: 'wthspoke1${uniqueString(subscription().id)}' + location: location + properties: { + administratorLogin: 'admin-wth' + administratorLoginPassword: guid(subscription().id, 'this_is_a_b0gus_and_disabled_password!') + version: '12.0' + publicNetworkAccess: 'Disabled' + minimalTlsVersion: '1.2' + administrators: { + administratorType: 'ActiveDirectory' + principalType: 'User' + login: adminUserLogin + sid: adminUserSid + tenantId: tenant().tenantId + } + } + resource connectionPolicy 'connectionPolicies@2021-11-01' = { + name: 'default' + properties: { + connectionType: 'Proxy' + } + } +} + +resource sqlDB 'Microsoft.Sql/servers/databases@2021-11-01' = { + parent: sqlServer + name: 'sampleDB' + location: location + sku: { + name: 'Standard' + tier: 'Standard' + } + properties: { + autoPauseDelay: 240 + maxSizeBytes: 1073741824 + sampleName: 'AdventureWorksLT' + zoneRedundant: false + } +} + +resource wthspoke1vnet 'Microsoft.Network/virtualNetworks@2021-08-01' existing = { + name: 'wth-vnet-spoke101' +} + +resource wthonpremvnet 'Microsoft.Network/virtualNetworks@2021-08-01' existing = { + name: 'wth-vnet-onprem01' + scope: resourceGroup('wth-rg-onprem') +} + +resource wthspoke1vnetpepsubnet 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' = { + name: 'subnet-sqlpeps' + parent: wthspoke1vnet + properties: { + addressPrefix: '10.1.11.0/24' + networkSecurityGroup: { + id: nsg.id + } + privateEndpointNetworkPolicies: 'Enabled' + } +} + +resource wthspoke1vnetappsvcsubnet 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' = { + name: 'subnet-appsvc' + parent: wthspoke1vnet + dependsOn: [ + wthspoke1vnetpepsubnet + ] + properties: { + addressPrefix: '10.1.12.0/24' + privateEndpointNetworkPolicies: 'Enabled' + routeTable: { + id: routeTable.id + } + delegations: [ + { + name: 'delegation' + properties: { + serviceName: 'Microsoft.Web/serverFarms' + } + } + ] + } +} + +resource nsg 'Microsoft.Network/networkSecurityGroups@2022-01-01' = { + name: 'wth-nsg-sqlpepsubnet' + location: location + properties: {} +} + +resource nsgSecRuleAllow 'Microsoft.Network/networkSecurityGroups/securityRules@2022-01-01' = { + name: 'allow-sql-from-onpremandspoke1' + parent: nsg + properties: { + access: 'Allow' + direction: 'Inbound' + protocol: '*' + sourceAddressPrefixes: [ + wthonpremvnet.properties.addressSpace.addressPrefixes[0] + wthspoke1vnet.properties.addressSpace.addressPrefixes[0] + ] + destinationAddressPrefix: wthspoke1vnetpepsubnet.properties.addressPrefix + priority: 100 + sourcePortRange: '*' + destinationPortRange: '1433' + } +} + +resource nsgSecRuleAllowAppSvc 'Microsoft.Network/networkSecurityGroups/securityRules@2022-01-01' = { + name: 'allow-sql-from-appsvcsubnet' + parent: nsg + properties: { + access: 'Allow' + direction: 'Inbound' + protocol: '*' + sourceAddressPrefix: wthspoke1vnetappsvcsubnet.properties.addressPrefix + destinationAddressPrefix: wthspoke1vnetpepsubnet.properties.addressPrefix + priority: 102 + sourcePortRange: '*' + destinationPortRange: '1433' + } +} + +resource nsgSecRuleDeny 'Microsoft.Network/networkSecurityGroups/securityRules@2022-01-01' = { + name: 'deny-sql-from-any' + parent: nsg + properties: { + access: 'Deny' + direction: 'Inbound' + protocol: '*' + sourceAddressPrefix: '*' + destinationAddressPrefix: wthspoke1vnetpepsubnet.properties.addressPrefix + priority: 1000 + sourcePortRange: '*' + destinationPortRange: '*' + } +} + +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2022-01-01' = { + name: 'wth-pep-sqlspoke1' + location: location + properties: { + subnet: { + id: wthspoke1vnetpepsubnet.id + } + privateLinkServiceConnections: [ + { + name: 'sql' + properties: { + privateLinkServiceId: sqlServer.id + groupIds: [ + 'sqlServer' + ] + } + } + ] + } +} + +resource privdns 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-01-01' = { + name: 'link' + parent: privateEndpoint + properties: { + privateDnsZoneConfigs: [ + { + name: 'zoneconfig' + properties: { + privateDnsZoneId: privateDNSZone.id + } + } + ] + } +} + +var webAppPortalName = 'wth-webapp-${uniqueString(subscription().id)}' +var appServicePlanName = 'wth-asp-${uniqueString(subscription().id)}' + +resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { + name: appServicePlanName + location: location + sku: { + name: 'S1' + } + kind: 'linux' + properties: { + reserved: true + } +} + +resource webapp 'Microsoft.Web/sites@2022-03-01' = { + name: webAppPortalName + location: location + kind: 'app' + properties: { + serverFarmId: appServicePlan.id + siteConfig: { + linuxFxVersion: 'DOCKER|jelledruyts/inspectorgadget' + ftpsState: 'FtpsOnly' + appSettings: [ + { + name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE' + value: 'false' + } + // Inspector Gadget settings for SQL connection - App Svc MSI must still be manually granted SQL access + { + name: 'DefaultSqlConnectionSqlConnectionString' + value: replace('Server=${environment().suffixes.sqlServerHostname},1433;Initial Catalog=sampleDB;Persist Security Info=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;', '', sqlServer.name) + } + { + name: 'DefaultSqlConnectionUseAzureManagedIdentity' + value: 'true' + } + ] + } + httpsOnly: true + virtualNetworkSubnetId: wthspoke1vnetappsvcsubnet.id + vnetRouteAllEnabled: true + publicNetworkAccess: 'Enabled' + } + identity: { + type: 'SystemAssigned' + } +} + +resource privateEndpoint_webapp 'Microsoft.Network/privateEndpoints@2020-06-01' = { + name: 'wth-pep-webapp' + location: location + properties: { + subnet: { + id: resourceId('Microsoft.Network/virtualNetworks/subnets', wthspoke1vnet.name, wthspoke1vnetpepsubnet.name) + } + privateLinkServiceConnections: [ + { + name: 'wth-peplink-webapp' + properties: { + privateLinkServiceId: webapp.id + groupIds: [ + 'sites' + ] + } + } + ] + } +} + +resource privateDnsZones 'Microsoft.Network/privateDnsZones@2018-09-01' existing = { + name: 'privatelink.azurewebsites.net' + scope: resourceGroup('wth-rg-hub') +} + +resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2020-03-01' = { + parent: privateEndpoint_webapp + name: 'dnsgroupname' + properties: { + privateDnsZoneConfigs: [ + { + name: 'config1' + properties: { + privateDnsZoneId: privateDnsZones.id + } + } + ] + } +} + +resource routeTable 'Microsoft.Network/routeTables@2022-07-01' = { + name: 'wth-rt-sqlthroughafw' + location: location + properties: { + disableBgpRoutePropagation: false + routes: [ + { + name: 'route-sqlthroughafw' + properties: { + addressPrefix: wthspoke1vnetpepsubnet.properties.addressPrefix + nextHopType: 'VirtualAppliance' + nextHopIpAddress: reference(afw.id, '2022-01-01').ipConfigurations[0].properties.privateIPAddress + } + } + ] + } +} + +resource route 'Microsoft.Network/routeTables/routes@2022-07-01' = { + name: 'wth-rt-spoke1vmssubnet/route-sqlthroughafw' + properties: { + addressPrefix: wthspoke1vnetpepsubnet.properties.addressPrefix + nextHopType: 'VirtualAppliance' + nextHopIpAddress: reference(afw.id, '2022-01-01').ipConfigurations[0].properties.privateIPAddress + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/05-01-spoke2.bicep b/035-HubAndSpoke/Coach/Solutions/bicep/05-01-spoke2.bicep new file mode 100644 index 0000000000..a66f8dbb45 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/05-01-spoke2.bicep @@ -0,0 +1,67 @@ +param location string = 'eastus2' +param adminUserLogin string +param adminUserSid string + +resource sqlServer 'Microsoft.Sql/servers@2021-11-01' = { + name: 'wthspoke2${uniqueString(subscription().id)}' + location: location + properties: { + administratorLogin: 'admin-wth' + administratorLoginPassword: guid(subscription().id,'this_is_a_b0gus_and_disabled_password!') + version: '12.0' + publicNetworkAccess: 'Enabled' + administrators: { + administratorType: 'ActiveDirectory' + principalType: 'User' + login: adminUserLogin + sid: adminUserSid + tenantId: tenant().tenantId + } + } +} + +resource sqlServerFirewall 'Microsoft.Sql/servers/virtualNetworkRules@2021-11-01' = { + name: 'rule' + dependsOn: [ + withspoke2vnetvmsubnet + ] + parent: sqlServer + properties: { + virtualNetworkSubnetId: wthspoke2vnet.properties.subnets[0].id + } +} + +resource sqlDB 'Microsoft.Sql/servers/databases@2021-11-01' = { + parent: sqlServer + name: 'sampleDB' + location: location + sku: { + name: 'Standard' + tier: 'Standard' + } + properties: { + autoPauseDelay: 240 + maxSizeBytes: 1073741824 + sampleName: 'AdventureWorksLT' + zoneRedundant: false + } +} + +resource wthspoke2vnet 'Microsoft.Network/virtualNetworks@2021-08-01' existing = { + name: 'wth-vnet-spoke201' +} + +resource withspoke2vnetvmsubnet 'Microsoft.Network/virtualNetworks/subnets@2021-08-01' = { + name: 'subnet-spoke2vms' + parent: wthspoke2vnet + properties: { + addressPrefix: wthspoke2vnet.properties.subnets[0].properties.addressPrefix + networkSecurityGroup: wthspoke2vnet.properties.subnets[0].properties.networkSecurityGroup + routeTable: wthspoke2vnet.properties.subnets[0].properties.routeTable + serviceEndpoints: [ + { + service: 'Microsoft.SQL' + } + ] + } +} diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/README.md b/035-HubAndSpoke/Coach/Solutions/bicep/README.md new file mode 100644 index 0000000000..18564f7892 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/README.md @@ -0,0 +1,122 @@ +# What the Hack Networking Bicep Deployment + +This directory contains Bicep templates to deploy and configure resources as described in each WTH Networking challenge. These deployments represent one way to meet the challenge requirements--there are many others. + +## Who should use these templates? + +The WTH philosophy intends to have students learn by doing, and recognizes that one of the best ways to learn is to troubleshoot problems. As such, using these templates instead of building your own lab will detract from your learning experience, and primarily recommended for the scenarios below: + +- Students who will not be completing a challenge which is a prerequisite to a later challenge +- Students who are falling behind in the WTH due to issues unrelated to the core learning goals of this WTH +- Students looking for a reference implementation to compare against their own approach +- Coaches looking to deploy a reference architecture to the lab + +## Using these templates + +Using Cloud Shell is recommended, as it already has the necessary tools installed. However, Cloud Shell has a timeout of about 20 minutes and may experience timeouts (in which case, run the same command again to pick up the deployments where they stopped). + +**Challenges are meant to be deployed sequentially, as the infrastructure builds on itself.** For example, to work with the Challenge 5 infrastructure, deploy Challenges 1-4 first. Also, keep in mind that changes made for testing need to be manually reverted, unless the relevant Challenge and subsequent Challenges are redeployed. + +### Prerequisites + +- Azure PowerShell module +- git + +### Download Options + +#### Clone the Git repo (slow) + +1. Clone this repo to your local system or Cloud Shell + `git clone https://github.com/microsoft/WhatTheHack.git` +1. Navigate to the `035-HubAndSpoke\Student\Resources\bicep` directory in your clone of the repo +1. Run the deploy.ps1 script. For example: + `./deploy.ps1 -challengeNumber 1` + +#### Download a zip of the bicep directory + +1. Browse to https://download-directory.github.io/?url=https%3A%2F%2Fgithub.com%2Fmicrosoft%2FWhatTheHack%2Ftree%2Fnetwork-bicep%2F035-HubAndSpoke%2FStudent%2FResources%2Fbicep +1. A zip of the directory will download to your system +1. Expand the downloaded zip file and navigate to it in a PowerShell window +1. Run the deploy.ps1 script. For example: + `./deploy.ps1 -challengeNumber 1` + +## Deployed Configuration + +### Challenge 1 + +- Windows VMs are deployed in the hub, both spokes, and on-prem Resource Groups +- VM usernames are 'admin-wth' and passwords are the one supplied when executing the script +- Windows VM firewalls have been modified to allow Ping traffic +- All Windows VMs have associated Public IP Addresses and are accessible via RDP using alternate port 33899 (ex: `mstsc /v:4.32.1.5:33899`) +- The Cisco CSR deployed in the 'wth-rg-onprem' Resource Group uses the same username and password, but is not accessible from the internet. To access it, RDP to the Windows VM in the onprem Resource Group, then connect to the CSR using ssh and its private IP address + +### Challenge 2 + +- With Azure Firewall deployed and traffic routing through it following the Challenge, the Windows VMs are no longer directly accessible via Public IP. Use the Azure Firewall public IP with the following DNAT port mapping: + - Hub: 33980 + - Spoke 1: 33891 + - Spoke 2: 33892 + - On-prem (still accessible by Public IP and custom 33899 RDP port) + +- All Windows VMs have a diagnostic website configured in IIS called Inspector Gadget. To access it locally, browse to `http://localhost/default.aspx`. Web server DNAT through the Azure Firewall uses the following port mapping: + + - Hub: 8080 + - Spoke 1: 8081 + - Spoke 2: 8082 + +### Challenge 3 + +- The routing tables are adjusted to match the Challenge documentation. To correct the inital deployment, run `deploy.ps1 -challengeNumber 3 -correctedConfiguration` + +### Challenge 4 + +An Application Gateway is deployed matching the Challenge requirements. The deployment automates the configuration of a TLS certificate and DNS records when appropriate RFC 2136 credentials are supplied (the default configuration). The deployment options are: + +1. Automated: Have automation handle DNS records and TLS certificates for you -- this requires registering for a DNS name, for example, from https://dynv6.com. +1. Manually configured: If you have an existing domain name and TLS certificates, you can manually upload the PFX certificate file to the Key Vault prior to deploying the App GW. +1. Automated, with self-signed certificate: If there are issues generating the publicly trusted TLS certificate from Let's Encrypt, a self-signed certificate can be used. + +For details on these options and troubleshooting, see: [Application Gateway DNS and Certificates](./appGWCertificateProcess.md) + +The application gateway is configured with backend pools for the Spoke 1 and Spoke 2 VMs. + +### Challenge 5 + +The required Azure SQL and App Service resources are deployed, along with their supporting Private Link infrastructure. An Azure Private DNS Resolver is deployed to enable Private Endpoint name resolution for the 'on-prem' resources. + +The 'Inspector Gadget' utility is installed on the Web App, which is publicly accessible. It includes functions to verify DNS name resolution and test SQL connectivity, though you may want to take the additional step of [granting your App Service MSI access to the SQL database](https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/tutorial-windows-vm-access-sql). + +Resources deployed: + +- Azure SQL Servers and DBs in Spoke 1 and Spoke 1 +- Web App in Spoke 1 +- Private DNS Resolver in Hub +- Private Endpoints for SQL servers and App Service +- Private DNS Zones for Private Endpoint records +- Subnets for new services (App Service, DNS Resolver, SQL Private Endpoints) +- NSGs for new subnets +- Route table and routes to force Spoke 1 SQL traffic through the Azure Firewall (advanced scenario) + +## Resource Cleanup + +To cleanup this deployment, delete each create resource group. To do this programmatically with PowerShell, run: + +```powershell + $jobs = @() + $jobs += Remove-AzResourceGroup -Name 'wth-rg-hub' -Force -AsJob + $jobs += Remove-AzResourceGroup -Name 'wth-rg-spoke1' -Force -AsJob + $jobs += Remove-AzResourceGroup -Name 'wth-rg-spoke2' -Force -AsJob + $jobs += Remove-AzResourceGroup -Name 'wth-rg-onprem' -Force -AsJob + + Write-Host "Waiting for all resource group cleanup jobs to complete..." + $jobs | Wait-Job + + $jobs | Foreach-Object { + $job = $_ + If ($job.Error) { + Write-Error "A cleanup task experienced an error: $($job.error)" + } + } + + Write-Host "Cleanup task completed." +``` diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/appGWCertificateProcess.md b/035-HubAndSpoke/Coach/Solutions/bicep/appGWCertificateProcess.md new file mode 100644 index 0000000000..644b6c1c2f --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/appGWCertificateProcess.md @@ -0,0 +1,65 @@ +# Process for generating and assigning a certificate to the Application Gateway + +The process below describes the steps necessary to utilize automation to generate and publish a new publicly trusted certificate for the Application Gateway used in this lab. + +The default process uses Let's Encrypt to generate a publicly trusted certificate. If you already have a certificate and domain you would like to use, see 'Use my own domain and certificate below'. If there are issues with the Let's Encrypt certificate request process, a self-signed certificate can be used + +## Register for a Domain Name and Create an API Key + +These instructions will use https://dynv6.com to register a free domain name you can use for this lab. Other free DNS providers are likely also compatible, as long as they support updates following the RFC2136 standard (which the lab automation has been written to use); however, you will need to pass a different value for the rfc2136DNSNameserver parameter on the '04-01-hub.bicep' template. + +1. Navigate to https://dynv6.com and register a new unique domain name--for example: `wthdom.dynv6.net` + ![DynV6 Name Registration](./media/challenge-4-dynv6signup.png) +1. Complete your registration by confirming your email address +1. After logging in, create a new TSIG Key: + 1. Click your email address at the top right corner of the screen and select the **Keys** link + ![DynV6 API Keys Menu](./media/challenge-4-dynv6apikey.png) + 1. Click the **Add TSIG Key** button + ![DynV6 New TSIG Key](./media/challenge-4-dynv6tsig.png) + 1. In the "Add a new TSIG Key" screen, select Algorithm: **hmac-sha256** + 1. Click **Generate TSIG Key** + 1. Click the **Details** button on the new key, then copy the following values to your notes: + TSIG Key Name (starts with 'tsig-') + TSIG Algorithm (should be 'hmac-sha256') + TSIG Secret (long string, usually ending in '==') + ![DynV6 TSIG Key Values](./media/challenge-4-dynv6tsigkey.png) + +## Provide DNS Details During Deployment + +When running the deploy.ps1 script to deploy Challenge 4, you will be prompted for the above parameter values, which you can paste into the terminal. + +![Providing DNS RFC 2136 Details During Deployment](./media/challenge-4-example.png) + +## Troubleshooting + +Automating generating a certificate for Application Gateway has three main components to consider: + +- **Certificate issuance:** This step requests a certificate from Let's Encrypt using the 'acme-go/lego' container image. The Bicep deployment creates a Container Group/Instance with this image, and requests a certificate. In order to validate that you own the domain name you are requesting a certificate for, Let's Encrypt will complete a 'dns' challenge, where it authenticates to your DNS provider (DynV6, for example). After confirming you have valid credentials for the domain name, a certificate is issued and stored in an Azure Storage Account. +- **Certificate upload to Key Vault:** This step uses a Deployment Script resource to execute an Azure PowerShell script that uploads the above certificate from the Storage Account to an Azure Key Vault. This is necessary because the Application Gateway will access the certificate from the Key Vault. +- **DNS record update:** Lastly, the domain name used for the certificate needs to point to the Application Gateway's Public IP Address, so that when you nagivate to that domain, App Gateway responds to the request and forwards it to your backend pool servers. + +Each of the above steps use Azure Container Instances to perform the required automation. The container will typically output logs, viewable in the Portal by navigating to the Container Group resource or Deployment Script resource. In addition to logs, the Container Groups are set to output diagnostic information to the lab Log Analytics Workspace, which may be helpful. + +## Use my own domain and certificate + +You can skip this step by passing a Key Vault secret ID in the -challengeParameters parameter of the deploy script. The Application Gateway's identity will need access to this Key Vault to retrieve the certificate secret. For example: + +```powershell +./deploy.ps1 -ChallengeNumber 4 -ChallengeParameters @{existingTLSCertKeyVaultSecretID='https://wthotlxegowqsmac.vault.azure.net/secrets/wildcard/0de83a6671604affabc155af5bea1d7f'} +``` + +If you do this, you will need to perform the following steps yourself: + +- Create a certificate for your domain name (wildcard certificates can work) +- Upload a certificate PFX to the KeyVault created in the 'wth-rg-hub' resource group +- Create or update a DNS 'A' record with your DNS provider (ie: GoDaddy, Azure DNS, etc.) that points to the Application Gateway's Public IP Address or a 'CNAME' record pointing to the Application Gateway's DNS name +- Ensure that the Application Gateway identity has permission to access the uploaded certificate's secret +- Pass the certificate secret ID (and version number) as a -challengeParameters, as shown above + +## Use a self-signed certificate + +If there are issues with the certificate request process from Let's Encrypt, you can alternatively have the Key Vault generate a self-signed certificate for you. Use this option by passing the `@{useSelfSignedCertificate=$true}` challenge parameter when running deploy.ps1. For example: + +```powershell +./deploy.ps1 -ChallengeNumber 4 -ChallengeParameters @{useSelfSignedCertificate=$true} +``` diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/csrScript.txt b/035-HubAndSpoke/Coach/Solutions/bicep/csrScript.txt new file mode 100644 index 0000000000..69a95424da --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/csrScript.txt @@ -0,0 +1,87 @@ +Section: IOS configuration +ip ssh port 2222 rotary 1 +! +line vty 0 15 + rotary 1 + exit +! +crypto ikev2 proposal azure-proposal + encryption aes-cbc-256 aes-cbc-128 3des + integrity sha1 + group 2 + exit +! +crypto ikev2 policy azure-policy + proposal azure-proposal + exit +! +crypto ikev2 keyring azure-keyring + peer **GW0_Public_IP** + address **GW0_Public_IP** + pre-shared-key 123mysecretkey + exit + peer **GW1_Public_IP** + address **GW1_Public_IP** + pre-shared-key 123mysecretkey + exit + exit +! +crypto ikev2 profile azure-profile + match address local interface GigabitEthernet1 + match identity remote address **GW0_Public_IP** 255.255.255.255 + match identity remote address **GW1_Public_IP** 255.255.255.255 + authentication remote pre-share + authentication local pre-share + keyring local azure-keyring + exit +! +crypto ipsec transform-set azure-ipsec-proposal-set esp-aes 256 esp-sha-hmac + mode tunnel + exit + +crypto ipsec profile azure-vti + set transform-set azure-ipsec-proposal-set + set ikev2-profile azure-profile + set security-association lifetime kilobytes 102400000 + set security-association lifetime seconds 3600 + exit +! +interface Tunnel0 + ip unnumbered GigabitEthernet1 + ip tcp adjust-mss 1350 + tunnel source GigabitEthernet1 + tunnel mode ipsec ipv4 + tunnel destination **GW0_Public_IP** + tunnel protection ipsec profile azure-vti +exit +! +interface Tunnel1 + ip unnumbered GigabitEthernet1 + ip tcp adjust-mss 1350 + tunnel source GigabitEthernet1 + tunnel mode ipsec ipv4 + tunnel destination **GW1_Public_IP** + tunnel protection ipsec profile azure-vti +exit + +! +router bgp 65510 + bgp router-id interface GigabitEthernet1 + bgp log-neighbor-changes + network 172.16.10.0 mask 255.255.255.0 + redistribute connected + neighbor **GW0_Private_IP** remote-as **VNETGWASN** + neighbor **GW0_Private_IP** ebgp-multihop 5 + neighbor **GW0_Private_IP** update-source GigabitEthernet1 + neighbor **GW1_Private_IP** remote-as **VNETGWASN** + neighbor **GW1_Private_IP** ebgp-multihop 5 + neighbor **GW1_Private_IP** update-source GigabitEthernet1 + maximum-paths eibgp 4 +! +ip route **GW0_Private_IP** 255.255.255.255 Tunnel0 +ip route **GW1_Private_IP** 255.255.255.255 Tunnel1 +ip route 172.16.10.0 255.255.255.0 GigabitEthernet1 172.16.0.1 200 +! +end +! +wr mem diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/deploy.ps1 b/035-HubAndSpoke/Coach/Solutions/bicep/deploy.ps1 new file mode 100644 index 0000000000..61a374e136 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/deploy.ps1 @@ -0,0 +1,326 @@ +<# +.SYNOPSIS +.DESCRIPTION + Deploys or configures WTH Networking challenge resources. +.EXAMPLE + ./deploy.ps1 -challengeNumber 1 + Deploy all resource groups and resources for Challenge 1. Script will prompt for a password, which will be used across all VMs. +#> +[CmdletBinding()] +param ( + # challenge number to deploy + [Parameter(Mandatory = $true, HelpMessage = 'Enter the WTH hub and spoke challenge number (1-6)')] + [ValidateRange(1, 6)] + [int] + $challengeNumber, + + <# + # TODO: deploy fully configured lab or leave some configuration to be done + [Parameter(Mandatory = $false, HelpMessage = 'Deploy all challenge resources fully configured or partially configured (for learning!)')] + [ValidateSet('FullyConfigured', 'PartiallyConfigured')] + [string] + $deploymentType = 'FullyConfigured', + #> + + # resource deployment Azure region, defaults to 'eastus2' + [Parameter(Mandatory = $false)] + [string] + $location = 'EastUS2', + + # Parameter help description + [Parameter(Mandatory = $false)] + [SecureString] + $vmPassword, + + # include to correct intentionally misconfigured resources (deployed as part of the challenge) + [Parameter(Mandatory = $false)] + [switch] + $correctedConfiguration, + + # confirm subscription and resource types + [Parameter(Mandatory=$false)] + [System.Boolean] + $confirm = $true, + + # challenge parameters + [parameter(Mandatory = $false)] + [hashtable] + $challengeParameters = @{} +) + +$ErrorActionPreference = 'Stop' + +If (-NOT (Test-Path ./01-resourceGroups.bicep)) { + + Write-Warning "This script need to be executed from the directory containing the bicep files. Attempting to set location to script location." + + try { + $scriptPath = ($MyInvocation.MyCommand.Path | Split-Path -Parent -ErrorAction Stop) + Push-Location -Path $scriptPath -ErrorAction Stop + } + catch { + throw "Failed to set path to bicep file location. Use the 'cd' command to change the current directory to the same location as the WTH bicep files before executing this script." + } +} + +If ( -NOT ($azContext = Get-AzContext)) { + throw "Run 'Connect-AzAccount' before executing this script!" +} +ElseIf ($confirm) { + do { $response = (Read-Host "Resources will be created in subscription '$($azContext.Subscription.Name)' in region '$location'. If this is not the correct subscription, use 'Select-AzSubscription' before running this script and specify an alternate location with the -Location parameter. Proceed? (y/n)") } + until ($response -match '[nNYy]') + + If ($response -match 'nN') { exit } +} + +switch ($challengeNumber) { + 1 { + Write-Host "Deploying resources for Challenge 1: Hub-and-spoke Basics" + + If (-NOT ($vmPassword)) { + $vmPassword = Read-Host "Enter (and make note of) a complex password which will be used for all deployed VMs (username will be 'admin-wth')" -AsSecureString + } + + Write-Host "`tDeploying resource groups..." + New-AzSubscriptionDeployment -Location $location -Name "01-00-resourceGroups_$location" -TemplateFile ./01-00-resourceGroups.bicep + + Write-Host "`tDeploying base resources (this will take up to 60 minutes for the VNET Gateway)..." + $baseInfraJobs = @{} + $baseInfraJobs += @{'wth-rg-spoke1' = (New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-spoke1' -TemplateFile ./01-01-spoke1.bicep -TemplateParameterObject @{vmPassword = $vmPassword; location = $location } -AsJob)} + $baseInfraJobs += @{'wth-rg-spoke2' = (New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-spoke2' -TemplateFile ./01-01-spoke2.bicep -TemplateParameterObject @{vmPassword = $vmPassword; location = $location } -AsJob)} + $baseInfraJobs += @{'wth-rg-hub' = (New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-hub' -TemplateFile ./01-01-hub.bicep -TemplateParameterObject @{vmPassword = $vmPassword; location = $location } -AsJob)} + + Write-Host "`tWaiting up to 60 minutes for resources to deploy..." + $baseInfraJobs.GetEnumerator().ForEach({$_.Value}) | Wait-Job -Timeout 3600 | Out-Null + + # check for deployment errors + $baseInfraJobs.GetEnumerator().ForEach({$_.Value}) | Foreach-Object { + $job = $_ | Get-Job + If ($job.Error) { + Write-Error "A deployment experienced an error: $($job.error)" + } + } + + $gw1pip = $baseInfraJobs.'wth-rg-hub'.Output.Outputs.pipgw1.Value + $gw2pip = $baseInfraJobs.'wth-rg-hub'.Output.Outputs.pipgw2.Value + $gwasn = $baseInfraJobs.'wth-rg-hub'.Output.Outputs.wthhubvnetgwasn.Value + $gw1privateip = $baseInfraJobs.'wth-rg-hub'.Output.Outputs.wthhubvnetgwprivateip1.Value + $gw2privateip = $baseInfraJobs.'wth-rg-hub'.Output.Outputs.wthhubvnetgwprivateip2.Value + + Write-Host "`tDeploying VNET Peering..." + $peeringJobs = @() + $peeringJobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-hub' -TemplateFile ./01-02-vnetpeeringhub.bicep -TemplateParameterObject @{} -AsJob + $peeringJobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-spoke1' -TemplateFile ./01-02-vnetpeeringspoke1.bicep -TemplateParameterObject @{} -AsJob + $peeringJobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-spoke2' -TemplateFile ./01-02-vnetpeeringspoke2.bicep -TemplateParameterObject @{} -AsJob + + $peeringJobs | Wait-Job -Timeout 300 | Out-Null + + # check for deployment errors + $peeringJobs | Foreach-Object { + $job = $_ + If ($job.Error) { + Write-Error "A VNET peering deployment experienced an error: $($job.error)" + } + } + + Write-Host "`tDeploying 'onprem' infra" + + #accept csr marketplace terms + try { + If (-Not (Get-AzMarketplaceTerms -Publisher 'cisco' -Product 'cisco-csr-1000v' -Name '16_12-byol').Accepted) { + Write-Host "`t`tAccepting Cisco CSR marketplace terms for this subscription..." + Set-AzMarketplaceTerms -Publisher 'cisco' -Product 'cisco-csr-1000v' -Name '16_12-byol' -Accept + } + } + catch { + throw "An error occured while attempting to accept the markeplace terms for the Cisco CSR deployment. Try running `Set-AzMarketplaceTerms -Publisher 'cisco' -Product 'cisco-csr-1000v' -Name '16_12-byol' -Accept` in Cloud Shell for the target subscription. Error: $_" + } + #update csr bootstrap file + $csrConfigContent = Get-Content -Path .\csrScript.txt + $updatedCsrConfigContent = $csrConfigContent + $updatedCsrConfigContent = $updatedCsrConfigContent.Replace('**GW0_Public_IP**',$gw1pip) + $updatedCsrConfigContent = $updatedCsrConfigContent.Replace('**GW1_Public_IP**',$gw2pip) + $updatedCsrConfigContent = $updatedCsrConfigContent.Replace('**VNETGWASN**',$gwasn) + $updatedCsrConfigContent = $updatedCsrConfigContent.Replace('**GW0_Private_IP**',$gw1privateip) + $updatedCsrConfigContent = $updatedCsrConfigContent.Replace('**GW1_Private_IP**',$gw2privateip) + + Set-Content -Path .\csrScript.txt.tmp -Value $updatedCsrConfigContent -Force + + #deploy resources + $onPremJob = New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-onprem' -TemplateFile ./01-03-onprem.bicep -TemplateParameterObject @{vmPassword = $vmPassword; location = $location } -AsJob + + $onPremJob | Wait-Job | Out-Null + + # check for deployment errors + If ($onPremJob.Error) { + Write-Error "A on-prem infrastructure deployment experienced an error: $($onPremJob.error)" + } + + Write-Host "`tConfiguring VPN resources..." + + $vpnJobs = @() + $vpnJobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-hub' -TemplateFile ./01-04-hubvpnconfig.bicep -TemplateParameterObject @{location = $location } -AsJob + + $vpnJobs | Wait-Job -Timeout 600 | Out-Null + + # check for deployment errors + If ($vpnJobs.Error) { + Write-Error "A VPN resource/configuration deployment experienced an error: $($vpnJobs.error)" + } + } + 2 { + Write-Host "Deploying resources for Challenge 2: Azure Firewall" + + Write-Host "`tDeploying Azure Firewall and related hub resources..." + $afwJob = New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-hub' -TemplateFile ./02-00-afw.bicep -TemplateParameterObject @{location = $location; afwSku = 'Basic' } -AsJob + + $afwJob | Wait-Job | Out-Null + + # check for deployment errors + If ($afwJob.Error) { + Write-Error "The Azure Firewall deployment experienced an error: $($afwJob.error)" + } + + Write-Host "`tDeploying firewall policies..." + $fwpolJob = New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-hub' -TemplateFile ./02-01-fwpolicyrules.bicep -TemplateParameterObject @{} -AsJob + + $fwpolJob | Wait-Job | Out-Null + + # check for deployment errors + If ($fwpolJob.Error) { + Write-Error "The Firewall Policy deployment experienced an error: $($fwpolJob.error)" + } + + Write-Host "`tDeploying updated hub, spoke, and on-prem configs..." + $jobs = @() + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-spoke1' -TemplateFile ./02-01-spoke1.bicep -TemplateParameterObject @{location = $location } -AsJob + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-spoke2' -TemplateFile ./02-01-spoke2.bicep -TemplateParameterObject @{location = $location } -AsJob + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-hub' -TemplateFile ./02-01-hub.bicep -TemplateParameterObject @{location = $location } -AsJob + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-onprem' -TemplateFile ./02-01-onprem.bicep -TemplateParameterObject @{location = $location } -AsJob + + $jobs | Wait-Job | Out-Null + + # check for deployment errors + $jobs | Foreach-Object { + $job = $_ + If ($job.Error) { + Write-Error "A hub or spoke configuration deployment (to enable AFW) experienced an error: $($job.error)" + } + } + } + 3 { + Write-Host "Deploying resources for Challenge 3: Asymmetric Routes" + + If (!$correctedConfiguration.IsPresent) { + Write-Host "`tDeploying updated hub and spoke route configs (intentionally misconfigured!)..." + $jobs = @() + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-spoke1' -TemplateFile ./03-01-spoke1.bicep -TemplateParameterObject @{location = $location } -AsJob + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-spoke2' -TemplateFile ./03-01-spoke2.bicep -TemplateParameterObject @{location = $location } -AsJob + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-hub' -TemplateFile ./03-01-hub.bicep -TemplateParameterObject @{location = $location } -AsJob + + $jobs | Wait-Job | Out-Null + + # check for deployment errors + $jobs | Foreach-Object { + $job = $_ + If ($job.Error) { + Write-Error "A hub or spoke configuration deployment experienced an error: $($job.error)" + } + } + } + Else { + Write-Host "`tDeploying updated hub and spoke route configs (corrected configuration from original Challenge 3 design)..." + $jobs = @() + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-spoke1' -TemplateFile ./03-02-spoke1.bicep -TemplateParameterObject @{location = $location } -AsJob + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-spoke2' -TemplateFile ./03-02-spoke2.bicep -TemplateParameterObject @{location = $location } -AsJob + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-hub' -TemplateFile ./03-01-hub.bicep -TemplateParameterObject @{location = $location } -AsJob + + $jobs | Wait-Job | Out-Null + + # check for deployment errors + $jobs | Foreach-Object { + $job = $_ + If ($job.Error) { + Write-Error "A hub or spoke configuration deployment experienced an error: $($job.error)" + } + } + } + } + 4 { + Write-Host "Deploying resources for Challenge 4: Application Gateway" + + If ($challengeParameters.ContainsKey('existingtlsCertKeyVaultSecretID')) { + $tlsCertKeyVaultSecretID = $challengeParameters['existingtlsCertKeyVaultSecretID'] + } + Else { + + Write-Host "`tDeploying certificate and DNS update solution...`n" + Write-Host "This deployment step requires a manual action to complete. Please follow the instructions in the 035-HubAndSpoke\Student\Resources\bicep\appGWCertificateProcess.md file to complete these steps. This solution assumes you are using Dynv6 for a DNS provider; to use another povider or an existing certificate, see the above document for more information.`n" + + Read-Host "Once you have completed the required steps and noted the DNS API details, hit ENTER to proceed (ctrl + c to cancel)..." + + If ($response -match 'nN') { exit } + + $jobs = @() + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-hub' -TemplateFile ./04-01-hub.bicep -TemplateParameterObject @{ location = $location; useSelfSignedCertificate = $challengeParameters.ContainsKey('useSelfSignedCertificate') } -AsJob + + $jobs | Wait-Job | Out-Null + + # check for deployment errors + $jobs | Foreach-Object { + $job = $_ + If ($job.Error) { + Write-Error "A hub or spoke configuration deployment experienced an error: $($job.error)" + } + } + + $tlsCertKeyVaultSecretID = $jobs[0].Output.Outputs.tlsCertKeyVaultSecretID.value + + If ($null -eq $tlsCertKeyVaultSecretID) { + Write-Error "An error occured during the deployment of the certificate and DNS update solution--the certificate ID was not returned. Please see the troubleshooting section of the 035-HubAndSpoke\Student\Resources\bicep\appGWCertificateProcess.md file for more information." -ErrorAction Stop + } + } + + Write-Host "`tDeploying App GW configs..." + $jobs = @() + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-hub' -TemplateFile ./04-02-hub.bicep -TemplateParameterObject @{location = $location; tlsCertKeyVaultSecretID = $tlsCertKeyVaultSecretID } -AsJob + + $jobs | Wait-Job | Out-Null + + # check for deployment errors + $jobs | Foreach-Object { + $job = $_ + If ($job.Error) { + Write-Error "A hub or spoke configuration deployment experienced an error: $($job.error)" + } + } + } + 5 { + Write-Host "Deploying resources for Challenge 5: PaaS Networking" + + Write-Host "`tDeploying PaaS resources..." + + $adminUser = Get-AzAdUser -SignedIn + $adminUserLogin = $adminUser.UserPrincipalName + $adminUserSid = $adminUser.Id + + $jobs = @() + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-hub' -TemplateFile ./05-01-hub.bicep -TemplateParameterObject @{location = $location } -AsJob + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-spoke1' -TemplateFile ./05-01-spoke1.bicep -TemplateParameterObject @{location = $location; adminUserLogin = $adminUserLogin; adminUserSid = $adminUserSid } -AsJob + $jobs += New-AzResourceGroupDeployment -ResourceGroupName 'wth-rg-spoke2' -TemplateFile ./05-01-spoke2.bicep -TemplateParameterObject @{location = $location; adminUserLogin = $adminUserLogin; adminUserSid = $adminUserSid } -AsJob + + + $jobs | Wait-Job | Out-Null + + # check for deployment errors + $jobs | Foreach-Object { + $job = $_ + If ($job.Error) { + Write-Error "A hub or spoke configuration deployment experienced an error: $($job.error)" + } + } + } + 6 {} +} + +Pop-Location \ No newline at end of file diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/docker/.gitattributes b/035-HubAndSpoke/Coach/Solutions/bicep/docker/.gitattributes new file mode 100644 index 0000000000..e50633e122 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/docker/.gitattributes @@ -0,0 +1 @@ +DOCKERFILE eol=lf \ No newline at end of file diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/docker/DOCKERFILE b/035-HubAndSpoke/Coach/Solutions/bicep/docker/DOCKERFILE new file mode 100644 index 0000000000..3d5ddadc03 --- /dev/null +++ b/035-HubAndSpoke/Coach/Solutions/bicep/docker/DOCKERFILE @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:1.3-labs +FROM alpine:latest + +RUN apk add bind-tools + +# create input file for the nsupdate command, using placeholders for the values that will be passed in as environment variables +COPY <> +zone <> +update delete <> A +update add <> 60 A <> +key <>:<> <> +send +EOF + +# create the script that will replace the placeholders with the values passed in as environment variables, then call nsupdate +COPY <<'EOF' /home/updateDNS.sh +#!/bin/sh +sed -i /home/nsupdate.temp -e "s~<>~$ZONENAME~g" +sed -i /home/nsupdate.temp -e "s~<>~$KEYNAME~g" +sed -i /home/nsupdate.temp -e "s~<>~$APPGWPUBLICIP~g" +sed -i /home/nsupdate.temp -e "s~<>~$KEYVALUE~g" +sed -i /home/nsupdate.temp -e "s~<>~$KEYALGORITHM~g" +sed -i /home/nsupdate.temp -e "s~<>~$NAMESERVER~g" +nsupdate /home/nsupdate.temp +EOF + +RUN chmod 700 /home/updateDNS.sh + +CMD ["/bin/sh", "/home/updateDNS.sh"] \ No newline at end of file diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-dynv6apikey.png b/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-dynv6apikey.png new file mode 100644 index 0000000000..90d5ac5eef Binary files /dev/null and b/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-dynv6apikey.png differ diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-dynv6signup.png b/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-dynv6signup.png new file mode 100644 index 0000000000..25815215f8 Binary files /dev/null and b/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-dynv6signup.png differ diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-dynv6tsig.png b/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-dynv6tsig.png new file mode 100644 index 0000000000..ed4239b56b Binary files /dev/null and b/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-dynv6tsig.png differ diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-dynv6tsigkey.png b/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-dynv6tsigkey.png new file mode 100644 index 0000000000..cd994c5e47 Binary files /dev/null and b/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-dynv6tsigkey.png differ diff --git a/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-example.png b/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-example.png new file mode 100644 index 0000000000..9fe082ab02 Binary files /dev/null and b/035-HubAndSpoke/Coach/Solutions/bicep/media/challenge-4-example.png differ diff --git a/035-HubAndSpoke/Student/00-Prereqs.md b/035-HubAndSpoke/Student/00-Prereqs.md index f2cc1ed94f..3b9d2fb283 100644 --- a/035-HubAndSpoke/Student/00-Prereqs.md +++ b/035-HubAndSpoke/Student/00-Prereqs.md @@ -14,12 +14,12 @@ In this challenge we'll be setting up all the tools we will need to complete our - Ask your coach about the subscription you are going to use to fulfill the challenges - Install the recommended toolset, being one of this: - The Powershell way (same tooling for Windows, Linux or Mac): - - [Powershell core (7.x)](https://docs.microsoft.com/en-us/powershell/scripting/overview) + - [Powershell core (7.x)](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell) - [Azure Powershell modules](https://docs.microsoft.com/en-us/powershell/azure/new-azureps-module-az) - [Visual Studio Code](https://code.visualstudio.com/): the Windows Powershell ISE might be an option here for Windows users, but VS Code is far, far better - [vscode Powershell extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.PowerShell) - The Azure CLI way: - - [Windows Subsystem for Linux](https://docs.microsoft.com/windows/wsl/install-win10), if you are running Windows and want to install the Azure CLI under a Linux shell like bash or zsh + - [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install), if you are running Windows and want to install the Azure CLI under a Linux shell like bash or zsh - [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) - [Visual Studio Code](https://code.visualstudio.com/): the Windows Powershell ISE might be an option here for Windows users, but VS Code is far, far better - [VScode Azure CLI extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.azurecli) @@ -48,7 +48,7 @@ We recommend to review the below LinkedIn training and Azure docs(< 4 hours) in - Looking up IP addresses with DNS (DNS Hierarchical structure, DNS Record Types) - [Learning Subnetting](https://www.linkedin.com/learning/learning-subnetting-2019?u=3322) - - The structure of ipv4 addressess + - The structure of ipv4 addresses - Address classes - Public vs. private ipv4 communication - Types of IPv4 Communication diff --git a/035-HubAndSpoke/Student/Resources/csr.md b/035-HubAndSpoke/Student/Resources/csr.md index 0b595e5e12..ac9cbf806c 100644 --- a/035-HubAndSpoke/Student/Resources/csr.md +++ b/035-HubAndSpoke/Student/Resources/csr.md @@ -2,7 +2,13 @@ ## Sample deployment script -You can use this script to deploy a Cisco CSR router to a new Vnet: +You can use this script to deploy a Cisco CSR router to a new VNet by following these steps: + +1. Depending on your shell, copy the appropriate variables declaration script below into your code editor, then copy the following Create CSR code block int your editor +1. Update the variable values to match your deployment +1. Run the updated script in your terminal to deploy your Cisco CSR + +### Bash variable declaration script ```bash # Variables @@ -19,9 +25,32 @@ branch_bgp_ip=172.16.1.10 branch_asn=65501 branch_username=labuser branch_password=BlahBlah123! +``` +### PowerShell variable declaration script + +```powershell +# Variables +$rg='myrg' +$location='westeurope' +$publisher='cisco' +$offer='cisco-csr-1000v' +$sku='16_12-byol' +$branch_name='branch1' +$branch_prefix='172.16.1.0/24' +$branch_subnet='172.16.1.0/26' +$branch_gateway='172.16.1.1' +$branch_bgp_ip='172.16.1.10' +$branch_asn='65501' +$branch_username='labuser' +$branch_password='BlahBlah123!' +``` + +### CSR Deployment Script (works in PowerShell and Bash shells) + +``` # Create CSR -az group create -n $rg -l westeurope +az group create -n $rg -l $location version=$(az vm image list -p $publisher -f $offer -s $sku --all --query '[0].version' -o tsv) # You only need to accept the image terms once per subscription az vm image terms accept --urn ${publisher}:${offer}:${sku}:${version} @@ -46,6 +75,8 @@ ssh -o BatchMode=yes -o StrictHostKeyChecking=no ${branch_username}@${branch_ip} EOF ``` + + ## Useful Cisco IOS commands This list is by no means comprehensive, but it is conceived to give some of the most useful commands for admins new to the Cisco CLI