Azure, Bicep, Professional

API Manager, Host Name Certificate, and Key Vault

Overview

When leveraging Azure API Manager (APIM) it is not an uncommon request to have a custom DNS record in front of it. On top of that it wouldn’t be uncommon to have the SSL cert for DNS records in a Key Vault (In particular if it’s not leveraging some of the Azure native components like Front Door). This will walk through one way of integrating API Manger and Key Vault via RBAC and Bicep.

Problem

The issue becomes how to enable APIM to communicate to Key Vault via the most secure method available, Role Based Access Control (RBAC).

The Host name configuration is a property within APIM and is not a sub resource. As such APIM needs to have access to the Key Vault at deployment time in order to retrieve the certificate. The only issue is System Managed isn’t known until the resource is created.

This creates a Chicken and Egg Problem, which I have highlighted how to solve with Azure Key Vault. However, this solution does not work given the nature of the APIM deployment.

Disclaimer

As pointed in the comments leveraging a User Assigned Identity attached to APIM for retrieving the SSL cert from Key Vault will only work if Key Vault Firewall is not enabled on the Key Vault:

Source from: https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-use-managed-service-identity#use-ssl-tls-certificate-from-azure-key-vault-ua

Solution

To get around this we create a User Assigned Identity. The User Assigned Identity (UAI) will be created before APIM is created and after the Key Vault is created. This will allow us to attach the RBAC role to the UAI and then turn around and assign the UAI to APIM.

user-assigned-identity.module.bicep

param userAssignIdentityName string
param location string = resourceGroup().location
param tags object

resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
  name: userAssignIdentityName
  location: location
  tags: tags
}

output userAssignedIdentityNameOutput string = userAssignedIdentity.name
output userAsisgnedIdentityId string = userAssignedIdentity.properties.principalId

Nothing fancy here but we are exporting the UAI name and the Principal ID for future use.

key-vault.module.bicep

param location string = resourceGroup().location
param tags object
param tenantId string = subscription().tenantId
param keyVaultName string

param principalType string = 'ServicePrincipal'
param principalId string


var roleIds = [
  'a4417e6f-fecd-4de8-b567-7b0420556985' //Key Vault Certificate Office
  '4633458b-17de-408a-b874-0445c86b69e6' //Key Vault Secret User
]

resource keyVault 'Microsoft.KeyVault/vaults@2021-11-01-preview' = {
  name: keyVaultName
  location: location
  tags: tags
  properties: {

    enabledForTemplateDeployment: true
    enablePurgeProtection: true
    enableRbacAuthorization: true
    enableSoftDelete: true

    sku: {
      family: 'A'
      name: 'standard'
    }
    softDeleteRetentionInDays: 90
    tenantId: tenantId
  }
}

resource roleAssignment'Microsoft.Authorization/roleAssignments@2021-04-01-preview' =[ for roleId in roleIds: {
  name: guid(principalId, roleId,keyVaultName)
  scope: keyVault
  properties: {
    roleDefinitionId: '/providers/Microsoft.Authorization/roleDefinitions/${roleId}'
    principalId: principalId
    principalType: principalType
  }
}]



output keyVaultNameOutput string = keyVault.name
output keyVaultURI string = keyVault.properties.vaultUri

Just an Azure Key Vault with soft delete defined. We do include the RBAC assignment and will loop through the RoleIDs being passed in.

api-management.module.bicep

param tags object
param publisherEmail string
param publisherName string
param apiManagementName string
param capacity int
param appInsightsID string
param appInsightsInstumentationKey string
param location string
param apimHostName string
param keyVaultURI string
param subdomainCertKeyName string
param userAssignedIdentityNameOutput string

resource ApiManagedUserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = {
  name: userAssignedIdentityNameOutput
}

var userAssignedIdentity = {
  Default:{
    '${ApiManagedUserAssignedIdentity.id}' : {}
  }
}

resource ApiManagement 'Microsoft.ApiManagement/service@2021-08-01' = {
  name: apiManagementName
  tags: tags
  location: location
  sku: {
    capacity: capacity
    name: 'Consumption'
  }
  identity:{
    type: 'SystemAssigned, UserAssigned'
    userAssignedIdentities: userAssignedIdentity['Default']
  }
  properties: {
    publisherEmail: publisherEmail
    publisherName: publisherName
    hostnameConfigurations:[
    {
      type:'Proxy'
      hostName: '${apiManagementName}.azure-api.net'
      negotiateClientCertificate:false
      defaultSslBinding:false
      certificateSource:'BuiltIn'
    }
    {
     type:'Proxy'
     keyVaultId: '${keyVaultURI}secrets/${subdomainCertKeyName}'
     identityClientId: ApiManagedUserAssignedIdentity.properties.clientId
     hostName: apimHostName
     certificateSource: 'KeyVault'
     defaultSslBinding: true
     negotiateClientCertificate:false
   }
  ]
  }
}

output apimResourceID string = ApiManagement.id
output apimNameOutput string = ApiManagement.name

We import in the UAI as the first step using the existing keyword. This will be used to assign to the APIM instance.

main.bicep

param defaultLocation string
param env string
param apiPublisherName string
param apiPublisherEmail string
param apimCapacity int

param locationLookup object = {
  'centralus': 'cus'
  'eastus': 'eus'
  'westus': 'wus'
}
param utc string = utcNow()

var apiManagementName = 'apim-${appPrefix}-${appShortName}-${locationLookup[defaultLocation]}-${env}'
var defaultTags  = {
  'env' : env
  'app-name' : appName
}
var apimResourceGroupName = 'rg-${appPrefix}-${appShortName}-apim-${locationLookup[defaultLocation]}-${env}'
var appName = 'App- ${toUpper(env)}'
var apimkeyVaultName = 'kv-manual-apim-${locationLookup[defaultLocation]}-${env}'
var productionDeployment = env == 'prd'
var apimHostName = productionDeployment ? 'apim.site.com' : '${env}-apim.site.com'
var subdomainCertKeyName = '${env}-App-APIM'
var userAssignedIdentityName = '${env}-${appPrefix}-${appShortName}-api-manager'

module apiManagementModule 'modules/api-management.module.bicep' = {
  name: 'apiManagementModule${utc}'
  params: {
    apiManagementName: apiManagementName
    publisherEmail: apiPublisherEmail
    publisherName: apiPublisherName
    tags: defaultTags
    capacity: apimCapacity
    location: resourceGroup().location
    apimHostName: apimHostName
    keyVaultURI: apimKeyVaultModule.outputs.keyVaultURI
    subdomainCertKeyName:subdomainCertKeyName
    userAssignedIdentityNameOutput: apimUserAsisgnedIdentity.outputs.userAssignedIdentityNameOutput
  }
}


module apimKeyVaultModule 'modules/key-vault.module.bicep' = {
  name: 'keyVaultModule${utc}'
  params: {
    tags: defaultTags
    keyVaultName: apimkeyVaultName
    location: resourceGroup().location
    principalId: apimUserAsisgnedIdentity.outputs.userAsisgnedIdentityId
  }
}


module apimUserAsisgnedIdentity 'modules/user-assigned-identity.module.bicep' = {
  name: 'apiManagementHostNameModule${utc}'
  params: {
    tags: defaultTags
    location: resourceGroup().location
    userAssignIdentityName: userAssignedIdentityName
  }
 
}

Conclusion

This post went over deploying and integrating API Manger and Key Vault via RBAC and Bicep in the most secure way leveraging RBAC. We covered how to handle for deployment dependencies by leveraging a User Assigned Identity.

If you’d like to know more here is a great thread over on the Bicep Q&A on this topic. It is also a source I used to help write this.