When deploying an “edge” or “perimeter” network in Azure, by way of a peered edge VNET or an edge subnet, you’ll likely want to deploy virtual firewall appliances of some kind to manage and control that ingress and egress traffic. This comes at a cost though. That cost being that Azure services are generally accessed via public IP addresses or hosts, even within Azure. The most common of those and one that has come up recently is Azure Blob storage.
If you have ExpressRoute, you can get around this by implementing Public Peering. This essentially sends all traffic destined for Azure services to your ER gateway. A bottleneck? Perhaps.
Recently I ran into a road block on a customers site around the use of Blob storage. I designed an edge network that met certain traffic monitoring requirements. Azure NSGs were not able to meet all requirements, so, something considerably more complex and time consuming was implemented. It’s IT, isn’t that what always happens you may ask?
Here’s some reference blog posts:
WE deployed Cisco Firepower Threat Defence virtual appliance firewalls in an edge VNET. Our subnets had route tables with a default route of 0.0.0.0/0 directed to the “tag” “VirtualAppliance”. So all traffic to a host or network not known by Azure is directed to the firewall(s). How that can be achieved is another blog post.
When implementing this solution, Azure services that are accessed via an external or public range IP address or host, most commonly Blob Storage which is accessed via blah.blob.core.windows.net, additionally gets directed to the Cisco FTDs. Not a big problem, create some rules to allow traffic flow etc and we’re cooking with gas.
Not exactly the case as the FTDv’s have a NIC with a throughput of 2GiB’s per second. That’s plenty fast, but, when you have a lot of workloads, a lot of user traffic, a lot of writes to Blob storage, bottle necks can occur.
As I mentioned earlier this can be tackled quickly through a number of methods. These discarded methods in this situation are as follows:
Like I said, both above will work.
The solution in this blob post is a little bit more technical, but, does away with the above. Rather than any manual work, lets automate this process though AzureAutomation. Thankfully, this isn’t something new, but, isn’t something that is used often. Through the use of pre-configured Azure Automation modules and Powershell scripts, a scheduled weekly or monthly (or whatever you like) runbook to download the Microsoft publicly available .xml file that lists all subnets and IP addresses use in Azure. Then uses that file to update a route table the script creates with routes to Azure subnets and IP’s in a region that is specified.
This process does away with any manual intervention and works to the ethos “work smarter, not harder”. I like that very much, and no, that is not being lazy. It’s being smart.
The high five
I’m certainly not trying to take the credit for this, except for the minor tweak to the runbook code, so cheers to James Bannan (@JamesBannan) who wrote this great blog post (available here) on the solution. He’s put together the Powershell script that expands on a Powershell module written by Kieran Jacobson (@kjacobsen). Check out their Twitters, their blogs and all round awesome content for. Thank you and high five to both!
I’m going to speed through this as its really straight forward and nothing to complicated here. The only tricky part is the order in doing that. Stick to the order and you’re guaranteed to succeed:
$VerbosePreference = 'Continue'
### Authenticate with Azure Automation account
$cred = "AzureRunAsConnection"
try
{
# Get the connection "AzureRunAsConnection "
$servicePrincipalConnection=Get-AutomationConnection -Name $cred
"Logging in to Azure..."
Add-AzureRmAccount `
-ServicePrincipal `
-TenantId $servicePrincipalConnection.TenantId `
-ApplicationId $servicePrincipalConnection.ApplicationId `
-CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint
}
catch {
if (!$servicePrincipalConnection)
{
$ErrorMessage = "Connection $cred not found."
throw $ErrorMessage
} else{
Write-Error -Message $_.Exception
throw $_.Exception
}
}
### Populate script variables from Azure Automation assets
$resourceGroupName = Get-AutomationVariable -Name 'virtualNetworkRGName'
$resourceLocation = Get-AutomationVariable -Name 'virtualNetworkRGLocation'
$vNetName = Get-AutomationVariable -Name 'virtualNetworkName'
$azureRegion = Get-AutomationVariable -Name 'azureDatacenterRegions'
$azureRegionSearch = '*' + $azureRegion + '*'
[array]$locations = Get-AzureRmLocation | Where-Object {$_.Location -like $azureRegionSearch}
### Retrieve the nominated virtual network and subnets (excluding the gateway subnet)
$vNet = Get-AzureRmVirtualNetwork `
-ResourceGroupName $resourceGroupName `
-Name $vNetName
[array]$subnets = $vnet.Subnets | Where-Object {$_.Name -ne 'GatewaySubnet'} | Select-Object Name
### Create and populate a new array with the IP ranges of each datacenter in the specified location
$ipRanges = @()
foreach($location in $locations){
$ipRanges += Get-MicrosoftAzureDatacenterIPRange -AzureRegion $location.DisplayName
}
$ipRanges = $ipRanges | Sort-Object
### Iterate through each subnet in the virtual network
foreach($subnet in $subnets){
$RouteTableName = $subnet.Name + '-RouteTable'
$vNet = Get-AzureRmVirtualNetwork `
-ResourceGroupName $resourceGroupName `
-Name $vNetName
### Create a new route table if one does not already exist
if ((Get-AzureRmRouteTable -Name $RouteTableName -ResourceGroupName $resourceGroupName) -eq $null){
$RouteTable = New-AzureRmRouteTable `
-Name $RouteTableName `
-ResourceGroupName $resourceGroupName `
-Location $resourceLocation
}
### If the route table exists, save as a variable and remove all routing configurations
else {
$RouteTable = Get-AzureRmRouteTable `
-Name $RouteTableName `
-ResourceGroupName $resourceGroupName
$routeConfigs = Get-AzureRmRouteConfig -RouteTable $RouteTable
foreach($config in $routeConfigs){
Remove-AzureRmRouteConfig -RouteTable $RouteTable -Name $config.Name | Out-Null
}
}
### Create a routing configuration for each IP range and give each a descriptive name
foreach($ipRange in $ipRanges){
$routeName = ($ipRange.Region.Replace(' ','').ToLower()) + '-' + $ipRange.Subnet.Replace('/','-')
Add-AzureRmRouteConfig `
-Name $routeName `
-AddressPrefix $ipRange.Subnet `
-NextHopType Internet `
-RouteTable $RouteTable | Out-Null
}
### Add default route for Edge Firewalls
Add-AzureRmRouteConfig `
-Name 'DefaultRoute' `
-AddressPrefix 0.0.0.0/0 `
-NextHopType VirtualAppliance `
-NextHopIpAddress 10.10.10.10 `
-RouteTable $RouteTable
### Include a routing configuration to give direct access to Microsoft's KMS servers for Windows activation
Add-AzureRmRouteConfig `
-Name 'AzureKMS' `
-AddressPrefix 23.102.135.246/32 `
-NextHopType Internet `
-RouteTable $RouteTable
### Apply the route table to the subnet
Set-AzureRmRouteTable -RouteTable $RouteTable
$forcedTunnelVNet = $vNet.Subnets | Where-Object Name -eq $subnet.Name
$forcedTunnelVNet.RouteTable = $RouteTable
### Update the virtual network with the new subnet configuration
Set-AzureRmVirtualNetwork -VirtualNetwork $vnet -Verbose
}
I’ve made two changes to the original script. These changes are as follows: I changed the authentication to use an Azure Automation account. This streamlined the deployment process so I could reuse the script across another of subscriptions. This change was the following:
$cred = "AzureRunAsConnection"
try
{
# Get the connection "AzureRunAsConnection "
$servicePrincipalConnection=Get-AutomationConnection -Name $cred
"Logging in to Azure..."
Add-AzureRmAccount `
-ServicePrincipal `
-TenantId $servicePrincipalConnection.TenantId `
-ApplicationId $servicePrincipalConnection.ApplicationId `
-CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint
}
catch {
if (!$servicePrincipalConnection)
{
$ErrorMessage = "Connection $cred not found."
throw $ErrorMessage
} else{
Write-Error -Message $_.Exception
throw $_.Exception
}
}
Secondly, I added an additional static route. This was for the default route (0.0.0.0/0) which is used to forward to our edge firewalls. This change was the following:
### Add default route for Edge Firewalls
Add-AzureRmRouteConfig `
-Name 'DefaultRoute' `
-AddressPrefix 0.0.0.0/0 `
-NextHopType VirtualAppliance `
-NextHopIpAddress 10.10.10.10 `
-RouteTable $RouteTable
You can re-use this section to add further custom static routes