Contents

Update Azure NSG security rules using Bicep

Update Azure Network Security Groups (NSG) using Bicep


In this post I will be explaining how you can utilize Bicep (an Infrastructure as Code (IaC) language) for creating and/or updating the security rules on a Network Security Group (NSG) in Microsoft Azure. I have also shared a script which can be used to export a deployment template based on the security rules configured on an existing NSG resource so that you can use that for further deployments.

Update Azure Network Security Group (NSG) security rules using Bicep

Together with a colleague, I had to create and update a large amount of security rules on Azure Network Security Groups (NSGs) across multiple environments. Normally we would use an ARM deployment template to do NSG deployments. However, the logic used in the script could not achieve what was required and because it is an ARM template, quite complex as well. Instead of rewriting the template we decided to create a new template written in Bicep instead.

In this post I will go over the template as well as some other things I have learned/played with during that process.

/2022/05/azure-update-nsg-rules-bicep/AzureNetworkSecurityGroup.webp

Network Security Group deployment template

The template format for a NSG is quite straight forward. There aren’t that many properties and child resources to configure. You can find the full template format in the Microsoft template documentation. But as you properly already have noticed most of the properties have a singular and plural form (e.g. destinationAddressPrefix and destinationAddressPrefixes). My first thought was that it would be handy to only specify one of those properties in the parameters and based on the amount of values/prefixes specified in the parameter decide whether it should be the singular or plural form. But I soon realized that it was not an ideal solution. Instead Bicep has the contains object function that can be used. It can check whether an object contains a key and with that information we can decide whether we do something with that information or not. In our case if the key is present we want to assign the value and if is not pass an empty value (singular empty string, or pural an empty array).

InvalidTemplateDeployment

If you make the above mistake you will run into an InvalidTemplateDeployment error:

InvalidTemplateDeployment - The template deployment ‘$DEPLOYMENT_NAME’ is not valid according to the validation procedure. The tracking id is ‘$TRACKING_ID’. See inner errors for details. SecurityRuleParameterContainsUnsupportedValue - Security rule parameter DestinationAddressPrefix for rule with Id /subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCEGROUP_NAME/providers/Microsoft.Network/networkSecurityGroups/$NSG_NAME/securityRules/allow-http cannot specify existing VIRTUALNETWORK, INTERNET, AZURELOADBALANCER, ‘*’ or system tags. Unsupported value used: *.

The deployment failed because the asterisk (*) wildcard character was given as a value for the destionationAddressPrefixes key. The deployment would have succeeded if instead the value was given for the destionAddressPrefix key. This is also the case for any of the service tags such as VIRTUALNETWORK, INTERNET, AZURELOADBALANCER, etc.

In the JSON snippet below is an example of a security rule that would throw the InvalidTemplateDeployment error.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
        {
          "name": "allow-http",
          "properties": {
            "access": "Allow",
            "destinationAddressPrefix": "",
            "destinationAddressPrefixes": [
              "*"
            ],
            "destinationPortRange": "80",
            "destinationPortRanges": [],
            "direction": "Inbound",
            "protocol": "Tcp",
            "priority": "150",
            "sourceAddressPrefix": "*",
            "sourceAddressPrefixes": [],
            "sourcePortRange": "*",
            "sourcePortRanges": []
          }
        }

In this case the asterisk (*) wildcard character should have been a value of the destinationAddressPrefix key (e.g “destinationAddressPrefix”: “*”).

With what we know now the following template can be used:

Great! Before we dive into the security rules lets discuss another approach we can take to the priority.

How to define priority

Instead of specifying the priority for each rule that you create you can also do some more tweaking to the template. For example, instead of giving priority like:

 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var securityRuleArray = [for (securityRule, i) in securityRules: {
  name: securityRule.name
  properties: {
    access: securityRule.properties.access
    destinationAddressPrefix: contains(securityRule.properties, 'destinationAddressPrefix') ? securityRule.properties.destinationAddressPrefix : ''
    destinationAddressPrefixes: contains(securityRule.properties, 'destinationAddressPrefixes') ? securityRule.properties.destinationAddressPrefixes : []
    destinationPortRange: contains(securityRule.properties, 'destinationPortRange') ? securityRule.properties.destinationPortRange : ''
    destinationPortRanges: contains(securityRule.properties, 'destinationPortRanges') ? securityRule.properties.destinationPortRanges : []
    direction: securityRule.properties.direction
  --priority: securityRule.properties.priority //remove
  ++priority: 100 + i //add
    protocol: securityRule.properties.protocol
    sourceAddressPrefix: contains(securityRule.properties, 'sourceAddressPrefix') ? securityRule.properties.sourceAddressPrefix : ''
    sourceAddressPrefixes: contains(securityRule.properties, 'sourceAddressPrefixes') ? securityRule.properties.sourceAddressPrefixes : []
    sourcePortRange: contains(securityRule.properties, 'sourcePortRange') ? securityRule.properties.sourcePortRange : ''
    sourcePortRanges: contains(securityRule.properties, 'sourcePortRanges') ? securityRule.properties.sourcePortRanges : []
  }
}]

In this example you don’t have to give the priority for each rule that you provide in the deployment parameters. Instead based on the order of your rules the rules will get a priority starting from 100 and each rule afterwards will go up by one.

An approach on custom rule(s)

Great! We have our deployment template. But how do we provide our security rules to the securityRules parameter? I was going through the options and the two cleanest way of specifying the parameters were either:

  1. By having the security rules in a separate json file and using the loadTextContent file function to load the content of the specified file as a string. You can then use the json function to create a JSON objects from this. You can even concatenate the rules of multiple json files using this method. The properties loaded by these functions even have intelliSense in Visual Studio Code as well, cool! An example of the JSON function together with the loadTextContent can be found here. Yup, good example because it’s using a NSG as well.
  2. By just using the good old ARM deployment parameters template.

I chose to go with the second method but might try the first option again later. Concatinating a standard set of rules and your more customized rules does have its appeal.

loadJsonContent

Bicep v0.7.4 just got released and added the loadJsonContent function. As per the release notes:

  • Will more efficiently generate final ARM Template, resulting in smaller files
  • Allows for JSONPath queries to load only part of the requested JSON file

Sounds like this function is worth exploring

Working with the Network Security Rules parameter template

As mentioned above we are going with the deployment parameters option. Without any rules specified the template looks like this:

Yup, very empty! You could also add the nsgName or tags parameters to the template file.

Because I already had an existing NSG which has a lot of rules in place instead of manually typing them all out, I wanted to find a way to easily fetch those. Lets try that.

The Azure Network Security Group Rest Api

My colleague pointed me in the direction of the Network Security Group Rest API:

1
az rest --method get --uri "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$resourceGroup/providers/Microsoft.Network/networkSecurityGroups/$networkSecurityGroup?api-version=2021-08-01"

This returns all the information we need and more.. as you can see from the sample response. In case of a single destination port the API returns the singular key and its value (e.g. “destinationPortRange”: “80”) and in case of plural it returns the plural key and its values in an array. The only problem we have now is that there are other properties coming in as well. Like the id, etag, type and provisioningState. We don’t need those but luckily I had played with jq in the past and just found a new use case to play with the tool. So lets dig in.

Filtering JSON data with jq

From the jq site:

jq is like sed for JSON data - you can use it to slice and filter and map and transform structured data with the same ease that sed, awk, grep and friends let you play with text.

Perfect! Just what we need for both our use cases.

There is also documentation available on the site for all the commands (or try searching on StackOverflow if you are stuck 😉)

First the unneccesary properties need to be removed. We can use the builtin function del for this:

1
$input | jq 'del(.properties.securityRules[] | .id, .etag, .type, .properties.provisioningState) | .properties.securityRules'

Where $input is what is returned to us when we called the rest api method earlier. We pipe the output in jq, remove the specified keys and their values, and only return the custom security rules.

Now that we have the cleaned up security rules we want to store those in our template file. We can achieve this as well through jq:

1
jq --argjson input "$securityRules" '.parameters.securityRules.value += $input' $inputTemplate > $outputTemplate

We add the security rules we got returned earlier to the security rule values in the deploymentParameters template and output this as a new deploymentParameter file. And as you probably already have guessed by now.. The output file is what we are going to end up using as our deployment paramater file :-)

As you can see jq is a very powerful commandline tool and on top of that it is a lot of fun figuring out how to use it!

Azure Pipelines Agents

jq is also part of the installed software on the Microsoft hosted Azure Pipelines agents images:

Here’s a playground for jq if you want to take it for a quick spin.

yq
yq is also a handy tool to have. It’s similar to jq and works for YAML files as well. Fun to use when you work a lot with YAML files which is definitely the case if you work with pipelines, kubernetes, etc.

In the above examples we have learned how we can get the custom security rules by calling the Network Security Group Rest API using a GET request. We used the jq commandline tool to extract the security rules and deleted the keys that we didn’t need and we have used the jq tool to combine the security rules in a deploymentParameter template. We can put this all together into a PowerShell and/or Bash script.

Putting it all together in a bash script

By putting everything that we have just learned together we come up with the following bash script. I added the getopts command for easy parsing of commandline arguments:

If we run the above script for a given resource group and network security group:

1
./getSecurityRules -n Nsg1 -r ResourceGroup1

It will return an deployment parameter file with the name Nsg1.parameters.json in the same directory as where the executed script is located. We can use the parameter file for our future deployments. Lets take a look at how we can deploy new security rules using template.

(What-If) deployment

For now lets do an What-if deployment without editing the file. The What-if deployment will only predict what will happen if we deploy our template file. It won’t make any changes to the existing NSG or its rules. So in our case this should only show ignored resources and not show any changes (apart from some garbage perhaps).

1
az deployment group what-if --resource-group rg1 --name updateNsg --template-file ./nsg.bicep --parameters @nsg1.template.json --parameters nsgName=nsg1

And of course if we want to update our rules we just modify the rules in the our template.json file. When we want to do an actual deployment we can just run:

1
az deployment group create --resource-group rg1 --name updateNsg --template-file ./nsg.bicep --parameters @nsg1.template.json --parameters nsgName=nsg1

If the validation against Azure Resource Manager succeeds our deployment should kickstart and succesfully get deployed.

Deployment pipeline

The What-If deployment followed by the actual deployment can get combined into an Azure Pipelines deployment pipeline. For example, when you update the parameter template with a new rule the pipeline will get triggered. First it will run a validation stage which runs the what-if deployment against your environment and returns results of what security rules get created, modified and/or removed. Then you can have an additional stage which will do the actual deployment after a review approval is done. I really like this approach since it gives you more confidence in your deployments.

I am planning on writing a post about this as well since there is a lot to write about this topic.

Conclusion

In this post I showed how you can deploy security rules to your Network Security Group(s) in Azure using Bicep and how to get an deployment parameter template for your existing Network Security Groups.

Hopefully you found this useful. Have a nice day!