Skip to main content

EventGrid: Restricting Managed Identity Types on System Topics

When I deploy Event Grid system topics, the default behavior is to create them without any identity at all. That's a problem because it means teams can still use key-based authentication for event subscriptions, or they'll configure a user-assigned managed identity that I can't track or control. This policy forces every system topic to use a managed identity, and by default, limits it to system-assigned only.

The specific control I'm enforcing is that the identity.type field on Microsoft.EventGrid/systemTopics must match one of the allowed values I specify. In most environments, I set this to ["SystemAssigned"] because system-assigned identities are lifecycle-tied to the resource. When the system topic gets deleted, the identity goes with it. User-assigned identities require separate lifecycle management, and I've inherited environments where orphaned user-assigned identities pile up because nobody remembers to clean them up after the resources are gone.

Policy definition

{
"properties": {
"displayName": "eventgrid-restrict-managed-identity",
"policyType": "Custom",
"mode": "All",
"description": "",
"metadata": {
"category": "Event Grid",
"version": "1.0.0"
},
"version": "1.0.0",
"parameters": {
"effect": {
"type": "String",
"metadata": {
"displayName": "Effect"
},
"allowedValues": [
"Audit",
"Deny",
"Disabled"
],
"defaultValue": "Deny"
},
"allowedIdentityTypes": {
"type": "Array",
"metadata": {
"displayName": "Allowed Identity Types on the Topic",
"description": "Which managed identity types are permitted. Defaults to SystemAssigned only. Set to [\"SystemAssigned\", \"UserAssigned\", \"SystemAssigned, UserAssigned\"] to allow any managed identity type. ARM returns \"SystemAssigned, UserAssigned\" with a space for dual identity."
},
"defaultValue": [
"SystemAssigned"
]
}
},
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.EventGrid/systemTopics"
},
{
"not": {
"field": "identity.type",
"in": "[parameters('allowedIdentityTypes')]"
}
}
]
},
"then": {
"effect": "[parameters('effect')]"
}
}
}
}

What the policy checks

The policy targets Microsoft.EventGrid/systemTopics and validates the identity.type field. If the identity type isn't in the allowedIdentityTypes parameter, the policy fires. The default allowed value is ["SystemAssigned"], which means:

  • A system topic with no identity at all (the field is absent or null) gets denied
  • A system topic with identity.type set to "UserAssigned" gets denied
  • A system topic with identity.type set to "SystemAssigned, UserAssigned" (the dual identity case) gets denied
  • A system topic with identity.type set to "SystemAssigned" passes

The ARM API returns "SystemAssigned, UserAssigned" with a space when both identity types are configured. That's not a typo in the parameter description — it's how the API actually works. If I want to allow dual identity, I need to add "SystemAssigned, UserAssigned" (with the space) to the allowedIdentityTypes array.

Terraform assignment

I assign this at the management group level, not at individual subscriptions. The identity requirement applies uniformly across all landing zones in my deployments.

resource "azurerm_policy_definition" "eventgrid_restrict_mi" {
name = "eventgrid-restrict-managed-identity"
policy_type = "Custom"
mode = "All"
display_name = "EventGrid: Restrict Managed Identity Types on System Topics"

metadata = jsonencode({
category = "Event Grid"
version = "1.0.0"
})

policy_rule = jsonencode({
if = {
allOf = [
{
field = "type"
equals = "Microsoft.EventGrid/systemTopics"
},
{
not = {
field = "identity.type"
in = "[parameters('allowedIdentityTypes')]"
}
}
]
}
then = {
effect = "[parameters('effect')]"
}
})

parameters = jsonencode({
effect = {
type = "String"
defaultValue = "Deny"
allowedValues = ["Audit", "Deny", "Disabled"]
metadata = {
displayName = "Effect"
}
}
allowedIdentityTypes = {
type = "Array"
defaultValue = ["SystemAssigned"]
metadata = {
displayName = "Allowed Identity Types on the Topic"
description = "Which managed identity types are permitted. Defaults to SystemAssigned only."
}
}
})
}

resource "azurerm_management_group_policy_assignment" "eventgrid_restrict_mi_corp" {
name = "eg-restrict-mi-corp"
management_group_id = azurerm_management_group.corp.id
policy_definition_id = azurerm_policy_definition.eventgrid_restrict_mi.id
display_name = "EventGrid: Restrict Managed Identity Types (Corp)"
enforce = true

parameters = jsonencode({
effect = {
value = "Deny"
}
allowedIdentityTypes = {
value = ["SystemAssigned"]
}
})
}

I start with Audit mode, run a compliance scan, then flip to Deny after I've confirmed there are no existing system topics that would be non-compliant. Rolling out with Deny from day one breaks things, and the first support ticket I get is from a team that can't figure out why their Event Grid deployment is failing.

Things I've gotten wrong

The identity field can be absent entirely. When I first wrote this policy, I tested it against system topics that had identity.type set to "None" or null. That works fine. But if the identity block is missing from the ARM template entirely, the policy still fires because the field doesn't exist to compare against the parameter. That's the correct behavior — I want to deny system topics without any identity — but it surprised me the first time I tested it.

The dual identity string has a space in it. ARM returns "SystemAssigned, UserAssigned" with a space after the comma. If I put "SystemAssigned,UserAssigned" (no space) in my allowedIdentityTypes array, it won't match. I spent ten minutes debugging this before I looked at the actual API response.

This policy only covers system topics, not event subscriptions. The managed identity on the system topic itself is separate from the managed identity used for event delivery in the subscription. This policy doesn't prevent someone from creating an event subscription that uses key-based authentication to deliver to a destination. That gap is what the second policy addresses.

User-assigned identities require explicit RBAC grants. When I allow user-assigned identities, the team deploying the system topic needs to grant the identity the correct roles on whatever resources it needs to interact with. With system-assigned identities, I can often automate that grant as part of the deployment because the identity gets created at the same time as the topic. With user-assigned, the identity exists first, which means the RBAC grant happens in a separate step and teams forget to do it.