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:
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.
This might not work if Azure Key Vault is behind firewall
I am honestly not sure. I would guess it would depend on the firewall configuration. Knee jerk would be Azure DevOp’s IP range would need to be allowed through to talk to Key Vault. I do know this is a fairly common issue in terms of Azure hosted agent’s IP addresses being allowed to talk to Azure resources behind a firewall.See the updated disclaimer that this has been confirmed as a limitation of APIM leveraging a user assigned identity to access an Azure Key Vault that used a firewall.
Does this work with a user assigned identity if the public access is disabled on the keyvault
Please see 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 and https://github.com/MicrosoftDocs/azure-docs/issues/86983#issuecomment-1138836139 – there is explicitly stated that if the firewall is enabled on Key Vault, then you can’t use User Assigned Managed Identities with API Management.
Thanks Hans for pointing that out. I was unaware so appreciate learning something new. I have also updated the article with a disclaimer for those who come across this in the future.