A KeyVault for the Power Platform
sandr0-p
Posted on November 25, 2024
My friend and Microsoft MVP David Wyatt recently asked me to provide him with an Azure Key Vault for his Power Apps. "Easy", I thought, but everyone who ever worked with Microsoft products knows that "easy" is a relative term.
We started by creating a standalone Key Vault. I added David and the Power Platform SPN with Secret Get/List
permissions, and we gave it a go. It will not surprise you that it didn't work (otherwise I wouldn't write, right?!). The Key Vault Firewall rejected the traffic.
Fair, but thanks to our Company policies, we couldn't just allow public traffic. Which admittedly makes sense if you want to keep your secrets safe 🤷♂️
In the next step, we added a couple of Power Platform IP addresses to the Key Vault Firewall. Unfortunately, it had the same outcome. The firewall rejected the traffic. It was at this moment we knew this was getting out of hand. It became actual work.
As it turns out, we needed an entire Virtual Network with a Subnet and Network Security Group. If you ask me, this is too much work, but David insisted... and this is what it looks like:
As just mentioned, we need four resources. A Key Vault (kv
), a Virtual Network (vnet
), a Subnet (subnet
) and a Network Security Group (nsg
). To make matters worse, they have dependencies all over the place. If you are, like me, not a network specialist, this is a bit confusing. But here is what it looks like.
One of the resources without any dependencies is the nsg
, which makes it a prime starting point.
resource nsg 'Microsoft.Network/networkSecurityGroups@2024-03-01' = {
location: 'eastus2'
name: 'nsg'
properties: {
securityRules: [
{
name: 'AllowPP'
properties: {
access: 'Allow'
destinationAddressPrefix: 'VirtualNetwork'
destinationPortRange: '*'
direction: 'Inbound'
priority: 100
protocol: '*'
sourceAddressPrefix: 'PowerPlatformInfra'
sourcePortRange: '*'
}
}
]
}
}
The main focus here is on the sourceAddressPrefix
and sourceAddressPrefix
. These indicate from where to where we want to allow traffic. Usually, we would enter a list of IP addresses here, but Microsoft was so kind to provide us with Virtual Network Service Tags, which we can use instead of IP addresses.
Next up, the vnet
. This is the second resource without a dependency.
resource vnet 'Microsoft.Network/virtualNetworks@2024-03-01' = {
name: 'vnet'
location: 'eastus2'
properties: {
addressSpace: {
addressPrefixes: [
'10.0.0.0/16'
]
}
}
}
You can choose your addressPrefixes
pretty much at random if this is an isolated network. If you intend to connect this network to another, make sure that you use a compatible IP range. The IP range in the example is a CIDR notation and means that we can use the 65,536 IP addresses between 10.0.0.0
and 10.0.255.255
.
Let's continue with the subnet
, which depends on the vnet
and nsg
which we have just defined.
resource vnet 'Microsoft.Network/virtualNetworks@2024-03-01' existing = {
name: 'vnet'
}
resource nsg 'Microsoft.Network/networkSecurityGroups@2024-03-01' existing = {
name: 'nsg'
}
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2024-03-01' = {
name: 'default'
parent: vnet
properties: {
addressPrefix: '10.0.1.0/24'
networkSecurityGroup: {
id: nsg.id
}
serviceEndpoints: [
{
service: 'Microsoft.KeyVault'
}
]
}
}
We use the existing
keyword to access the vnet
and nsg
, which should already exist when the subnet
is being deployed. The vnet
is used as the parent
and the nsg.id
for the networkSecurityGroup
. We must also decide which IPs available on the vnet
should be assigned to this subnet
. In the example, we assigned all IPs from 10.0.1.0
to 10.0.1.255
. And very importantly, we need to make Microsoft.KeyVault
available in the serviceEndpoints
. Otherwise, the traffic will be blocked once again.
💡 To be safe, ensure your related resources use the same API version. In the above, we have three
Microsoft.Network
resources using API version2024-03-01
. More often than not, mixed versions will work, but sometimes you end up with bizarre behaviour. Using the same API version will increase the chance that everything works.
And last but not least, we define the resource that has kicked off this odyssey, the Key Vault.
resource vnet 'Microsoft.Network/virtualNetworks@2024-03-01' existing = {
name: 'vnet'
}
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2024-03-01' existing = {
name: 'default'
parent: vnet
}
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = {
name: 'kv'
location: 'eastus2'
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: subscription().tenantId
networkAcls: {
bypass: 'AzureServices'
defaultAction: 'Deny'
virtualNetworkRules: [
{
id: subnet.id
}
]
}
enabledForTemplateDeployment: true
accessPolicies: [
{
tenantId: subscription().tenantId
objectId: '00000000-0000-0000-0000-000000000000'
permissions: {
certificates: []
keys: []
secrets: [
'get'
'list'
]
storage: []
}
}
]
}
}
Once again, we use the existing
keyword to access the already deployed vent
and subnet
. The points of interest here are the networkAcls
and accessPolicies
.
The defaultAction: Deny
property of networkAcls
prohibits all traffic by default. With bypass: AzureServices
we overrides= the defaultAction
and allow Azure-hosted services, such as a Web App, direct access to the kv
. The same is true for all IPs and rules defined in the subnet and
nsg, which we assign to the
virtualNetworkRules.
accessPolicies
Inwe which identities (Users, Applications, Resources
can perform which actions against Key Vault items. In the example, we grant get/list
access to the given objectId
. Where list
allows to fetch a list of the name of all stored secrets and get
allows to read the secret value.
To wrap things up, let's write the pipeline we can run in Azure DevOps.
trigger:
- none
parameters:
- name: TargetEnvironment
type: string
values:
- 'your-azure-resource-group'
- name: kv01
displayName: "Deploy Key Vault"
type: boolean
default: false
- name: vnet01
displayName: "Deploy Virtual Network"
type: boolean
default: false
- name: nsg01
displayName: "Deploy Network Security Group"
type: boolean
default: false
- name: subnet01
displayName: "Deploy Subnet"
type: boolean
default: false
variables:
- name: serviceConnection
value: "your-azure-service-connection"
stages:
- stage: 'Infrastructure'
displayName: 'Infrastructure'
pool:
vmImage: ubuntu-latest
# Deploy kv01
- job: kv01
displayName: 'Create Key Vault'
condition: eq('${{ parameters.kv01 }}', true)
dependsOn:
- vnet01
- subnet01
steps:
- task: AzureCLI@2
inputs:
azureSubscription: $(serviceConnection)
scriptType: "bash"
scriptLocation: "inlineScript"
inlineScript: |
az deployment group create \
--resource-group ${{parameters.TargetEnvironment}} \
--template-file $(Build.SourcesDirectory)/KeyVault.bicep
# Deploy vnet01
- job: vnet01
displayName: 'Create Virtual Network'
condition: eq('${{ parameters.vnet01 }}', true)
steps:
- task: AzureCLI@2
inputs:
azureSubscription: $(serviceConnection)
scriptType: "bash"
scriptLocation: "inlineScript"
inlineScript: |
az deployment group create \
--resource-group ${{parameters.TargetEnvironment}} \
--template-file $(Build.SourcesDirectory)/VirtualNetwork.bicep
# Deploy nsg01
- job: nsg01
displayName: 'Create Network Security Group'
condition: eq('${{ parameters.nsg01 }}', true)
steps:
- task: AzureCLI@2
inputs:
azureSubscription: $(serviceConnection)
scriptType: "bash"
scriptLocation: "inlineScript"
inlineScript: |
az deployment group create \
--resource-group ${{parameters.TargetEnvironment}} \
--template-file $(Build.SourcesDirectory)/NetworkSecurityGroup.bicep
# Deploy subnet01
- job: subnet01
displayName: 'Create Subnet'
condition: eq('${{ parameters.subnet01 }}', true)
dependsOn:
- vnet01
- nsg01
steps:
- task: AzureCLI@2
inputs:
azureSubscription: $(serviceConnection)
scriptType: "bash"
scriptLocation: "inlineScript"
inlineScript: |
az deployment group create \
--resource-group ${{parameters.TargetEnvironment}} \
--template-file $(Build.SourcesDirectory)/Subnet.bicep
With trigger: -none
, we indicate that we want to execute the pipeline manually and not, for example, as part of a PR or merge. The following block allows to select which resources should be deployed on a run and to which resource group.
For the deployment of the resources, we have four identical blocks, one for each resource. With condition: eq('${{ parameters.kv01 }}', true)
, we instruct Azure to execute the job only if we have selected it. Otherwise, skip it.
dependsOn: - vnet01 - subnet01
defines our dependicies. Azure will attempt to execute the jobs in a way that all dependencies are finished before the job runs.
So yeah, that was our attempt to "quickly" spin up a Key Vault for the Power Platform. I hope this helps when your friend asks you for help.
If you are interested in Microsoft PowerPlatform, I highly recommend to check out David's Blog and Power DevBox.
Resources
Posted on November 25, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.