Tuesday, November 29, 2022

Azure: Bulk update secrets in Azure key vault using ARM

 At current client we have a requirement to periodically rotate some 50 secrets in Azure key vaults that are used for storing the shared keys for S2S VPN connections.

For keys (not secrets) there is an auto rotation option that is now GA, see link, however it doesn't apply to secrets.

There's an option to use logic apps to rotate secrets but the MS documentation I could find is not very good.

At first I was looking to write a PowerShell script that pulls the new secrets from a csv file and then applies them to a key vault, but it ended up being easier just creating an ARM template that does the same.

The secret names and secret values are stored in the parameters file (in the file there are a couple of test secrets for demonstration purposes). The name of the KV should also be updated in the parameters file. The ARM template itself does not have to be changed.

If you add a secret in the param file that already exists in the KV, it will overwrite the existing secret and add a new/current version and keep the previous version(s) as Older versions, see screenshot below. If you have secrets already in the KV that are not defined in the param file, these secrets will not be changed or deleted. And if you add new secrets in the param file that are not in the KV currently, they will be added.

The template is available on Github, the files are:

deploy_secrets.json

deploy_secrets.parameters.json

Powershell cmd to run ARM template.txt





Monday, July 11, 2022

Azure Firewall with Azure policy and IP groups using Bicep

 In the previous post I deployed a firewall with some additional components such as VMs, NSGs, and route tables to have a working test setup. In this post it will mainly be the FW, the FW policies and the IP groups that will be deployed.

This post is based on the this Quickstart template with only a few modifications. The main reason I do this is to verify that the files work in my environment to inspect deployed resources.

I have uploaded the modified files to Github (fw-with-policy-001.bicep and fw-with-policy-001.parameters.json). 

The resources deployed can be seen below:


The two IP groups are used in combination with the policy. The policy is associated with the firewall. The are two rule collection groups defined with a total of three rules, see below two screenshots:






Sunday, July 10, 2022

Azure: Deploy Azure Firewall in availability zones using Bicep

 Microsoft has some great Quickstart templates for deploying resources as codes. For this post I've looked at deploying an Azure Firewall into multiple availability zones using Bicep.

The Quickstart guide used can be found here.

The template needs only a few changes. I updated the VNet name and IP address info and inserted a reference to a key vault for the VM admin password.

My updated files can be found on Github (fw-conn-weu-001.bicep and fw-conn-weu-001.parameters.json).

The firewall is deployed into three availability zones in West Europe. This increases the availability to 99.99%. There is still only one logical firewall deployed, so the increased availability is managed in the background by Microsoft. And from a Azure Portal point of view it looks the same as if only deploying to one availability zone.

The availability zones can be verified under Azure Firewall -> Properties, see below:


There is no immediate additional cost to deploying to multiple availability zones, however, there is an increased bandwidth cost, see below (or link):




The bicep file deploys the following resources:


If you are testing in a Visual Studio subscription make sure to delete resources again when done as the cost of especially the FW can run up quickly.

One network rule and one application rule are added under Firewall -> Rules (classic) -> Network/Application rule collection tabs, see below. Usually a Firewall Policy will be created and associated with the FW but for testing purposes this can be used.


Using the Bicep visualizer from VS Code you can get a nice graphical overview of the resources to be deployed. To do this, right click the bicep file in VS code and choose "Open Bicep Visualizer", see below: 







Friday, July 8, 2022

ARM template to create VNet with multiple subnets, NSGs, and security rules using copy loop

 At current client we have a scenario where a number of spoke landing zones (LZs) have been created which contain, typically, a VNet and multiple subnets. The landing zones are filtered via an Azure Firewall in the Hub, however, there is an additional requirement to deny inter-VNet traffic between subnets using NSG rules (traffic not leaving the VNet and therefore won't reach the firewall).

The ARM templates for the LZs use copy loops to create the subnets and NSGs. What I wanted to add was a specific security rule (deny) to each of the NSGs that only contained the other subnets (so multiple IP address ranges) in the VNet but not the subnet that is associated with that subnet. I was struggling a bit with the syntax (around adding multiple ranges) and got some assistance in the end from Microsoft to get it to work.

I've collected the whole thing in a template and uploaded to Github (files: VNet-with-3xSubnet-3xNsgs-3xRules-copyloop.json and VNet-with-3xSubnet-3xNsgs-3xRules-copyloop.parameters.json). See this MS article for more info on copy loops.

It creates a VNet with three subnets using a copy loop. It then creates three NSGs, one for each subnet, and two security rules for each NSG. The first rule is static/identical to all NSGs and the second rule varies according to content of the nsgPrefixes array. The second rule relies on an array within an array that adds the IP ranges of the two other subnets to a deny rule. 

I'll paste the content of the ARM template below, but formatting is not great, so suggest getting the files from Github linked above:

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "vnetName": {
            "type": "String"
        },
        "location": {
            "type": "string"
        },
        "addressPrefixes": {
            "type": "string"
        },
        "snetAddressPrefixes": {
            "type": "array"
        },
        "snetNames": {
            "type": "array"
        },
        "nsgNames": {
            "type": "array"
        },
        "nsgPrefixes": {
            "type": "array",
            "metadata": {
                "description": "description"
            }
        }
    },    
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.Network/virtualNetworks",
            "apiVersion": "2020-11-01",
            "name": "[parameters('vnetName')]",
            "location": "[parameters('location')]",
            "properties": {
                "addressSpace": {
                    "addressPrefixes": [
                        "[parameters('addressPrefixes')]"
                    ]
                },
                "copy": [
                    {
                        "name": "subnets",
                        "dependsOn": [
                            "[resourceId('Microsoft.Network/networkSecurityGroups/', parameters('nsgNames')[copyIndex('subnets')])]"
                        ],
                        "count": "[length(parameters('snetNames'))]",
                        "input": {
                        "name": "[concat(parameters('snetNames')[copyIndex('subnets')])]",
                        "properties": {
                            "addressPrefix": "[parameters('snetAddressPrefixes')[copyIndex('subnets')]]",
                            "networkSecurityGroup": {
                                "id": "[resourceId('Microsoft.Network/networkSecurityGroups/', parameters('nsgNames')[copyIndex('subnets')])]"
                                }
                            }
                        }
                    }
                   
                ]
           
            }        
               
        },
        {
            "type": "Microsoft.Network/networkSecurityGroups",
            "apiVersion": "2021-08-01",
            "name": "[concat(parameters('nsgNames')[copyIndex()])]",
            "location": "[parameters('location')]",
            "properties": {
                "securityRules": [
                    {
                        "name": "Port_3389",
                        "properties": {
                            "protocol": "*",
                            "sourcePortRange": "*",
                            "destinationPortRange": "3389",
                            "sourceAddressPrefix": "10.XXX.XXX.XXX",
                            "destinationAddressPrefix": "*",
                            "access": "Allow",
                            "priority": 100,
                            "direction": "Inbound",
                            "sourcePortRanges": [],
                            "destinationPortRanges": [],
                            "sourceAddressPrefixes": [],
                            "destinationAddressPrefixes": []
                        }
                    },
                    {
                        "name": "DenyInternalSubnetInbound",
                        "properties": {
                            "protocol": "*",
                            "sourcePortRange": "*",
                            "destinationPortRange": "*",
                            "destinationAddressPrefix": "*",
                            "access": "Deny",
                            "priority": 4096,
                            "direction": "Inbound",
                            "sourcePortRanges": [],
                            "destinationPortRanges": [],
                            "sourceAddressPrefixes":
                                "[concat(parameters('nsgPrefixes')[copyIndex()])]",
                            "destinationAddressPrefixes": []
                        }
                    }

                ]
            },
            "copy": {
            "name": "NSGcopy",
            "count": "[length(parameters('nsgNames'))]"
            }
        }    
    ]
       
}

-------

Parameters file:

-------

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "vnetName": {
            "value": "vnet-conn-weu-001"
        },
        "location": {
            "value": "westeurope"
        },
        "addressPrefixes": {
            "value": "10.100.0.0/18"
        },
        "snetAddressPrefixes": {
            "value": [
                "10.100.0.0/24",
                "10.100.1.0/24",
                "10.100.2.0/24"
            ]
        },
        "snetNames": {
            "value": [
                "snet-conn-weu-001",
                "snet-conn-weu-002",
                "snet-conn-weu-003"
            ]
        },
        "nsgNames": {
            "value": [
                "nsg-snet-conn-weu-001",
                "nsg-snet-conn-weu-002",
                "nsg-snet-conn-weu-003"
            ]
        },
        "nsgPrefixes": {
            "value": [
                ["10.100.1.0/24", "10.100.2.0/24"],              
                ["10.100.0.0/24", "10.100.2.0/24"],
                ["10.100.0.0/24", "10.100.1.0/24"]
            ]
        }
    }    
}

Wednesday, July 6, 2022

Azure: Load balancer with pre-allocated IPs in backend pool

 When deploying a load balancer there are two different types of backend pool configurations: NIC based and IP based, see second screenshot below. When using IP based pools, you can pre-allocate IP addresses and then when VM's are added at a later stage they can be assigned the allocated IP addresses.

If you add VMs to the backend pool manually via the portal post deployment, the default choice is NIC based. If you want to deploy the NIC based setup with the ARM templates it's a bit more tricky. Then you have to deploy a couple of vNICs and then associate them with the pool. And then when the backend VMs are deployed, you can associate them with the already deployed NICs.

Note that there are some limitations around using IP based pools and private link services. According to this article "A load balancer with IP based Backend Pool can’t function as a Private Link service".

The content in this post uses this Microsoft article as a starting point.

The ARM template can be found on Github (lb-netw-dv-weu-001.json and lb-netw-dv-weu-001.parameters.json).

The LB is internal, has a frontend IP, one backend pool with two local IPs pre-allocated, one http probe and a load balancing rule that forwards requests on port 80, see more details in screenshots below.







Sunday, June 26, 2022

Bicep - Add an Azure Key Vault with Access Policies enabled

 As opposed to using ARM templates, you can use Bicep which is also a declarative language but one that is more simple to use than ARM templates.

For this example, I've created a key vault in Bicep, one that I'm using to store local administrator passwords for Windows Servers.

The parameters files are the same as for the ARM template, however, the main template is simpler in Bicep. 

Files are uploaded to Github (kv-mgmt-weu-002.bicep and kv-mgmt-weu-002.parameters.json).

Note that this key vault uses Access Policies as opposed to RBAC for authentication.

I have a regular administrator user which has full access and then there is a service principal (or App Registration) in Azure AD which is configured in Azure DevOps (this is specified in the parameters file). This service principal has permissions to get secrets from the key vault.

In the parameters file the tenant ID (the Azure AD tenant ID) must be specified as well as the object ID of the service principal or any regular users. This can be located in Azure AD, see below:



Bicep file is shown below:


param name string
param location string
param sku string
param accessPolicies array
param tenant string
param enabledForDeployment bool
param enabledForTemplateDeployment bool
param enabledForDiskEncryption bool
param enableRbacAuthorization bool
param publicNetworkAccess string
param enableSoftDelete bool
param softDeleteRetentionInDays int
param networkAcls object

resource name_resource 'Microsoft.KeyVault/vaults@2021-10-01' = {
  name: name
  location: location
  properties: {
    enabledForDeployment: enabledForDeployment
    enabledForTemplateDeployment: enabledForTemplateDeployment
    enabledForDiskEncryption: enabledForDiskEncryption
    enableRbacAuthorization: enableRbacAuthorization
    accessPolicies: accessPolicies
    tenantId: tenant
    sku: {
      name: sku
      family: 'A'
    }
    publicNetworkAccess: publicNetworkAccess
    enableSoftDelete: enableSoftDelete
    softDeleteRetentionInDays: softDeleteRetentionInDays
    networkAcls: networkAcls
  }
  tags: {
  }
  dependsOn: []
}


ARM template - simple Ubuntu VM for troubleshooting

 This ARM template for this VM deploys a small Ubuntu Server v20.04. It assumes that all networking is in place (VNet, subnet, NSG) and is similar in specs as the Win2k19 server described here.

No public IP configured.

ARM template can be found on Github (vm-srv-002-ubuntu20.json)

Login method is using SSH keys (not username and password), so a key pair has to be created in advance. There are multiple ways of doing this, see here for more info. And neat way to create a key pair is via the Azure Portal. Go to Portal -> All Services -> SSH Keys -> Create. This will create a new key pair, store the public key in Azure and let you download the private key. The public key will have to be added to the parameters file for the 'adminPublicKey' parameter.



Note that if you are using Putty to connect to the VM, you first have to convert the .pem file to the .ppk format which Putty uses. This can be done by using Puttygen (simply import your pem key and save the private key as a ppk file, see more info here). 

When opening Putty, specify the private key under Connection -> SSH -> Auth, see below:


 The OS version is the current latest one, v20.04, see below:




Saturday, June 25, 2022

ARM template - simple Win2k19 VM for troubleshooting

 From time to time you need a virtual machine for troubleshooting purposes. At current client these VMs have to be removed when not in use, so I usually go via the portal to deploy a new one when needed, which is not too bad.

However, to speed things up a little bit, I created the same VM as I usually deploy as an ARM template. I have uploaded the files to Github (vm-srv-001-win2k19.json). 

It's a Windows Server 2019. Sku is Standard_B2s with 2 vCPUs and 4 GB memory.

It requires an existing VNet and subnet (which is specified in the parameters file).

For the local administrator password, this references a key vault in Azure (this can be replaced by adding the password directly in the ARM template if needed).

User is: localadmin

Auto shutdown is enabled every day at 19.00 hrs CET, no email notification.

No public IP, so to access this VM you need access to the VNet and subnet from somewhere.

RDP port 3389 is enabled/opened in the local firewall.

No NSG as expecting to have an NSG associated with the subnet.

Remember to update path to correct subscription and key vault.






Deploy an AKS cluster using an ARM template

 If you want to deploy an AKS (Azure Kubernetes Services) cluster, there is a useful QuickStart guide for this purpose, see link here.

If you click on the "Deploy to Azure" button, you can set the required parameters and download the ARM template from there.

I have tested this in my own environment and have uploaded the corresponding template to Github, see link to files here (aks-pr-weu-001.json). Had to make some minor adjustments such as choosing a VM SKU that is available in the West Europe region and adjusting disk size to not be null (set it to 0 to get default disk size for the sku.

The QuickStart template let's you generate a new SSH key pair, but I created my own in advance via the cloud shell, https://shell.azure.com. So if reusing the template from Github remember to add you own public key file in the parameters file.

Validating the deployment and deploying the application I did via the cloud shell as well, see below.

The deployment consists of an AKS cluster with one agent pool with three agents/VMs running in the pool, see screenshot below.

If you follow the guide and create the .yaml file, it will publish the voting app which is reachable via a public IP address.






Cloning an Enterprise Scale git repo fails with error file name too long

 When working with Microsoft Enterprise Scale for Azure, it creates a rather long folder structure in the git repo. So usually when setting up a new developer machine for the first time and try to clone the repo, Git throws and error stating that file names are too long.

To fix this, open a PowerShell window as administrator (this can be from within VS code as well) and run the following command:

> git config --system core.longpaths true

And then you can rerun the git clone command.

(If Git asks you to configure user.name and user.email on first commit, add the following from a window not opened as administrator, see more here):

> git config --global user.name "John Doe"

> git config --global user.email "johndoe@email.com")

Friday, June 24, 2022

Azure: Creating Private DNS Zones using copy loops with arrays and objects

 At current client, we potentially have to create +50 Private DNS Zones, so I was looking at optimizing the ARM templates as to not have hundreds of lines of code with basically the same info.

Furthermore the DNS zones have to be linked to two VNets (where the custom DNS servers reside).

This can be achieved using copy loop with a mix of arrays and objects.

The ARM template including parameters file is available on Github.

The list of private DNS zones (see full list here) is specified in an array and to loop through this list, a copy loop is used on the privateDnsZones resource, I have marked in red below the relevant lines.

For the virtual network links, a similar copy loop is used. In addition, an object is used as a parameter to make a bit nicer. The object contains two sets of info with link name and VNet resource ID for the two VNets, see more info on using objects here.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "privateZoneNames": {
            "type": "array"
        },
        "vnetSettings": {
            "type": "object",
            "metadata": {
                "description": "Object containing vlink name and vnet ID"
            }
        }
    },
    "resources": [
        {
            "apiVersion": "2018-09-01",
            "type": "Microsoft.Network/privateDnsZones",
            "name": "[concat(parameters('privateZoneNames')[copyIndex()])]",
            "location": "global",
            "dependsOn": [],
            "tags": {},
            "properties": {},
            "copy": {
            "name": "PrivateZoneCopy",
            "count": "[length(parameters('privateZoneNames'))]"  
            }
        },
        {
            "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
            "apiVersion": "2018-09-01",
            "name": "[concat(parameters('privateZoneNames')[copyIndex()], '/', parameters('vnetSettings').vlink[0].name)]",
            "location": "global",
            "dependsOn": [
                "[resourceId('Microsoft.Network/privateDnsZones', concat(parameters('privateZoneNames')[copyIndex()]))]"
            ],
            "properties": {
                "registrationEnabled": false,
                "virtualNetwork": {
                    "id": "[parameters('vnetSettings').vlink[0].vnetId]"
                }
            },
            "copy": {
            "name": "vlinkWeuCopy",
            "count": "[length(parameters('privateZoneNames'))]"  
            }
        },
        {
            "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
            "apiVersion": "2018-09-01",
            "name": "[concat(parameters('privateZoneNames')[copyIndex()], '/', parameters('vnetSettings').vlink[1].name)]",
            "location": "global",
            "dependsOn": [
                "[resourceId('Microsoft.Network/privateDnsZones', concat(parameters('privateZoneNames')[copyIndex()]))]"
            ],
            "properties": {
                "registrationEnabled": false,
                "virtualNetwork": {
                    "id": "[parameters('vnetSettings').vlink[1].vnetId]"
                }
            },
            "copy": {
            "name": "vlinkSwcCopy",
            "count": "[length(parameters('privateZoneNames'))]"  
            }
        }
    ]
}

--------------------------------
Param file:
--------------------------------

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "privateZoneNames": {
            "value": [
                "privatelink.blob.core.windows.net",
                "privatelink.vaultcore.azure.net"
            ]
        },
        "vnetSettings": {
            "value": {
                "vlink": [
                    {
                        "name": "vlink-vnet-conn-weu-001",
                        "vnetId": "/subscriptions/XXX/resourceGroups/rg-netw-conn-001/providers/Microsoft.Network/virtualNetworks/vnet-conn-weu-001"
                    },
                    {
                        "name": "vlink-vnet-conn-weu-002",
                        "vnetId": "/subscriptions/XXX/resourceGroups/rg-netw-conn-001/providers/Microsoft.Network/virtualNetworks/vnet-conn-weu-002"
                    }
                ]
            }
        }
    }
}

Thursday, June 23, 2022

Azure: Adding routes to route tables using Service Tags fail with CIDR error

At current client we were deploying some API Management service resources yesterday. For this we needed a direct route to the internet for management and monitoring services for the setup to function properly. This can be achieved by using Service Tags. The two service tags used are: ApiManagement and AzureMonitor.

It worked fine to add the routes to the route table using the Azure Portal. However, when trying to push the same via ARM templates I got an error from Azure DevOps stating the following (also see screenshot below):

"AzureMonitor of resource /subscriptions/XXX/resourceGroups/XXX/providers/Microsoft.Network/routeTables/udr-XXX-001/routes/AzureMonitor is not formatted correctly. It should follow CIDR notation, for example 10.0.0.0/24."

It turns out that the API version of the route table resource in the ARM template was using a too old apiVersion. When updating the apiVersion to the latest one, it works (not for template itself but specifically for the resource, see below (initially a 2017-08-01 version was used):





Wednesday, May 25, 2022

Azure VNet with nested VNet peering - ARM template

When creating a VNet peering between two VNets, this has to be configured separately on both VNets as it's two separate peerings. If instead you want to configure both VNet peerings in the same ARM template, this can be done using nested templates.

The resource type to use for peering is: "Microsoft.Network/virtualNetworks/virtualNetworkPeerings"

It's a standalone resource but is typically added together with VNet json.

The ARM template first adds the VNet peering on the current or the spoke VNet and the nested template creates an additional peering which is configured on the remote or hub VNet pointing back to the current/spoke VNet.

I've uploaded the full files to Github. This file (VNet-with-nested-peering.json and its parameters file) will deploy a VNet, a subnet, an NSG and the two VNet peerings. You just have to update the parameters file.

Below is shown the code specific to the VNet peerings (it doesn't format well below, but you can copy and paste it into a separate file if needed or preferably use the Github link above):

-----------------

{
            "name": "[concat(parameters('VNet_name'), variables('spokePeeringName'))]",
            "type": "Microsoft.Network/virtualNetworks/virtualNetworkPeerings",
            "apiVersion": "2019-11-01",
            "dependsOn": [
                "[parameters('VNet_name')]"
            ],
            "properties": {
                "allowVirtualNetworkAccess": true,
                "allowForwardedTraffic": true,
                "allowGatewayTransit": false,
                "useRemoteGateways": false,
                "remoteVirtualNetwork": {
                "id": "[resourceid(parameters('HubSubscriptionId'), parameters('hubVnetRG'), 'Microsoft.Network/virtualNetworks', parameters('hubVnetName'))]"
                }
            }
        },
        {
            "apiVersion": "2017-05-10",
            "name": "nestedTemplate",
            "type": "Microsoft.Resources/deployments",
            "resourceGroup": "[parameters('hubVnetRG')]",
            "subscriptionId": "[parameters('HubSubscriptionId')]",
            "dependsOn": [
                "[resourceId(variables('currentSub'), variables('currentRg'), 'Microsoft.Network/virtualNetworks', parameters('VNet_name')) ]",
                "[concat(resourceId(variables('currentSub'), variables('currentRG'), 'Microsoft.Network/virtualNetworks', parameters('VNet_name')), '/virtualNetworkPeerings',variables('spokePeeringName'))]"
            ],
            "properties": {
                "mode": "Incremental",
                "template": {
                "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json",
                "contentVersion": "1.0.0.0",
                "resources": [
                    {
                    "name": "[concat(parameters('hubVnetName'), variables('hubPeeringName'))]",
                    "type": "Microsoft.Network/virtualNetworks/virtualNetworkPeerings",
                    "apiVersion": "2019-11-01",
                    "properties": {
                        "allowVirtualNetworkAccess": true,
                        "allowForwardedTraffic": true,
                        "allowGatewayTransit": false,
                        "useRemoteGateways": false,
                        "remoteVirtualNetwork": {
                        "id": "[resourceId(variables('currentSub'), variables('currentRg'), 'Microsoft.Network/virtualNetworks', parameters('VNet_name')) ]"
                        }
                    }
                    }
                ]
                }
            }
        }