Use nested ARM template to provision User Assigned Managed Identity and add access policy to key-vault from different subscription

Evgeny Borzenin · May 11, 2020

Azure Application Gateway v2 supports integration with Key Vault for server certificates that are attached to HTTPS-enabled listeners. One of the main benefits of this integration, is support for automatic renewal of certificates. This is especially practical if you use Azure App Service Certificates that you purchase and maintain directly on Azure.

Application Gateway integration with key-vault requires a three-step configuration process:

infra

1. Create a user-assigned managed identity

2. Configure access policy at key-vault

We need to define access policies in the key-vault to allow the identity to be granted get access to the secret.

3. Configure the application gateway

After we complete the two previous steps, we can configure application gateway to use the user-assigned managed identity

"identity": {
    "type": "UserAssigned",
    "userAssignedIdentities": {
        "managed-identity-id": {
        }
    }
}

where userAssignedIdentities is defined as

The list of user identities associated with resource. The user identity dictionary key references will be ARM resource ids in the form: ‘/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}’.

And HTTP listener’s TLS/SSL certificate to point to the complete URI of secret ID containing certificate.

"sslCertificates": [
    {
        "name": "appGwSslCertificate",
        "properties": {
            "keyVaultSecretId": "[parameters('certificateKeyVaultSecretId')]"
        }
    }
]

Check out this complete implementation of ARM template for AGW v2 with key-vault integration from Azure Resource Manager QuickStart Templates collection.

At my current project we use “hybrid” approach, that is - we provision Application Gateway instance using ARM templates, but creation of the user assigned managed identities and configuring access policy at key-vault we do with az cli script as part of resource group initialization process. Here is the script that does the job.

#!/usr/bin/env bash
# Usage:
#   ./02-create-mi.sh iac-certificates-kv <kv-subscription-id> iac-agw-mi iac-agw-ssl-rg

kvName=$1
kvSubscriptionId=$2
miName=$3
miResourceGroupName=$4

echo -e "Create new user assigned managed identity ${miName} in ${miResourceGroupName}"
miObjectId=$(az identity create -n ${miName} -g ${miResourceGroupName} -l westeurope --query principalId -o tsv)

echo -e "Grant ${miName}(${miObjectId}) secret get permission at ${kvName}"
az keyvault set-policy --name ${kvName} --object-id ${miObjectId} --secret-permissions get --subscription ${kvSubscriptionId} 1> /dev/null

In this post I decided to check what it takes to implement ARM template that does the same.

Use-case description

Our managed identity and key-vault with SSL certificates are located at the different resource groups in different Azure subscriptions. What we want to implement is ARM template that will:

  • create user assigned managed identity called iac-agw-mi
  • grant iac-agw-mi managed identity get access policy to the secrets level at iac-certificates-kv key-vault.

User Assigned Managed Identity

This is probably one of the simplest ARM resources you can find. Here is Microsoft.ManagedIdentity userAssignedIdentities template reference documentation. There are no configurable properties, so the resource implementation looks like this:

{
    "name": "[parameters('managedIdentityName')]",
    "type": "Microsoft.ManagedIdentity/userAssignedIdentities",
    "apiVersion": "2018-11-30",
    "location": "[variables('loaction')]"
}

Configure key-vault accessPolicies

You can manage key-vault’s access policies with Microsoft.KeyVault vaults/accessPolicies ARM resource.

Here is a good example of how you can add an Access Policy to an existing key-vault.

Let’s look at the syntax:

{
    "type": "Microsoft.KeyVault/vaults/accessPolicies",
    "name": "[concat(parameters('certificateKeyVaultName'), '/add')]",
    "apiVersion": "2019-09-01",
    "properties": {
        "accessPolicies": [
            {
                "tenantId": "[parameters('tenantId')]",
                "objectId": "[reference(parameters('managedIdentityName')).principalId]",
                "permissions": {
                    "secrets": [
                        "get"
                    ]
                }
            }
        ]
    }
}

name can be one of the following values (add, replace, remove). Since we adding the new policy, we will use /add.

objectId - is the object ID of our managed identity, since both resources are at the same ARM template, we can use reference() function to get our managed identity object id.

tenantId - the Azure Active Directory tenant ID that should be used for authenticating requests to the key vault.

permissions - permissions the identity has for keys, secrets and certificates.

If we try to deploy this template, it will fail with the following error message:

$ az deployment group create -g iac-agw-ssl-rg --template-file template.json
Deployment failed. Correlation ID: xxxxxxxxxxxxxxx. {
  "error": {
    "code": "ParentResourceNotFound",
    "message": "Can not perform requested operation on nested resource. Parent resource 'iac-certificates-kv' not found."
  }
}

That’s actually correct, because managed identity and certificate’s key-vault are deployed to different resource groups in different Azure subscriptions. How can we solve this with ARM templates?

Nested templates

ARM templates has a technique called, nested templates.

To nest a template, add a deployments resource to your main template. In the template property, specify the ARM template syntax:

{
    "name": "grant-access",
    "type": "Microsoft.Resources/deployments",
    "apiVersion": "2019-10-01",
    "subscriptionId": "[parameters('certificateKeyVaultSubscriptionId')]",
    "resourceGroup": "[parameters('certificateKeyVaultResourceGroupName')]",
    "dependsOn": [
        "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName'))]"
    ],
    "properties": {
        "mode": "Incremental",
        "template": {
            "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
            "contentVersion": "1.0.0.0",
            "resources": [
                {
                    "type": "Microsoft.KeyVault/vaults/accessPolicies",
                    "name": "[concat(parameters('certificateKeyVaultName'), '/add')]",
                    "apiVersion": "2019-09-01",
                    "properties": {
                        "accessPolicies": [
                            {
                                "tenantId": "[parameters('tenantId')]",
                                "objectId": "[reference(parameters('managedIdentityName')).principalId]",
                                "permissions": {
                                    "secrets": [
                                        "get"
                                    ]
                                }
                            }
                        ]
                    }
                }
            ]
        }
    }
}

As you can see, we can specify what subscription (subscriptionId attribute) and resource group (resourceGroup attribute) we want to deploy resource to. Inside properties->template element, we just implemented regular Microsoft.KeyVault/vaults/accessPolicies resource.

Final version

Here is our final version of the template:

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "certificateKeyVaultName": {
            "type": "string"
        },
        "certificateKeyVaultResourceGroupName": {
            "type": "string"
        },
        "certificateKeyVaultSubscriptionId": {
            "type": "string"
        },
        "managedIdentityName": {
            "type": "string"
        },
        "tenantId": {
            "type": "string",
            "defaultValue": "[subscription().tenantId]"
        }
    },
    "variables": {
        "loaction": "[resourceGroup().location]"
    },
    "resources": [
        {
            "name": "[parameters('managedIdentityName')]",
            "type": "Microsoft.ManagedIdentity/userAssignedIdentities",
            "apiVersion": "2018-11-30",
            "location": "[variables('loaction')]"
        },
        {
            "name": "grant-access",
            "type": "Microsoft.Resources/deployments",
            "apiVersion": "2019-10-01",
            "subscriptionId": "[parameters('certificateKeyVaultSubscriptionId')]",
            "resourceGroup": "[parameters('certificateKeyVaultResourceGroupName')]",
            "dependsOn": [
                "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName'))]"
            ],
            "properties": {
                "mode": "Incremental",
                "template": {
                    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
                    "contentVersion": "1.0.0.0",
                    "resources": [
                        {
                            "type": "Microsoft.KeyVault/vaults/accessPolicies",
                            "name": "[concat(parameters('certificateKeyVaultName'), '/add')]",
                            "apiVersion": "2019-09-01",
                            "properties": {
                                "accessPolicies": [
                                    {
                                        "tenantId": "[parameters('tenantId')]",
                                        "objectId": "[reference(parameters('managedIdentityName')).principalId]",
                                        "permissions": {
                                            "secrets": [
                                                "get"
                                            ]
                                        }
                                    }
                                ]
                            }
                        }
                    ]
                }
            }
        }
    ]
}

To sum-up

So, I managed to grant access to key-vault for user assigned identity from ARM templates. I personally think that it’s easier with az cli script, but at the same time, if you don’t want to (or if you are not allowed to) use a mix of ARM templates and az cli scripts, it’s totally possible to implement everything with ARM templates.

If you have any issues/comments/suggestions related to this post, you can reach out to me at evgeny.borzenin@gmail.com.

With that - thanks for reading!

LinkedIn, Twitter, Facebook