What is – Azure Route Server

Back in March 2021 at Ignite, Microsoft announced a new network feature in Preview, Azure Route Server. As the name suggests, this service is designed to act as a routing service within your Azure footprint. Specifically, it should simplify the management of dynamic routing between your network virtual appliance (NVA) and your Virtual Network. It does so by allowing you to exchange routes via BGP between the NVA and the Azure Software Defined Network (SDN) in your Virtual Network. As it’s a fully managed service, this means you no longer need to implement and maintain multiple route tables.

This service has just gone GA, and as a result, I thought I would share some thoughts relative to its implementation and functionality.

First up, a little bit more on how it works. Microsoft have an excellent graphic to show you the functionality at a high-level:

Diagram showing Azure Route Server configured in a virtual network.

However, the key to understanding this service is having familiarity with implementing an NVA in an environment without it. So, say we build a Palo Alto, or any vendor NVA in our VNET. It is a flat VNET which has several subnets. And we want all our traffic to route via the NVA for filtering. Without Route Server, we have to implement single, or multiple, Route Tables on the subnets and manually manage route modifications on the NVA. Meaning a lot of admin overhead and room for potential user error.

Route Server removes this requirement, which believe me, on complex NVA hub-spoke designs is very welcome. The service leverages BGP to expose the system routing table of the VNET to allow the NVA write directly. Similarly, the NVA can learn routes from the VNET as they update, for example, the addition of a peering.

Creation of the service itself is very simple, a dedicated subnet of /27 or larger named RouteServerSubnet and a Public IP. Essentially a similar creation process to Azure Bastion. Guides for deployment are well documented, and include IaC options (although no Bicep example as yet).

Once Route Server is built, you then peer, and complete configuration as required with your NVA(s). Route Server advertises an ASN and IPs from the dedicated subnet for this, as below

I’ve only mentioned NVAs to this point, but Route Server also works with Virtual Network Gateways, supporting both VPNs and ExpressRoute. Integration here is even tighter, you don’t need to configure the peering, you just enable the switch within Route Server. This switch is called Route Exchange, and when enabled, your NVA and ExpressRoute wil learn each others routes.

One thing I have found so far is that this seems more like a service that should be enabled on the VNET itself, like DNS or Service Endpoints, rather than a resource you have to build and configure. Having it “built into” every VNET would seem like the better option to me, and would definitely be cleaner.

Also, there doesn’t appear to be any integration offered with Azure Firewall as yet, however, I want to dig a bit more on that and will update this post accordingly.

Perhaps someday we will get a VNET Version 2.0 which includes the likes of Route Server, Bastion etc all within the one blade and configuration item!

Until then however, if you want to try out Route Server, the absolute best option is to follow the Docs tutorial which covers everything except Virtual Network Gateway, although you could add that on yourself manually if required as I did (ping me if you need help). The full tutorial is here and is a really clean implementation and removal which gives you the ability to test each feature.

One thing that did catch me out here was advertising the default route, however, while I realised my mistake and corrected it with a UDR, I was glad to see this configuration issue, and others are already documented.

Before deploying to production, consider the FAQ to ensure you understand limits and supported scenarios. Peer and route limits are specifically important here for larger designs!

As always, if you have any questions, get in touch!

How To – Enable Web Application Firewall Geomatch Custom Rules

At the end of July, Microsoft announced the general availability of geomatching via Custom Rules in Web Application Firewall. This is a feature I am quite fond of, and is excellent at reducing your attack surface. Thankfully, it’s also quite simple to implement!

First, what is Web Application Firewall (WAF)? Well it’s a service that provides protection for your web apps from common exploits and vulnerabilities. It can be deployed with Azure Application Gateway, Azure Front Door, and Azure Content Delivery Network (CDNN is preview for now). WAF allows for central management, meaning you can react to threats faster, instead of securing each individual web application. There are a couple of specific differences depending on the service you attach a WAF policy to, and you can read about them here.

WAF overview
Nice graphical explainer of WAF from Microsoft

Next, what are Custom Rules? As part of each WAF policy, you have to configure a set of standard rules, such as Prevention or Detection mode, and managed rules (OWASP). However, you can also create your own rules, and in WAF these are simply called Custom Rules (CR). A CR is made up of one or more conditions followed by an action. All CRs for a WAF policy are match rules. You can have multiple CRs per policy and they are processed in order of priority in a range of 1-100, with 1 being highest priority, or processed earliest.

Now that we have the foundation, let’s look at the geomatch option. This is available within your CR as a match Condition.

Create your CR with an appropriate name and priority, then choose ‘Geo location’ from the Match type drop down as above. Next, you’ll want to ensure you choose RemoteAddr as the match variable, and decide what logic you want to apply. By logic I mean the pattern that will fire the rule. In this example, I want all traffic except Ireland blocked. So I will choose the Operation ‘Is not’, then location Ireland, then Deny. If I wanted all traffic allowed and Ireland blocked, I would simply choose the Operation ‘Is’. I recommend figuring out your pattern then working your way through the final section of the CR.

So, based on my example with Ireland, my CR now looks like this:

Now most commonly, I would see a geomatch policy applied to an Application Gateway, but it can be applied to Front Door also. On of the nice features of a WAF policy when using it with Application Gateway is that you get association choices. This means you can be as granular as having different policies per listener, or even per path rule. Giving you huge flexibility when combined with CRs.

Finally, I have obviously shown you how to create this CR in the portal, but there are other methods, like Powershell. These can be very handy if you need to recreate a geomatch CR, especially if you have multiple conditions with many countries (there is a max of 10 per condition). Note, if using these methods, you need to use country codes, like IE, and these are all listed here.

And that’s it! You have your CR created and associated. WAFs take effect pretty quickly so you should be able to test within a couple of minutes. If you want to perform a simple verify, you can switch the action from Deny traffic to Allow and retest.

Finally, a nice tip when you’re happy with your configuration is to setup an alert against your WAF should there be a change made, or worse, deleted (apply resource locks people). This runs as a signal alert from Azure Monitor against an action group as per standard

That’s it for this post, as always, if you have any questions, please get in touch!

How to – Use Azure Firewall IP Groups

If you’re familiar with Azure Firewall you would know that the introduction of an IP Group resource is most welcome. IP Groups are still in preview at the moment, so as usual be cautious on production environments as there is no SLA. However, it’s always nice to try out a service to see if it can work for you, or make your life easier.

IP Groups themselves are a relatively simple resource. They can contain a single IP address, multiple IP addresses, or one or more IP address ranges. They can then be used for DNAT, Network, or Application rules in Azure Firewall.

They currently have some interesting limitations that are a little bit confusing at first. From Docs:

For 50 IP Groups or less, you can have a maximum of 5000 individual IP addresses each per firewall instance. For 51 to 100 IP Groups, you can have 500 individual IP address each per firewall instance.

What this means is that while your rules should already be scoped accurately, you may need to use a couple of extra IP groups if you’re working with large address ranges. A simple example is a /16 will simply not work in an IP Group, /20 is basically your limit per IP Group.

I actually tried this on my own sub and it appears to actually work for now. Expect that to change as preview progresses.

If you’ve worked with Azure Firewall, I’m sure you’ve already thought of several places these rules can really help. For me, it was within Network Rule Collections.

However, as the service is in preview, there are a few aspects to be ironed out. Unfortunately, one of those is the ability to add an IP Group as a destination within a network rule when using the Portal. See below

UPDATE: As expected, this is now resolved! However, read on to see how to do this at scale.

At this point, I am going to flag extreme caution if your Azure Firewall is in production and you are trying this. It is very easy to overwrite all of your collections, take your time and export them before making any changes!

I’m a Windows guy, so I’m going to explain how to do this with Powershell, but it also works for CLI. Similarly, I’m showing a Network Rule, same process works for Application Rules.

First up, you need to all of the details for your Azure Firewall as we will work with it’s config as a variable and finally update it.

#Get the AFW I want to edit
$afw = Get-AzFirewall -Name wda-afw-test -ResourceGroupName rg-wda-afw

#Save current Network Rule Collection to a variable for reference
$oldcol = $afw.NetworkRuleCollections

#Get the IP Group I want to use
$ipg = Get-AzIpGroup -Name wda-group1 -ResourceGroupName rg-wda-afw

#Create my new network rule
$newrule = New-AzFirewallNetworkRule -Name "rule2" -Protocol TCP -SourceAddress * -DestinationIpGroup $ipg.Id -DestinationPort 445

Now this is where it can get a bit tricky. Collections are stored as nested arrays. My AFW has two collections, I want to add my new rule to the second one which means I need to reference index 1. See the collections below, the one we’ll be editing is “collection2” which currently only has “rule2”

#view all collections
$afw.NetworkRuleCollections

#view the specific collection rules using place in array
$afw.NetworkRuleCollections[1].Rules | ft

#add my new rule to my collection
$afw.NetworkRuleCollections[1].AddRule($newrule)

#if you like, check it has updated as desired
$afw.NetworkRuleCollections[1].Rules | ft

#If as expected, update AFW
Set-AzFirewall -AzureFirewall $afw

The last command can take a minute or two to complete. Once it has, you can see the rule is now added to my collection2. The Portal will display it correctly, but you cannot edit correctly with the glitch.

And that’s it! You’ve successfully added an IP Group as a destination to your Azure Firewall. Again, please be careful, the above is only a guide and I cannot be responsible for your Azure Firewall ūüôā

Azure Private Link – Where to Start?

Last week, Microsoft announced that the majority of Private Link is now GA. This means it’s now supported for production workloads, however, is it useful for your production workloads? Hopefully this post will help you understand the service a bit more, and whether it’s worth investing your time in exploring.

First up, what exactly is Azure Private Link (APL)? Well, it allows you to access Azure resources using a private endpoint in a virtual network. There are two possible component within APL.

  • Private Endpoint
  • Private Link Service

A Private Endpoint is a private IP that exists within your vnet and presents a service via APL. Traffic targeting a service via APL travels via the Azure backbone, removing the need for public access if it’s not required. The service could be Azure PaaS, or a service presented by yourself, or a Partner via Private Link Service.

A Private Link Service leverages Azure Standard Load Balancer frontend IP configuration to present as a service, for use by a Private Endpoint. The workload behind ALB could be your own, or a vendors.

Below is a somewhat complex diagram explaining this visually, from Microsoft

Private endpoint overview

So, let’s try break that down into simpler concepts.

First, it allows you to access PaaS services without the need to implement a Microsoft Peering for ExpressRoute. Which gives you greater flexibility as well as a simpler footprint should you have VPN connectivity.

APL maps to single instances of resources, rather than whole services, so you have a more direct and therefore secure connectivity footprint. For example, allow connections to an Azure SQL instance, rather than all Azure SQL instances. This is also relative to vendors providing a service, you can connect to just their presented endpoint rather than a broader or more complex connection.

All of the above can be done across regions and across Azure AD tenants, so if you are providing a service, you can offer it via APL at a global scale.

A lot of Azure PaaS is GA, but there are still quite a few in Preview, so be wary of your production requirements, a full list is here – https://docs.microsoft.com/en-us/azure/private-link/private-link-overview#availability

So what might an APL deployment look like in your environment? Below, I’ve put together a quick example from my own tenant. I have a very simple website, running from a Storage Account and I have a test VM I will RDP to to load the site.

I created an APL, connected it to the SA, which automatically makes a couple of changes and auto-approves the connection within the SA. So now, when I lookup the corresponding endpoint, it shows as a private IP within my vnet (10.55.1.5), and I can browse to it successfully (cert error is expected due to lack of DNS here).

Within APL, I can check the status of my Private Endpoint, which confirms settings as expected.

And on the SA itself, I can see that a Private Endpoint has been activated.

So all of the above seems quite simple and very usable. But there are some current limitations to be aware of.

For Private Endpoint, NSGs are not supported. While subnets containing the private endpoint can have NSG associated with it, the rules will not be effective on traffic processed by the private endpoint. When you create a Private Endpoint via the Portal, it automatically makes a switch to the subnet to disable network policies. Other deployment methods require a manual change, documented here – https://docs.microsoft.com/en-us/azure/private-link/disable-private-endpoint-network-policy

For Private Link Service, it must be a Standard ALB, only IPv4 and only TCP traffic. The above note about subnets and network policies is also valid, documented here – https://docs.microsoft.com/en-us/azure/private-link/disable-private-link-service-network-policy

Overall I think APL is a great addition to the network offerings and more closely aligns what Operations teams like to be able to control when working with PaaS. The options it introduces for vendors could also see some clever solutions brought to market, especially with the supported global capability.

As always, if there are any questions, get in touch!

How to – Troubleshoot Azure Firewall

Networking in Azure is one of my favourite topics. As my work has me focus primarily on Azure Virtual Datacenter builds, networking is key. When Microsoft introduced Azure Firewall (AFW), I was excited to see a platform based option as a hopeful alternative to the traditional NVAs. Feature wise in preview, AFW lacked some key functionality. Once it went GA a lot of the asks from the community were rectified however there are still some outstanding issues like cost, but all of that is for a blog post for another day!

AFW is used in a lot of environments. It’s simple to deploy, resilient and relatively straight forward to configure. However, once active in the environment, I noticed that finding out what is going wrong can be tricky. Hopefully this post helps with that and can save you some valuable time!

I don’t know about you, but the first thing I always check when trying to solve a problem is the most simple solution. For AFW that check is to make sure it’s not stopped. Yes that’s right you can “stop” AFW. It’s quick and easy to do via shell:

# Stop an existing firewall

$azfw = Get-AzFirewall -Name "FW Name" -ResourceGroupName "RG Name"
$azfw.Deallocate()
Set-AzFirewall -AzureFirewall $azfw

But how do you check if it has been stopped? Very simply, via the Azure Portal. On the overview blade for AFW it shows provisioning state. If this is anything but “Succeeded” you most likely have an issue.

So, how do you enable it again should you find your AFW deallocated? Again, quite simply via shell, however, it must be allocated to the original resource group and subscription. Also, while it deallocates almost instantly, it takes roughly the same amount of time to allocate AFW as it does to create one from scratch.

# Start a firewall

$azfw = Get-AzFirewall -Name "FW Name" -ResourceGroupName "RG Name"
$vnet = Get-AzVirtualNetwork -ResourceGroupName "RG Name" -Name "VNet Name"
$publicip = Get-AzPublicIpAddress -Name "Public IP Name" -ResourceGroupName " RG Name"
$azfw.Allocate($vnet,$publicip)
Set-AzFirewall -AzureFirewall $azfw

So, your AFW is active and receiving traffic via whatever method (NAT, Custom Route Tables etc.) and you have created rules to allow traffic as required. Don’t forget all traffic is blocked by default until you create rules.

By default the only detail you can get from AFW are metrics. These can show a small range of traffic with no granular detail, such as rules hit count.


To get detailed logs, like other Azure services, you need to enable them. I recommend doing this as part of your creation process. In terms of what to do, you have to add a diagnostic setting. There are two logs available, and I recommend choosing both.

  • AzureFirewallApplicationRule
  • AzureFirewallNetworkRule

In terms of where to send the logs, I like the integration offered by Azure Monitor Logs and there is a filtered shortcut right within the AFW blade too.

Once enabled, you should start seeing logs flowing into Azure Monitor Logs within five to ten minutes. One aspect that can be viewed as a slight negative is that logs are sent in JSON. As a result most of the interesting data you want is part of an object array:

{
  "category": "AzureFirewallNetworkRule",
  "time": "2018-06-14T23:44:11.0590400Z",
  "resourceId": "/SUBSCRIPTIONS/{subscriptionId}/RESOURCEGROUPS/{resourceGroupName}/PROVIDERS/MICROSOFT.NETWORK/AZUREFIREWALLS/{resourceName}",
  "operationName": "AzureFirewallNetworkRuleLog",
  "properties": {
      "msg": "TCP request from 111.35.136.173:12518 to 13.78.143.217:2323. Action: Deny"
  }
}

So, when running your queries, you need to parse that data. For those who have strong experience in Kusto, this will be no problem. For those who don’t, Microsoft thankfully provide guidance on how to parse both logs including explanatory comments

For ApplicationRule log

AzureDiagnostics
| where Category == "AzureFirewallApplicationRule"
//using :int makes it easier to pars but later we'll convert to string as we're not interested to do mathematical functions on these fields
//this first parse statement is valid for all entries as they all start with this format
| parse msg_s with Protocol " request from " SourceIP ":" SourcePortInt:int " " TempDetails
//case 1: for records that end with: "was denied. Reason: SNI TLS extension was missing."
| parse TempDetails with "was " Action1 ". Reason: " Rule1
//case 2: for records that end with
//"to ocsp.digicert.com:80. Action: Allow. Rule Collection: RC1. Rule: Rule1"
//"to v10.vortex-win.data.microsoft.com:443. Action: Deny. No rule matched. Proceeding with default action"
| parse TempDetails with "to " FQDN ":" TargetPortInt:int ". Action: " Action2 "." *
//case 2a: for records that end with:
//"to ocsp.digicert.com:80. Action: Allow. Rule Collection: RC1. Rule: Rule1"
| parse TempDetails with * ". Rule Collection: " RuleCollection2a ". Rule:" Rule2a
//case 2b: for records that end with:
//for records that end with: "to v10.vortex-win.data.microsoft.com:443. Action: Deny. No rule matched. Proceeding with default action"
| parse TempDetails with * "Deny." RuleCollection2b ". Proceeding with" Rule2b
| extend 
SourcePort = tostring(SourcePortInt)
|extend
TargetPort = tostring(TargetPortInt)
| extend
//make sure we only have Allowed / Deny in the Action Field
Action1 = case(Action1 == "Deny","Deny","Unknown Action")
| extend
    Action = case(Action2 == "",Action1,Action2),
    Rule = case(Rule2a == "",case(Rule1 == "",case(Rule2b == "","N/A", Rule2b),Rule1),Rule2a), 
    RuleCollection = case(RuleCollection2b == "",case(RuleCollection2a == "","No rule matched",RuleCollection2a),RuleCollection2b),
    FQDN = case(FQDN == "", "N/A", FQDN),
    TargetPort = case(TargetPort == "", "N/A", TargetPort)
| project TimeGenerated, msg_s, Protocol, SourceIP, SourcePort, FQDN, TargetPort, Action ,RuleCollection, Rule

For NetworkRule log

AzureDiagnostics
| where Category == "AzureFirewallNetworkRule"
//using :int makes it easier to pars but later we'll convert to string as we're not interested to do mathematical functions on these fields
//case 1: for records that look like this:
//TCP request from 10.0.2.4:51990 to 13.69.65.17:443. Action: Deny//Allow
//UDP request from 10.0.3.4:123 to 51.141.32.51:123. Action: Deny/Allow
//TCP request from 193.238.46.72:50522 to 40.119.154.83:3389 was DNAT'ed to 10.0.2.4:3389
| parse msg_s with Protocol " request from " SourceIP ":" SourcePortInt:int " to " TargetIP ":" TargetPortInt:int *
//case 1a: for regular network rules
//TCP request from 10.0.2.4:51990 to 13.69.65.17:443. Action: Deny//Allow
//UDP request from 10.0.3.4:123 to 51.141.32.51:123. Action: Deny/Allow
| parse msg_s with * ". Action: " Action1a
//case 1b: for NAT rules
//TCP request from 193.238.46.72:50522 to 40.119.154.83:3389 was DNAT'ed to 10.0.2.4:3389
| parse msg_s with * " was " Action1b " to " NatDestination
//case 2: for ICMP records
//ICMP request from 10.0.2.4 to 10.0.3.4. Action: Allow
| parse msg_s with Protocol2 " request from " SourceIP2 " to " TargetIP2 ". Action: " Action2
| extend
SourcePort = tostring(SourcePortInt),
TargetPort = tostring(TargetPortInt)
| extend 
    Action = case(Action1a == "", case(Action1b == "",Action2,Action1b), Action1a),
    Protocol = case(Protocol == "", Protocol2, Protocol),
    SourceIP = case(SourceIP == "", SourceIP2, SourceIP),
    TargetIP = case(TargetIP == "", TargetIP2, TargetIP),
    //ICMP records don't have port information
    SourcePort = case(SourcePort == "", "N/A", SourcePort),
    TargetPort = case(TargetPort == "", "N/A", TargetPort),
    //Regular network rules don't have a DNAT destination
    NatDestination = case(NatDestination == "", "N/A", NatDestination)
| project TimeGenerated, msg_s, Protocol, SourceIP,SourcePort,TargetIP,TargetPort,Action, NatDestination

Using either query gives you clear readable that you can filter. One tip however, is to add a sort command to the end of the queries, normally I use by TimeGenerated to show me the latest data. So to condense and add that for the NetworkRule query above, it would look like:

AzureDiagnostics
| where Category == "AzureFirewallNetworkRule"
| parse msg_s with Protocol " request from " SourceIP ":" SourcePortInt:int " to " TargetIP ":" TargetPortInt:int *
| parse msg_s with * ". Action: " Action1a
| parse msg_s with * " was " Action1b " to " NatDestination
| parse msg_s with Protocol2 " request from " SourceIP2 " to " TargetIP2 ". Action: " Action2
| extend SourcePort = tostring(SourcePortInt),TargetPort = tostring(TargetPortInt)
| extend Action = case(Action1a == "", case(Action1b == "",Action2,Action1b), Action1a),Protocol = case(Protocol == "", Protocol2, Protocol),SourceIP = case(SourceIP == "", SourceIP2, SourceIP),TargetIP = case(TargetIP == "", TargetIP2, TargetIP),SourcePort = case(SourcePort == "", "N/A", SourcePort),TargetPort = case(TargetPort == "", "N/A", TargetPort),NatDestination = case(NatDestination == "", "N/A", NatDestination)
| project TimeGenerated, msg_s, Protocol, SourceIP,SourcePort,TargetIP,TargetPort,Action, NatDestination
| sort by TimeGenerated desc

Using parsed data, you can immediately see all the traffic hitting AFW and, for example, filter on options such as Action to see only denied traffic.

Microsoft also provide pre-cooked visualisation should you prefer it, you can download from here – https://raw.githubusercontent.com/Azure/azure-docs-json-samples/master/azure-firewall/AzureFirewall.omsview – then import into Azure Monitor. The detail is great for quick glance work, I really like the ApplicationRule breakout

Application rule log data

That about sums it up. Hopefully you are now informed and equipped to troubleshoot traffic issues in your Azure Firewall instance. As always, if there are any questions, please get in touch!

If you need more info on how to enable logs – https://docs.microsoft.com/en-us/azure/firewall/tutorial-diagnostics

Log and metrics concepts – https://docs.microsoft.com/en-us/azure/firewall/logs-and-metrics