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.