From 90c28a3f3d00150258af26632ad68643914a17c7 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 12 Aug 2026 10:32:34 -0700 Subject: [PATCH] Add opt-in Key Vault RBAC authorization for CAF/landing zone compliance Adds an `enableRbacAuthorization` parameter (default false) that switches the remote hub Key Vault from access policies to Azure RBAC, satisfying CAF / Enterprise-Scale landing zone guardrails that require RBAC-authorized key vaults. When enabled, the Data Factory managed identity is granted an equivalent Key Vault Secrets User role assignment so secret access continues to work instead of silently breaking. Defaults to false to avoid an irreversible auth-model change on redeploys of existing hubs, matching the existing opt-in `enablePurgeProtection` parameter, which cannot be disabled once enabled either. Fixes #1067 --- docs-mslearn/toolkit/changelog.md | 1 + .../finops-hub/createUiDefinition.json | 10 +++++++- src/templates/finops-hub/main.bicep | 6 ++++- .../finops-hub/modules/fx/hub-app.bicep | 23 ++++++++++++++++--- .../finops-hub/modules/fx/hub-types.bicep | 6 +++++ src/templates/finops-hub/modules/hub.bicep | 6 ++++- 6 files changed, 46 insertions(+), 6 deletions(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 8849f945f..78f9ed241 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -29,6 +29,7 @@ The following section lists features and enhancements that are currently in deve - **Added** - Added VNet and private network modes, including opt-in NAT Gateway support for private mode; NAT Gateway incurs additional cost when enabled ([#2163](https://github.com/microsoft/finops-toolkit/pull/2163)). + - Added an opt-in `enableRbacAuthorization` parameter to switch the remote hub Key Vault from access policies to Azure RBAC, satisfying Cloud Adoption Framework / Enterprise-Scale landing zone guardrails that require RBAC-authorized key vaults; the Data Factory managed identity is granted an equivalent Key Vault Secrets User role assignment so secret access keeps working when enabled ([#1067](https://github.com/microsoft/finops-toolkit/issues/1067)). - **Changed** - Replaced redundant `tolower()` comparisons in hub KQL with case-insensitive operators (`has`, `=~`, `!~`) so the engine can use the term index instead of scanning every row ([#2213](https://github.com/microsoft/finops-toolkit/issues/2213)). - Replaced whole-term `contains` matches with `has` across hub KQL and the query catalog (resource ID paths, licensing phrases, SKU description terms) and added a per-row operator-equivalence regression harness with unit test coverage ([#2220](https://github.com/microsoft/finops-toolkit/pull/2220)). diff --git a/src/templates/finops-hub/createUiDefinition.json b/src/templates/finops-hub/createUiDefinition.json index d7ea79097..87f5bac0b 100644 --- a/src/templates/finops-hub/createUiDefinition.json +++ b/src/templates/finops-hub/createUiDefinition.json @@ -203,7 +203,14 @@ "name": "enablePurgeProtection", "type": "Microsoft.Common.CheckBox", "label": "Enable Key Vault purge protection", - "toolTip": "Enables purge protection for the Key Vault used to store the remote hub storage key. Purge protection prevents permanent deletion of the Key Vault for 90 days after deletion. Note: If the key is lost, you can regenerate it from the remote hub's storage account.", + "toolTip": "Enables purge protection for the Key Vault used to store the remote hub storage key. Purge protection prevents permanent deletion of the Key Vault for 90 days after deletion. Note: If the key is lost, you can regenerate it from the remote hub's storage account. This cannot be disabled once enabled.", + "visible": "[equals(basics('analyticsBackend').analyticsEngine, 'remote')]" + }, + { + "name": "enableRbacAuthorization", + "type": "Microsoft.Common.CheckBox", + "label": "Enable Key Vault RBAC authorization", + "toolTip": "Enables Azure RBAC instead of access policies to authorize access to the Key Vault used to store the remote hub storage key. Required by some organizations for policy compliance (e.g., Cloud Adoption Framework guardrails). Enable this if you are deploying to a subscription that enforces this requirement.", "visible": "[equals(basics('analyticsBackend').analyticsEngine, 'remote')]" } ], @@ -1013,6 +1020,7 @@ "remoteHubStorageUri": "[if(equals(basics('analyticsBackend').analyticsEngine, 'remote'), basics('analyticsBackend').remoteHubStorageUri, '')]", "remoteHubStorageKey": "[if(equals(basics('analyticsBackend').analyticsEngine, 'remote'), basics('analyticsBackend').remoteHubStorageKey, '')]", "enablePurgeProtection": "[if(equals(basics('analyticsBackend').analyticsEngine, 'remote'), coalesce(basics('analyticsBackend').enablePurgeProtection, false), false)]", + "enableRbacAuthorization": "[if(equals(basics('analyticsBackend').analyticsEngine, 'remote'), coalesce(basics('analyticsBackend').enableRbacAuthorization, false), false)]", "tagsByResource": "[steps('tags').tagsByResource]" } } diff --git a/src/templates/finops-hub/main.bicep b/src/templates/finops-hub/main.bicep index 687c26174..63b99526c 100644 --- a/src/templates/finops-hub/main.bicep +++ b/src/templates/finops-hub/main.bicep @@ -26,9 +26,12 @@ param storageSku string = 'Premium_LRS' @description('Optional. Enable infrastructure encryption on the storage account. Default = false.') param enableInfrastructureEncryption bool = false -@description('Optional. Enable purge protection for the Key Vault. Default: false.') +@description('Optional. Enable purge protection for the Key Vault. Once enabled on a vault, purge protection cannot be disabled. Default: false.') param enablePurgeProtection bool = false +@description('Optional. Enable Azure RBAC for authorizing access to the Key Vault instead of access policies. Required by some organizations for policy compliance (e.g., Cloud Adoption Framework guardrails). Switching an existing vault from access policies to RBAC has migration implications, so this defaults to false for backward compatibility with existing deployments; review before enabling on an upgrade. Default: false.') +param enableRbacAuthorization bool = false + @description('Optional. Storage account to push data to for ingestion into a remote hub.') param remoteHubStorageUri string = '' @@ -179,6 +182,7 @@ module hub 'modules/hub.bicep' = { storageSku: storageSku enableInfrastructureEncryption: enableInfrastructureEncryption enablePurgeProtection: enablePurgeProtection + enableRbacAuthorization: enableRbacAuthorization enableManagedExports: enableManagedExports enableRecommendations: enableRecommendations enableAHBRecommendations: enableAHBRecommendations diff --git a/src/templates/finops-hub/modules/fx/hub-app.bicep b/src/templates/finops-hub/modules/fx/hub-app.bicep index 6bdd0cf21..eac1abbec 100644 --- a/src/templates/finops-hub/modules/fx/hub-app.bicep +++ b/src/templates/finops-hub/modules/fx/hub-app.bicep @@ -83,8 +83,9 @@ var storageInfrastructureEncryptionProperties = !app.hub.options.storageInfrastr } } -// KeyVault access policies -var keyVaultAccessPolicies = [ +// KeyVault access policies -- only used when the vault uses the legacy access-policy auth model +// (RBAC-authorized vaults must have an empty accessPolicies array; Azure rejects a non-empty array otherwise) +var keyVaultAccessPolicies = app.hub.options.keyVaultEnableRbacAuthorization ? [] : [ { #disable-next-line BCP318 // Null safety warning for conditional resource access // Null safety warning for conditional resource access objectId: dataFactory.identity.principalId @@ -93,6 +94,10 @@ var keyVaultAccessPolicies = [ } ] +// Built-in role definition IDs used for Key Vault RBAC role assignments +// Key Vault Secrets User -- https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#key-vault-secrets-user +var keyVaultSecretsUserRoleId = '4633458b-17de-408a-b874-0445c86b69e6' + //============================================================================== // Resources @@ -484,7 +489,7 @@ resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = if (usesKeyVault) { softDeleteRetentionInDays: 90 // Use null instead of false when purge protection is disabled - Azure requires null to indicate the property should not be set enablePurgeProtection: app.hub.options.keyVaultEnablePurgeProtection ? true : null - enableRbacAuthorization: false + enableRbacAuthorization: app.hub.options.keyVaultEnableRbacAuthorization createMode: 'default' tenantId: subscription().tenantId accessPolicies: keyVaultAccessPolicies @@ -495,6 +500,18 @@ resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = if (usesKeyVault) { } } +// Grant ADF identity RBAC access to read secrets when the vault uses RBAC instead of access policies +resource keyVaultRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (usesKeyVault && usesDataFactory && app.hub.options.keyVaultEnableRbacAuthorization) { + name: guid(keyVault.id, keyVaultSecretsUserRoleId, dataFactory.id) + scope: keyVault + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', keyVaultSecretsUserRoleId) + #disable-next-line BCP318 // Null safety warning for conditional resource access + principalId: dataFactory.identity.principalId + principalType: 'ServicePrincipal' + } +} + resource keyVaultPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = if (usesKeyVault && app.hub.options.privateRouting) { name: 'privatelink${replace(environment().suffixes.keyvaultDns, 'vault', 'vaultcore')}' // cSpell:ignore privatelink, vaultcore location: 'global' diff --git a/src/templates/finops-hub/modules/fx/hub-types.bicep b/src/templates/finops-hub/modules/fx/hub-types.bicep index d9b197e75..bac869071 100644 --- a/src/templates/finops-hub/modules/fx/hub-types.bicep +++ b/src/templates/finops-hub/modules/fx/hub-types.bicep @@ -71,6 +71,7 @@ type HubRoutingProperties = { enableTelemetry: 'Indicates whether telemetry should be enabled for deployments.' keyVaultSku: 'KeyVault SKU. Allowed values: "standard", "premium".' keyVaultEnablePurgeProtection: 'Indicates whether purge protection is enabled for the Key Vault. When enabled, deleted Key Vault and its secrets cannot be permanently deleted until the retention period expires, which is required for compliance in some environments.' + keyVaultEnableRbacAuthorization: 'Indicates whether the Key Vault uses Azure RBAC instead of access policies to authorize access to secrets. When enabled, access policies are ignored and callers need an RBAC role assignment (e.g., Key Vault Secrets User) on the vault, which is required for compliance in some environments.' networkAddressPrefix: 'Address prefix for the FinOps hub isolated virtual network, if private network routing is enabled.' natGateway: 'Indicates whether a NAT Gateway should be deployed for controlled outbound internet access. When enabled, subnets disable Azure default outbound access and route through the NAT Gateway.' privateRouting: 'Indicates whether private network routing is enabled.' @@ -96,6 +97,7 @@ type HubProperties = { enableTelemetry: bool keyVaultSku: string keyVaultEnablePurgeProtection: bool + keyVaultEnableRbacAuthorization: bool networkAddressPrefix: string natGateway: bool privateRouting: bool @@ -192,6 +194,7 @@ func newHubInternal( storageSku string, keyVaultSku string, keyVaultEnablePurgeProtection bool, + keyVaultEnableRbacAuthorization bool, enableInfrastructureEncryption bool, enablePublicAccess bool, enableNatGateway bool, @@ -213,6 +216,7 @@ func newHubInternal( enableTelemetry: isTelemetryEnabled ?? true keyVaultSku: keyVaultSku keyVaultEnablePurgeProtection: keyVaultEnablePurgeProtection + keyVaultEnableRbacAuthorization: keyVaultEnableRbacAuthorization networkAddressPrefix: networkAddressPrefix natGateway: !enablePublicAccess && enableNatGateway privateRouting: !enablePublicAccess @@ -253,6 +257,7 @@ func newHub( storageSku string, keyVaultSku string, keyVaultEnablePurgeProtection bool, + keyVaultEnableRbacAuthorization bool, enableInfrastructureEncryption bool, enablePublicAccess bool, enableNatGateway bool, @@ -268,6 +273,7 @@ func newHub( storageSku, keyVaultSku, keyVaultEnablePurgeProtection, + keyVaultEnableRbacAuthorization, enableInfrastructureEncryption, enablePublicAccess, enableNatGateway, diff --git a/src/templates/finops-hub/modules/hub.bicep b/src/templates/finops-hub/modules/hub.bicep index 0c9e6b9e5..4b267eea7 100644 --- a/src/templates/finops-hub/modules/hub.bicep +++ b/src/templates/finops-hub/modules/hub.bicep @@ -34,9 +34,12 @@ param enableInfrastructureEncryption bool = false ]) param keyVaultSku string = 'premium' -@description('Optional. Enable purge protection for the Key Vault. Default: false.') +@description('Optional. Enable purge protection for the Key Vault. Once enabled on a vault, purge protection cannot be disabled. Default: false.') param enablePurgeProtection bool = false +@description('Optional. Enable Azure RBAC for authorizing access to the Key Vault instead of access policies. Required by some organizations for policy compliance (e.g., Cloud Adoption Framework guardrails). Switching an existing vault from access policies to RBAC has migration implications, so this defaults to false for backward compatibility with existing deployments; review before enabling on an upgrade. Default: false.') +param enableRbacAuthorization bool = false + @description('Optional. Remote storage account for ingestion dataset.') param remoteHubStorageUri string = '' @@ -195,6 +198,7 @@ var hub = newHub( storageSku, keyVaultSku, enablePurgeProtection, + enableRbacAuthorization, enableInfrastructureEncryption, enablePublicAccess, enableNatGateway,