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!
Hi John, nice post. Thanks for that.
Any suggestions on how to add support to Policy Exemptions? From what i read it`s not yet supported, but this is something i need to implement in one of my projects.
Good question….I did look and see there is an open Terraform feature request for this. To work around this, I haven’t tried it myself, would suggest using the Terraform local-exec Provider. And would suggest running the PowerShell commands to add the exemption to the assignment.
Alternatively, could run these commands as a separate task in your deployment. However, would need to keep track of the variables for scope, name, etc…..
Hope this helps!