The DevOps Jedi

Taking the cloud by storm one line of code at a time....

Modifying An Existing Azure Policy

2024-07-1410 min readGovernanceDarren Johnson

There may well come a time when there isn’t a Built In Azure Policy that meets your needs, and you need to either modify an existing policy, or create new a policy from scratch. Creating custom policies should always be a last resort, as the BuiltIn policies Azure provides are kept up to date and version controlled by Microsoft. As soon as you create a custom policy, you are responsible for maintaining it when capability is added or deprecated.

This post will look at how to modify an existing policy to enhance its functionality. I will be working in my own Azure subscription with just 2 Management Groups, but if you are in a large Enterprise you may want to check out the Enterprise Azure Policy as Code (EPAC) solution created by Microsoft.

Modifying An Existing Policy

I’m going to modify a fairly simple policy named Storage accounts should restrict network access to Deny the creation of storage accounts that do not have the firewall enabled. I want to add the capability to exclude resources from this policy via tags if unknown internet endpoints need to access the storage account, for example if I was hosting a static website.

NOTE: The Deny effect applies when attempting to create a new uncompliant resource, therefore preventing it’s creation, but also when attempting to modify an uncompliant resource, therefore preventing its modification. If you have a resource that cannot be made compliant without redeploying the resource don’t get caught out by this. Always remember to test ANY policy throughly at a narrowed scope before deploying wider.

Source Policy Definition ID

The source policy has the Definition ID of /providers/Microsoft.Authorization/policyDefinitions/34c877ad-507e-4c82-993e-3452a6e0ad3c. It is important to note the structure of Definition ID, as this tells us where in Azure the policy is defined. The fact that neither managementGroups or subscriptions are present in it’s path tells me this is a BuiltIn policy created by Microsoft. This is important as this means the policy can be assigned anywhere within the tenant.

Key Takeaway: Ensure you are only defining your policy once within your tenant. If you have followed Microsoft’s Management Group Recommendations and configured a dedicated management group under the Tenant Root Group for all new subscriptions, then configure all policies there so they can be used anywhere within the tenant. If not, then configure them at the Tenant Root Group level as policies can only be assigned underneath where they have been defined.

Duplicating The Policy To Be Modified

You cannot modify BuiltIn policies, so you need to duplicate them first. In the portal I selected the policy and clicked Duplicate Definition. Lets have a look at the JSON it generated:

{
  "mode": "Indexed",
  "policyRule": {
    "if": {
      "allOf": [
        {
          "field": "type",
          "equals": "Microsoft.Storage/storageAccounts"
        },
        {
          "field": "Microsoft.Storage/storageAccounts/networkAcls.defaultAction",
          "notEquals": "Deny"
        }
      ]
    },
    "then": {
      "effect": "[parameters('effect')]"
    }
  },
  "parameters": {
    "effect": {
      "type": "String",
      "metadata": {
        "displayName": "Effect",
        "description": "The effect determines what happens when the policy rule is evaluated to match"
      },
      "allowedValues": [
        "Audit",
        "Deny",
        "Disabled"
      ],
      "defaultValue": "Audit"
    }
  }
}

You will notice that there are 3 top level objects:

  1. mode is part of the policy properties which I will look at later
  2. policyRule is the conditional logic that needs to be matched for the policy to apply and is made up of if and then blocks
  3. parameters are values that can be set when assigning the policy and reduce the number of policies that need to be defined

Lets look at the policy definition in more detail.

The Policy Rule states if allOf the conditions are met, then the effect will be applied. I won’t dive into all the details here as the Microsoft Docs covers this well. For this policy, the resource type needs to be equal to Microsoft.Storage/storageAccounts and the networkAcls.defaultAction must not equal Deny. This means the Firewall on the storage account is Disabled and access is permitted from all networks, including the internet.

NOTE: With this configuration any changes that are made at the Microsoft.Storage/storageAccounts level will be Denied if the Firewall hasn’t been enabled!

The Parameters control the effect of the policyRule which is a string that is supplied to the policyRule section of the definition. However there is extra data here too. The metadata controls the displayName and description of the parameter as seen in the portal. The allowedValues is a list of values that the parameter will accept when the policy is assigned. These values are case sensitive so be sure to be consistent when defining them in your policy. The defaultValue is optional and sets the parameter if no value is specified when the policy is assigned. When using allowedValues this must exactly match.

The Mode is actually part of the properties section of the definition. The properties section isn’t required within the JSON definition, but I like to keep my configurations declarative and include as much information as possible instead of supplying it via the command line. By populating this I can supply the policy Display Name and Description as well as including metadata to provide additional information about the policy.

Modifying The Properties

I’ll start by showing how I modify the properties section:

{
    "properties": {
        "mode": "All",
        "displayName": "Policy Display Name 64 Characters Maximum",
        "description": "Policy Detailed Description 512 Characters Maximum",
        "metadata": {
            "category": "Policy Category",
            "source": "Definition ID or Web URL"
        },
        "policyRule": {...
        },
        "parameters": {...
        }
    }
}

If it is not already set, I change the mode to All as recommended by Microsoft .

Next I include both the displayName and description as these fields shouldn’t need to change when the policy is assigned. The Display Name can actually be up to 128 characters, but I use this to generate the policy assignment name which has a maximum length of 64 characters so I stay within this limit.

I then add some metadata and specify the category. It’s good practice to keep the category the same as the policy this was duplicated from, but if you are creating a completely custom policy from scratch, you may want to specify your own bespoke category so the policy stands out. This is outside the scope of this post. Finally I include the source of the policy so any changes can be compared in the future. If this was from a BuiltIn policy, I set this value to the Definition ID, but if it was a custom policy I include the URL to the repository where the JSON file is stored. You really should be storing all your custom and modified policies in a repository so they can be version controlled.

Modifying The Policy Rule

I mentioned before that I wanted to add a capability to exclude resources when this policy is applied. I could do this when assigning the policy by adding exclusions, but as this is my environment I wanted to experiment with excluding resources by adding tags to them.

        "policyRule": {
            "if": {
                "allOf": [
                    {
                        "field": "type",
                        "equals": "Microsoft.Storage/storageAccounts"
                    },
                    {
                        "field": "Microsoft.Storage/storageAccounts/encryption.requireInfrastructureEncryption",
                        "notEquals": "true"
                    },
                    {
                        "field": "[concat('tags[', parameters('tagName'), ']')]",
                        "notEquals": "[parameters('tagValue')]"
                    }
                ]
            },
            "then": {
                "effect": "[parameters('effect')]"
            }
        }

The capability was fairly simple to implement. I found some sample code in the Microsoft Docs that covered my use case and added this in the allOf conditional checks block. If the specified tag name and value were present the policy wouldn’t apply.

Modifying The Parameters

Now I had the conditional logic configured I just needed to add the new parameters for tagName and tagValue.

        "parameters": {
            "effect": {
                "type": "String",
                "metadata": {
                    "displayName": "Effect",
                    "description": "'Audit' allows a non-compliant resource to be created or updated, but flags it as non-compliant. 'Deny' blocks the non-compliant resource creation or update. 'Disabled' turns off the policy.",
                    "portalReview": true
                },
                "allowedValues": [
                    "Audit",
                    "Deny",
                    "Disabled"
                ],
                "defaultValue": "Audit"
            },
            "tagName": {
                "type": "String",
                "metadata": {
                    "displayName": "Tag Name",
                    "description": "Name of the tag, such as 'environment'"
                }
            },
            "tagValue": {
                "type": "String",
                "metadata": {
                    "displayName": "Tag Value",
                    "description": "Value of the tag, such as 'production'"
                }
            }
        }

Again I lifted this code straight from the sample code and I was good to go. I kept in line with the sample code by not specifying a defaultValue for the tag name and value as this would allow me to reuse this code in other environments if I needed to. By not setting this the user would always be prompted to specify a value when assigning the policy.

However you may notice a couple of extra changes above. Whilst testing both creating and assigning policies I discovered a couple of tweaks I could make to the definition.

I had been looking at the Temp disks and cache for agent node pools in Azure Kubernetes Service clusters should be encrypted at host policy, and spotted they had updated the description to clearly articulate how the effect worked, and also they had added a portalReview attribute. This meant that when the policy was assigned via the portal the person assigning it would always see the Effect displayed on screen. The default behaviour is to Only show parameters that need input or review which I don’t like, so I added this in.

NOTE: I always set the defaultValue of the effect to Audit so that when the policy is assigned its takes no effect unless someone specifies a different value. This means you have to opt in to making changes.

Putting It All Together

I was now ready to deploy the policy so I combined the sections into a single JSON file with my actual values:

{
    "properties": {
        "mode": "All",
        "displayName": "Require Storage Account Firewall Exclude Via Tag",
        "description": "Network access to storage accounts should be restricted. Configure network rules so only applications from allowed networks can access the storage account. To allow connections from specific internet or on-premises clients, access can be granted to traffic from specific Azure virtual networks or to public internet IP address ranges.  To exclude resources from this policy ensure to tag them correctly.",
        "metadata": {
            "category": "Storage",
            "source": "/providers/Microsoft.Authorization/policyDefinitions/34c877ad-507e-4c82-993e-3452a6e0ad3c"
        },
        "policyRule": {
            "if": {
                "allOf": [
                    {
                        "field": "type",
                        "equals": "Microsoft.Storage/storageAccounts"
                    },
                    {
                        "field": "Microsoft.Storage/storageAccounts/encryption.requireInfrastructureEncryption",
                        "notEquals": "true"
                    },
                    {
                        "field": "[concat('tags[', parameters('tagName'), ']')]",
                        "notEquals": "[parameters('tagValue')]"
                    }
                ]
            },
            "then": {
                "effect": "[parameters('effect')]"
            }
        },
        "parameters": {
            "effect": {
                "type": "String",
                "metadata": {
                    "displayName": "Effect",
                    "description": "'Audit' allows a non-compliant resource to be created or updated, but flags it as non-compliant. 'Deny' blocks the non-compliant resource creation or update. 'Disabled' turns off the policy.",
                    "portalReview": true
                },
                "allowedValues": [
                    "Audit",
                    "Deny",
                    "Disabled"
                ],
                "defaultValue": "Audit"
            },
            "tagName": {
                "type": "String",
                "metadata": {
                    "displayName": "Tag Name",
                    "description": "Name of the tag, such as 'environment'"
                }
            },
            "tagValue": {
                "type": "String",
                "metadata": {
                    "displayName": "Tag Value",
                    "description": "Value of the tag, such as 'production'"
                }
            }
        }
    }
}

I set the Display Name to something more descriptive and took the description form the original policy but expanded it to include the exclusions.

Now all that was left to do was save the JSON file and create the new definition. I decided to follow the rough naming convention the team over at Enterprise Scale were using, which is along the lines of Effect-Resource-Description.json so I named the file Deny-StorageAccount-WithoutFirewall-ExcludeByTag.json.

Creating The Custom Policy Definition

The final task was to create the policy definition so it could be assigned. I did this with PowerShell using the New-AzPolicyDefinition Cmdlet which actually creates or updates an existing definition. There are a number of different parameters that can be passed on the command line here, but as I’ve kept all the required information in the definition JSON file I don’t need to specify those.

I decided to extract the Policy Definition Name from the file name for consistency. The BuiltIn policies normally use a GUID for this as the name forms part of the definition ID, which needs to be unique within your organisation. The file name should not be longer than 64 characters (excluding the .json extension) and shouldn’t contain spaces.

I needed to specify the Management Group ID where the policy will be defined. For this example I have left this as Tenant Root Group which actually has a GUID as it’s ID.

NOTE: The PowerShell Cmdlet Parameter is named ManagementGroupName but it actually requires the ID.

The commands I used to define this as are follows:

$policyDefinitionFile = "./Deny-StorageAccount-WithoutFirewall-ExcludeByTag.json"
$policyDefinitionName = (Split-Path $policyDefinitionFile -LeafBase)
$tenantRootManagementGroupName = (Get-AzManagementGroup | Where-Object { $_.DisplayName -eq "Tenant Root Group" }).Name
New-AzPolicyDefinition -Name $policyDefinitionName -Policy $policyDefinitionFile -ManagementGroupName $tenantRootManagementGroupName