Azure Terraform Security

Azure Firewall with a Twist of AzApi

Azure Firewall with a Twist of AzApi

TL:DR; Use Terraform AzApi provider to deploy an Azure Firewall Policy Rule Collection Group containing Mssql type application rule. Full AzApi example here. Attempted module for re-use here.

Some background

Lately I have been working much more with Terraform, and it is really great to be back doing  infrastructure as code again! It also does not come without its issues.. This issue happens while working on an Azure Firewall deployed with Terraform and Firewall Policies. We need to allow certain Mssql traffic from somewhere to private endpoints somewhere else going through the Azure Firewall. Microsoft recommend using application rules as opposed to network rules for this scenario.

Azure Firewall Policies

There are two ways of managing an Azure Firewall, Classic Rules and Policies. According to the Well Architected Framework docs for Azure Firewall the best practice is to use Policies. This enables you to create a base firewall policy which will ensure a common set of rules.

Specific teams can then create their own child policies with their own rules. The base policy has a higher priority and runs before the child policy. Pricing is a little different, but no Azure Firewall Manager policy charges will be done for policies that are associated to a single firewall.

Migrating from classic to policies can be tricky, so choose carefully.

Rule Collection Groups

The first level of grouping is called Rule Collection Groups. This is a logical grouping of different Rule Collections, and is processed by their priority values. Rule collection groups contain one or multiple rule collections. You can read more about the actual values and priority processing in Microsoft documentation on rule processing.

Rule Collections

The next level of grouping is called Rule Collections. Rule Collection Groups contain Rule Collections, which in turn contains one or more Rules. The Rule Collection is also processed based on priority values, and can be one of three different rule collection types: 

  • DNAT
  • Network
  • Application

Rules

The last unit is the Rules. This unit specifies which traffic is explicitly allowed or denied. All traffic will be denied by default, and you have to allow the specific traffic you want to let through. There are three types of rules, and these are the three types mentioned above. Each rule type has its own use cases. This is an over-simplified explanation of them.

DNAT (Destination Network Address Translation) rules are for allowing point traffic from outside to you network. If you want to “map” an external port on the firewall public ip, you can point it to a VM inside your network with DNAT. Port 33890 on Azure Firewall public IP can point to port 3389 somewhere inside your network.

Network rules are used for network traffic filtering. Configure source/destination ports and source/destination addresses/fqdns/tags. Good for allowing e.g. windows update communication from inside your network to Microsoft update services via a service tag.

Application rules are used for e.g. outbound traffic to internet urls. You can decide if the traffic is allowed based on the url it is accessing. For example you can allow traffic from all your servers to www.google.com specifically, and this will not allow traffic to any other websites. Only Http, Https, and Mssql protocols supported here.

Aidan Finn wrote this article a few years ago, and it explains the rule types. Some of the information might be outdated.

The challenge

When you are using Firewall Policies, you need to configure Rule Collections that contain Rules. The Rule Collections are associated to Rule Collection Groups. The Rule Collection Groups are associated to an Azure Firewall Policy. The Azure Firewall Policy is associated to an Azure Firewall. This Terraform resource in the AzureRM provider creates and manages the Policy Rule Collection Groups.

The current challenge is with an Application type rule collection. If you look at the documentation for azurerm_firewall_application_rule_collection, it states that three protocol types are permitted for a rules protocol: Http, Https, and Mssql.

But if you look at the rules protocol block inside the azurerm_firewall_policy_rule_collection_group, you will see that only Http and Https is permitted.


Confusing?!

If you try to create a Rule Collection inside a Rule Collection Group with Mssql type protocol, you will get this error message on validation step:


Expected rule collection to be one of [Http Https], got Mssql.

This is not really intuitive because Azure Firewall Application rule supports Mssql as protocol type. There is an issue on GitHub tracking this, but I haven’t seen any feedback there the last weeks. It is still open as of writing this post, and I am not quite sure why this is taking long to implement. Could be there is a limitation in the SDK, or something like that. Might be a challenge because these are supposed to enable all three rule types, and there could be input validation troubles.

The solution

This is where AzApi provider comes to the rescue! If you haven’t tested the AzApi provider yet, or don’t know what it is, don’t fret. Simply put it is is a thin layer on top of the Azure ARM REST APIs. It is quite new, and very handy when working with unsupported configurations or preview functionality. Unsupported configurations is exactly the predicament we find ourselves in here!

I have not been able to spend hours and hours on this, but what I have done so far is create a policy rule collection group with AzApi provider. This allows me to create a policy rule collection group with the necessary Mssql type protocol, and still define my infrastructure with Terraform. Previously this would require using ARM template or worse clickOps.

Prereqs

You need a few things before this can be tested or used in your environment:

  • An Azure environment for testing.
  • An Azure Firewall already configured with Firewall Policy.
  • Some Terraform and Azure knowhow.
  • A service principal for authentication of AzApi (it does not yet support OIDC)
    • The rest of your Terraform may run OIDC, but the AzApi provider needs a client_id and a client_secret.
    • Fortunately you can just add the ARM_CLIENT_ID and ARM_CLIENT_SECRET to environment variables, and it will pick them up automatically.

Code

I won’t paste all of the actual Terraform code here in this post. You can find a reusable module here and just a configuration example here. Both of those will get you a policy rule collection group, and the latter will give you a clear structure for how this is performed with AzApi.

It looks something like this:

resource “azapi_resource” “rule_collection_group” { type = “Microsoft.Network/firewallPolicies/ruleCollectionGroups@${var.api_version}” name = “Rule-Collection-Name” parent_id = var.firewall_policy_id

body = jsonencode({ name = var.firewall_rule_collection_group_name properties = { priority = 1000 ruleCollections = [ { ruleCollectionType = “FirewallPolicyFilterRuleCollection” name = “ruleCollectionProd” priority = 1000 action = { type = “Allow” } rules = [ { ruleType = “ApplicationRule” name = “allow-inbound-to-prod-databases-from-somewhere” protocols = [ { protocolType = “Mssql” port = 1433 } ] terminateTLS = false sourceAddresses = [ “10.1.0.4/32”, “10.1.0.5/32” ] targetFqdns = [ “proddb01.database.windows.net”, “proddb02.database.windows.net” ] } ] } ] } }) }

You should copy the module and make it your own. It needs some TLC to be really reusable and at the moment it is a bare bones example that works mostly for my specific use case. Given more time I would generalize it a bit more, but I don’t think it’s worth it at the moment.

Simplification and generalization of modules for use everywhere is not trivial. I tried several approaches, but none seemed to give me the flexibility or possibilities that I wanted. In any case you needed to supply the rules or rule collections in a variable. Suggestions are welcome, but I think the issue will be fixed eventually, and we can go back to vanilla Terraform for this.

In summary

This is one of those times I very much like the effort Microsoft is putting into supporting the toolset around Terraform. If it wasn’t for AzApi in this case, we would have to rely on an ARM deployment from Terraform, and that is not ideal.

As of AzureRM provider version 3.10.0 this is still an issue.

Please leave a comment if this was useful, or if you have improved the module / solved it differently!