'How to Use Azure Managed Identity as Credential For Debugging Cosmos Database access inside of Blazer Server App?
Background:
As described in bullet #4 of my other post I'm trying to follow CDennig's example bicep script to grant my blazor (server) application access to both my key vault (to get the AAD client secret) and the azure cosmos db. CDennig's bicep script grants a managed identity (MI) access (RBAC) to the cosmos database. Unlike CDennig's use of a kubernetes's cluster, I'm using a App Service Webapp and assigning the MI as the user assigned principal of the webapp.
When I deploy to the Azure App Service as a web app, the deployment fails with no error messages... I strongly suspect the problem is that the managed identity (MI) is being denied access to my key vault in spite having granted it my bicep script.
Questions:
How can I run my blazor web app locally on my development machine using the MI as the credential to simulate the environment of running inside the App Serviced webapp and confirm my hypothesis that the key vault access is the problem?
If it is the problem, how would I fix it? See my bicep script below where I grant the MI access to the key vault.
2022 April 17 Sun Morning Update:
Problem has been partially fixed by granting both the system assigned and user assigned MI for the App Service Web App access to the key vault and I can now log in when running on Azure. It seems that Azure is ignoring my user assigned MI for access to the key vault and I suspect it is also ignoring the RBAC for the user assigned MI RBAC. When running locally on my development machine I can also write to the cosmos database because of the RBAC applied to my personal Azure account.
I would still like to know how to run under the user assigned MI when running locally on my development machine. Is this possible with one of the DefaultAzureCredential classes?
And of course, I would still like to know how to access cosmos database via RBAC (no passwords) when deployed to Azure.
2022 April 17 Sun Evening Update: Progress!
When I grant the RBAC access to the system assigned service principal for the Azure App Service Web App using powershell, it works and I can access the cosmos database in azure app service... Please help me fix my bicep script. When I try to fix my script by assigning RBAC to the user assigned service principal I get the error described here.
It seems that this could be done with DefaultAzureCredential I'm not clear on how to do this.
Here is my bicep script:
/*
* Begin commands to execute this file using Azure CLI with PowerShell
* $name='AADB2C_BlazorServerDemo'
* $rg="rg_$name"
* $loc='westus2'
* echo az.cmd group create --location $loc --resource-group $rg
* az.cmd group create --location $loc --resource-group $rg
* echo Set-AzDefault -ResourceGroupName $rg
* Set-AzDefault -ResourceGroupName $rg
* echo begin create deployment group
* az.cmd identity create --name umid-cosmosid --resource-group $rg --location $loc
* $MI_PRINID=$(az identity show -n umid-cosmosid -g $rg --query "principalId" -o tsv)
* write-output "principalId=${MI_PRINID}"
* az.cmd deployment group create --name $name --resource-group $rg --template-file deploy.bicep --parameters '@deploy.parameters.json' --parameters managedIdentityName=umid-cosmosid ownerId=$env:AZURE_OBJECTID --parameters principalId=$MI_PRINID
* Get-AzResource -ResourceGroupName $rg | ft
* echo end create deployment group
* End commands to execute this file using Azure CLI with Powershell
*
*/
@description('AAD Object ID of the developer so s/he can access key vault when running on development')
param ownerId string
@description('Principal ID of the managed identity')
param principalId string
var principals = [
principalId
ownerId
]
@description('The base name for resources')
param name string = uniqueString(resourceGroup().id)
@description('The location for resources')
param location string = resourceGroup().location
@description('Cosmos DB Configuration [{key:"", value:""}]')
param cosmosConfig object
@description('Azure AD B2C Configuration [{key:"", value:""}]')
param aadb2cConfig object
@description('Azure AD B2C App Registration client secret')
@secure()
param clientSecret string
@description('Dummy Azure AD B2C App CosmosConnectionString')
@secure()
param cosmosConnectionString string=newGuid()
@description('The web site hosting plan')
@allowed([
'F1'
'D1'
'B1'
'B2'
'B3'
'S1'
'S2'
'S3'
'P1'
'P2'
'P3'
'P4'
])
param sku string = 'F1'
@description('The App Configuration SKU. Only "standard" supports customer-managed keys from Key Vault')
@allowed([
'free'
'standard'
])
param configSku string = 'free'
resource config 'Microsoft.AppConfiguration/configurationStores@2020-06-01' = {
name: 'asc-${name}config'
location: location
sku: {
name: configSku
}
resource Aadb2cConfigValues 'keyValues@2020-07-01-preview' = [for item in items(aadb2cConfig): {
name: 'AzureAdB2C:${item.key}'
properties: {
value: item.value
}
}]
resource CosmosConfigValues 'keyValues@2020-07-01-preview' = [for item in items(cosmosConfig): {
name: 'CosmosConfig:${item.key}'
properties: {
value: item.value
}
}]
resource aadb2cClientSecret 'keyValues@2020-07-01-preview' = {
// Store secrets in Key Vault with a reference to them in App Configuration e.g., client secrets, connection strings, etc.
name: 'AzureAdB2C:ClientSecret'
properties: {
// Most often you will want to reference a secret without the version so the current value is always retrieved.
contentType: 'application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8'
value: '{"uri":"${kvaadb2cSecret.properties.secretUri}"}'
}
}
resource cosmosConnectionStringSecret 'keyValues@2020-07-01-preview' = {
// Store secrets in Key Vault with a reference to them in App Configuration e.g., client secrets, connection strings, etc.
name: 'CosmosConnectionStringSecret'
properties: {
// Most often you will want to reference a secret without the version so the current value is always retrieved.
contentType: 'application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8'
value: '{"uri":"${kvCosmosConnectionStringSecret.properties.secretUri}"}'
}
}
}
resource kv 'Microsoft.KeyVault/vaults@2019-09-01' = {
// Make sure the Key Vault name begins with a letter.
name: 'kv-${name}'
location: location
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: subscription().tenantId
accessPolicies: [
{
tenantId: subscription().tenantId
objectId: ownerId
permissions:{
secrets:[
'all'
]
}
}
{
tenantId: subscription().tenantId
objectId: principalId
permissions: {
// Secrets are referenced by and enumerated in App Configuration so 'list' is not necessary.
secrets: [
'get'
]
}
}
]
}
}
// Separate resource from parent to reference in configSecret resource.
resource kvaadb2cSecret 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {
name: '${kv.name}/AzureAdB2CClientSecret'
properties: {
value: clientSecret
}
}
resource kvCosmosConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {
name: '${kv.name}/CosmosConnectionStringSecret'
properties: {
value: cosmosConnectionString
}
}
resource plan 'Microsoft.Web/serverfarms@2020-12-01' = {
name: '${name}plan'
location: location
sku: {
name: sku
}
kind: 'linux'
properties: {
reserved: true
}
}
@description('Specifies managed identity name')
param managedIdentityName string
resource msi 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = {
name: managedIdentityName
}
// https://github.com/Azure/azure-quickstart-templates/blob/master/quickstarts/microsoft.web/web-app-managed-identity-sql-db/main.bicep#L73
resource web 'Microsoft.Web/sites@2020-12-01' = {
name: '${name}web'
location: location
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${msi.id}': {}
}
}
properties: {
httpsOnly: true
serverFarmId: plan.id
siteConfig: {
linuxFxVersion: 'DOTNETCORE|6'
connectionStrings: [
{
name: 'AppConfig'
connectionString: listKeys(config.id, config.apiVersion).value[0].connectionString
}
]
}
}
}
output appConfigConnectionString string = listKeys(config.id, config.apiVersion).value[0].connectionString
// output siteUrl string = 'https://${web.properties.defaultHostName}/'
output vaultUrl string = kv.properties.vaultUri
var dbName = 'rbacsample'
var containerName = 'data'
// Cosmos DB Account
resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2021-06-15' = {
name: 'cosmos-${uniqueString(resourceGroup().id)}'
location: location
kind: 'GlobalDocumentDB'
properties: {
consistencyPolicy: {
defaultConsistencyLevel: 'Session'
}
locations: [
{
locationName: location
failoverPriority: 0
}
]
capabilities: [
{
name: 'EnableServerless'
}
]
disableLocalAuth: false // switch to 'true', if you want to disable connection strings/keys
databaseAccountOfferType: 'Standard'
enableAutomaticFailover: true
publicNetworkAccess: 'Enabled'
}
}
// Cosmos DB
resource cosmosDbDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2021-06-15' = {
name: '${cosmosDbAccount.name}/${dbName}'
location: location
properties: {
resource: {
id: dbName
}
}
}
// Data Container
resource containerData 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2021-06-15' = {
name: '${cosmosDbDatabase.name}/${containerName}'
location: location
properties: {
resource: {
id: containerName
partitionKey: {
paths: [
'/partitionKey'
]
kind: 'Hash'
}
}
}
}
@batchSize(1)
module cosmosRole 'cosmosRole.bicep' = [for (princId, jj) in principals: {
name: 'cosmos-role-definition-and-assignment-${jj}'
params: {
cosmosDbAccountId: cosmosDbAccount.id
cosmosDbAccountName: cosmosDbAccount.name
principalId: princId
it: jj
}
}]
Here is the module to create the role assignments and definitions:
@description ('cosmosDbAccountId')
param cosmosDbAccountId string
@description ('cosmosDbAccountName')
param cosmosDbAccountName string
@description('iteration')
param it int
@description('Principal ID of the managed identity')
param principalId string
var roleDefId = guid('sql-role-definition-', principalId, cosmosDbAccountId)
var roleDefName = 'Custom Read/Write role-${it}'
var roleAssignId = guid(roleDefId, principalId, cosmosDbAccountId)
resource roleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2021-06-15' = {
name: '${cosmosDbAccountName}/${roleDefId}'
properties: {
roleName: roleDefName
type: 'CustomRole'
assignableScopes: [
cosmosDbAccountId
]
permissions: [
{
dataActions: [
'Microsoft.DocumentDB/databaseAccounts/readMetadata'
'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*'
]
}
]
}
}
resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2021-06-15' = {
name: '${cosmosDbAccountName}/${roleAssignId}'
properties: {
roleDefinitionId: roleDefinition.id
principalId: principalId
scope: cosmosDbAccountId
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|