Automation, Azure, DevOps, Professional, Terraform

Creating and Deploying Azure Policy via Terraform

Azure Policy is a way to proactively prevent Azure resources from being created that violate your organizations policies/standards/best practices. A policy can enforce a plethora of things like the setting of certain functionality, the requirement of certain tag values, ensure a resource SKU is on an allowed list, and deny a resource SKU this is on a denial list. All of this while also allowing for exemptions. All in all, Azure Policy can be a powerful tool.

For this example, we are going to piggy back off one of Azure Security Center best practices recommendations of “Managed identity should be used in your web app”. If you are unfamiliar with Managed Identities, they are essentially an Azure Active Directory Object that is either auto assigned or user assigned to an application/resource. The reason this is a best practice is it paves the way to limit the exchange of passwords between resources.

An example within Azure would be allowing an App Service to directly talk to a Storage account via RBAC role assignment as opposed to requiring an access key. Even further we wouldn’t need to potentially store that access in a resource like Azure Key Vault. This not only limits where the key might be placed (thus limiting potential accidental compromise) but also provides traceability into who is accessing what Azure resources when.

Walkthrough

The first step is to define the Azure Policy via the azurerm_policy_definition in Terraform.

 resource "azurerm_policy_definition" "app_service_msi_definition" {
   name         = "appServiceMSI"
   policy_type  = "Custom"
   mode         = "Indexed"
   display_name = "App Service - Managed Identity Enabled"

   metadata = <<METADATA
     {
       "version": "1.0.0",
       "category": "App Service"
     }

This is pretty straightforward with the biggest callouts being the policy_type needing to be Custom to indicate we are creating our own and the display_name as this is what will appear in Azure Policy. The next section policy_rule might feel a little more unnatural if you are more verse in Terraform.

   policy_rule = <<POLICY_RULE
   {
       "if": {
         "allOf": [
           {
             "field": "type",
             "equals": "Microsoft.Web/sites"
           },
           {
             "field": "kind",
             "like": "app*"
           }
         ]
       },
       "then": {
         "effect": "[parameters('effect')]",
         "details": {
           "type": "Microsoft.Web/sites/config",
           "name": "web",
           "existenceCondition": {
             "field": "Microsoft.Web/sites/config/managedServiceIdentityId",
             "exists": "true"
           }
         }
       }
       }
 POLICY_RULE


   parameters =  <<PARAMETERS
     {
         "effect": {
                 "type": "String",
                 "metadata": {
                 "displayName": "Effect",
                 "description": "Enable or disable the execution of the policy"
                 },
                 "allowedValues": [
                 "AuditIfNotExists",
                 "Disabled",
                 "Deny"
                 ],
                 "defaultValue": "AuditIfNotExists"
             }
   }
 PARAMETERS

If you are familiar with Azure’s native Azure Resource Manager (ARM) Templates, this section may look a little more familiar to you. Stop and think about it and this makes sense. How can Terraform account for all the possibilities we can pass into a custom policy? The same can be said on how Azure Logic App and Data Factory code is deployed inside ARM templates. There just simply isn’t one set schema with predefined options that we can point to and utilize.

So that being said how can we read this and what does it mean? Let’ stake it block by block.

 "if": {
         "allOf": [
           {
             "field": "type",
             "equals": "Microsoft.Web/sites"
           },
           {
             "field": "kind",
             "like": "app*"
           }
         ]
       },

So this is stating what we are evaluating. In this case, we want to evaluate all the resource types of Microsoft.Web/sites and that are of kind app (This denotes App Service).

If a resource meets this criteria, then this next block is evaluated.

 "then": {
         "effect": "[parameters('effect')]",
         "details": {
           "type": "Microsoft.Web/sites/config",
           "name": "web",
           "existenceCondition": {
             "field": "Microsoft.Web/sites/config/managedServiceIdentityId",
             "exists": "true"
           }
         }
       }
       }

Let’s put a pin on effect for now since we are dealing with that via an external parameter. So, if the resource is of type Microsoft.Web/sites AND of kind = app* then let’s evaluate for the existence of a Microsoft.Web/sites/config/managedServiceIdentityId under the Microsoft.Web/sites/config property. If wondering how we knew this would exist here I recommend two methods. First evaluate it against the documentation for Microsoft.Web sites/config, a second way would be within an App Service in Azure where the Managed Identity has already been enabled, select Export Template. This will include the property in the export.

Using Parameters

So now what happens if the condition of manageServiceyIdentityId not being present is met. That’s where the effect comes into play. So in this scenario we want to account for different environments that have their own subscriptions might have different desirable policy effects. In simple terms maybe our policy in a DEC subscription should only report if the field is missing while in PRD we’d want to deny it. This can be accomplish by setting up the effect as a parameter.

To do this in Terraform:


   parameters =  <<PARAMETERS
     {
         "effect": {
                 "type": "String",
                 "metadata": {
                 "displayName": "Effect",
                 "description": "Enable or disable the execution of the policy"
                 },
                 "allowedValues": [
                 "AuditIfNotExists",
                 "Disabled",
                 "Deny"
                 ],
                 "defaultValue": "AuditIfNotExists"
             }
   }
 PARAMETERS

Here we are creating a parameter called effect whose allowed values are AuditIfNotExists, Disabled, and Deny. If no parameter is passed into it then the AuditIfNotExists effect will be assigned.

So now how to substitute these values in Terraform for different environments. This could be accomplished via Terraform variables. In this case it would look something like:

variable "app_service_msi_parameters" {
  default = <<PARAMETERS
{
  "effect": {
    "value": "Deny
  }
}
PARAMETERS
}

Policy Assignment

So there you have it! By setting the desirable values per environment we can now deploy different policy actions for various subscriptions. However; we still would most likely want to assign the policy. This can be done via the azurerm_policy_assignment type which will pass in the parameter defined in the variable file above. Something like:

 resource "azurerm_policy_assignment" "app_service_msi_assignment" {
   name                 = "example-policy-assignment"
   scope                = data.azurerm_subscription.subscription_info.id
   policy_definition_id = azurerm_policy_definition.app_service_msi_definition.id
   description          = "Policy Assignment for MSI on App Services"
   display_name         = "App Service - Managed Identity Enabled"

   parameters =var.app_service_msi_parameters 

 }

This will now create the policy definition and create an assignment for said policy definition at the subscription level. The scope can be defined to resource group, resource, etc…

Hopefully this has been helpful! As always feel free to provide any feedback and/or constructive criticism!