Denny
Denny

Reputation: 306

Keyvault Stored Secrets for Azure Container Apps with RBAC auth from ARM templates

I feel like I'm stuck in a catch 22 with how Microsoft has implemented this functionality in the Container App templates. The issue, it seems to me, is that is actually validates the secret access as part of the template deploment... which creates the issue of the Managed Identity (system) not actually yet existing since the app hasn't been created, thus no way to assign that identity RBAC to the Keyvault, thus validation failes.

Am I missing something here, or is the process really going to require 2 templates... 1 with no secret references, just to "create" the thing, and then a second to actually properly configure it now that the managed identity is availabe?

The following field(s) are either invalid or missing. Field 'configuration.secrets' is invalid with details: 'Invalid value: "mysecret-name": 
Unable to get value using Managed identity system for secret mysecret-name. Error: unable to fetch secret 'mysecret-name' using Managed identity 'system'';.
"configuration": {
  "secrets": [
    {
      "name": "mysecret-name",
      "keyVaultUrl": "[concat('https://',variables('vaultname'),'.vault.azure.net/secrets/mysecret')]",
      "identity": "system"
    }
  ]
}

Upvotes: 1

Views: 1671

Answers (4)

Benjamin RD
Benjamin RD

Reputation: 12044

It is important to validate what strategy are you using to access the KeyVault. It can be RBAC or Access Policy.


resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = {
  name: keyVaultName
  location: location
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: subscription().tenantId
    enableRbacAuthorization: true
  }
}

Using enableRbacAuthorization: true you will need to grant the RBAC Role Assignment, possible using:

resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: managedIdentityName
  location: location
}

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(managedIdentity.id, 'KeyVaultSecretsUser')
  scope: keyVault
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User
    principalId: managedIdentity.properties.principalId
    principalType: 'ServicePrincipal'
  }
}

There you are using the RBAC, but also, you can use the Access Policy:

resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = {
  name: keyVaultName
  location: location
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: subscription().tenantId
    accessPolicies: [] // Managed Identity will be assigned separately
  }
}

Which could require an implementation like:

  name: 'poc-aca-kv'
  location: resourceGroup().location
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: subscription().tenantId
    accessPolicies: [
      {
        tenantId: subscription().tenantId
        objectId: managedIdentity.properties.principalId // Assign permissions to the Managed Identity
        permissions: {
          secrets: [
            'get'
            'list'
            'read'
          ]
        }
      }
    ]
  }
}

Now, depending of your implementation (RBAC or access policies), now you can use it:

resource secretref 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' existing = {
  name: 'DatabaseConnectionString'
  parent: keyVault
}

resource secretref2 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' existing = {
  name: 'DatabaseConnectionString'
  parent: keyVault
}

And the container app as:

resource containerApp 'Microsoft.App/containerApps@2024-10-02-preview' = {
  dependsOn: [roleAssignment]
  name: containerAppName
  location: location
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${managedIdentity.id}': {}
    }
  }
  properties: {
    managedEnvironmentId: environment.id
    configuration: {
    secrets: [
      {
        name: 'apikey'
        keyVaultUrl: secretref.properties.secretUri
        identity: managedIdentity.id
      }
      {
        name: 'apikey'
        keyVaultUrl: secretref2.properties.secretUri
        identity: managedIdentity.id
      }
    ]      
  }

    template: {
      containers: [
        {
          name: containerAppName
          image: containerAppImage
          env: [
            {
              name: 'db'
              secretRef: 'db'
            }
            {
              name: 'apikey'
              secretRef: 'apikey'
            }
          ]
        }
      ]
      scale: {
        maxReplicas: 1
        minReplicas: 0
      }
    }
  }
}

Upvotes: 0

cheeta
cheeta

Reputation: 1

I had this error when experimenting in the Azure Portal, trying to create a secret with a Key Vault reference in a Container App. For me the solution for this error was to go back to the Key Vault. There I created a new Role Assignment: I assigned the role Key Vault Secrets User to the Managed Identy I created in the Container App (under tab 'Identity': turn on System assigned managed identity).

After that, I was able to add a new secret to the Container App with a Key Vault reference.

I did all of this in the Azure Portal, so I have no idea how to do this in a template, sorry.

Upvotes: 0

viktorh
viktorh

Reputation: 187

One alternative is to do a nested deployment with the principalID as output to the main/master template, but i guess then yes, you do need "two" templates

ex:

output  webAppMSI string = webApp.identity.principalId

or use bicep modules, here is a good sample:

https://blog.johnfolberth.com/chicken-and-the-egg-how-to-handle-bicep-interdependencies/

Upvotes: 0

Thomas
Thomas

Reputation: 29736

Using a user-assigned managed identity will solve your problem:

  1. Create a user-assigned managed identity.
  2. Grant KV secrets read permission to the identity.
  3. Create the container app.

Using Bicep, that would look like that:

key vault role assignment module:

// key-vault-role-assignment.bicep
param keyVaultName string
param principalId string
param principalType string = 'ServicePrincipal'
param roleIds array

resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = {
  name: keyVaultName
}

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for roleId in roleIds: {
  name: guid(subscription().subscriptionId, resourceGroup().name, keyVaultName, roleId, principalId)
  scope: keyVault
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleId)
    principalId: principalId
    principalType: principalType
  }
}]

and your main template:

// main.bicep
param location string = resourceGroup().location
param identityName string
param keyVaultName string
param containerAppEnvName string
param containerAppName string

// Create the identity
resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: identityName
  location: location
}

// Grant KV RBAC to the identity
module kvRbac 'key-vault-role-assignment.bicep' = {
 name: '${identityName}-${keyVaultName}-rbac'
 params: {
  keyVaultName: keyVaultName
  principalId: identity.properties.principalId
  roleIds: [ '4633458b-17de-408a-b874-0445c86b69e6' ] // Key vault secret user
 }
}

// Create the container app env
resource containerAppEnv 'Microsoft.App/managedEnvironments@2023-05-01' = {
  name: containerAppEnvName
  ...
}

// Create the container app and assigned the identity
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
  name: containerAppName
  location: location
  dependsOn: [
    kvRbac
  ]
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${identity.id}': {}
    }
  }
  properties: {
    environmentId: containerAppEnv.id
    workloadProfileName: 'Consumption'
    configuration: {
      ...
      secrets: [
        {
          name: 'applicationinsights-connection-string'
          keyVaultUrl: 'https://${keyVaultName}${environment().suffixes.keyvaultDns}/secrets/mysecret'
          identity: identity.id
        }
      ]
    }
    ...
  }
}

Az CLI / Az Powershell supports natively bicep but you could always generate the related ARM template using az bicep build:

az bicep build --file main.bicep

Upvotes: 2

Related Questions