Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
60c147e
docs: prepare v15 release documentation
flanakin Aug 26, 2026
843a102
docs: add DeploymentScriptACIProvisioningTimeout mitigation
flanakin Aug 26, 2026
44293c2
chore: bump version to 15.0.0
flanakin Sep 2, 2026
327eafb
docs: finalize v15 changelog, what's new blurbs, and hubs docs
flanakin Sep 7, 2026
38a9bbe
Fix PathNotFound error in msexports ETL ingestion on first run (#2247)
flanakin Sep 8, 2026
deb6cb0
Merge origin/dev into flanakin/v15-prep and address notes
flanakin Oct 12, 2026
6e33ed6
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 12, 2026
42484fc
docs: update v15 changelog and what's new blurbs for October
flanakin Oct 14, 2026
aeb0b11
chore: keep marketplace.json versions three-part during version updates
flanakin Oct 14, 2026
4a3c5a6
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 14, 2026
07a5f23
Merge origin/dev into flanakin/v15-prep
flanakin Oct 14, 2026
5a9b60d
fix(power-bi): repoint rate optimization data quality link
flanakin Oct 15, 2026
cfafe27
Merge origin/dev into flanakin/v15-prep
flanakin Oct 15, 2026
5e2fcd6
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 15, 2026
edff142
chore: trigger CI after ms.date bot commit
flanakin Oct 16, 2026
38a071e
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 16, 2026
f1cd3fb
Use semver for plugin.json and add #2247 changelog entry
flanakin Oct 19, 2026
38e6957
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 19, 2026
417790e
Address review: drop percent() code block, simplify eligibility note
flanakin Oct 19, 2026
a172db5
docs: fill v15 changelog gaps (stacked on v15-prep) (#2365)
flanakin Oct 23, 2026
27d1755
Merge dev into v15-prep
flanakin Oct 23, 2026
4f7562a
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 23, 2026
2acd385
Merge remote-tracking branch 'origin/dev' into flanakin/v15-prep
flanakin Oct 23, 2026
2108db2
Merge origin/dev into flanakin/v15-prep
flanakin Oct 26, 2026
bd06cf2
docs: update what's new blurbs for latest v15 changes
flanakin Oct 26, 2026
2175275
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 26, 2026
3e60e4b
docs: add changelog entry for upgrade and data model docs
flanakin Oct 26, 2026
0602e65
Merge origin/dev into flanakin/v15-prep
flanakin Oct 26, 2026
ef9ef58
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 26, 2026
c8a0522
docs: Fix broken ecosystem link in roadmap
flanakin Oct 26, 2026
ecc8226
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 26, 2026
b79a7ef
docs: clean up the v15 changelog and roadmap
flanakin Oct 26, 2026
a460235
docs: address review comments on the roadmap and changelog
flanakin Oct 26, 2026
fa19bd1
docs: restore FinOps toolkit name in changelog entry
flanakin Oct 26, 2026
4a074e7
Add opt-in Key Vault RBAC authorization for CAF/landing zone compliance
flanakin Aug 12, 2026
a912078
Rename enableRbacAuthorization param to enableKeyVaultRbacAuthorization
flanakin Aug 26, 2026
66f1fda
Add EnableKeyVaultRbacAuthorization switch to Deploy-FinOpsHub and to…
flanakin Oct 7, 2026
5e8556b
fix(hubs): forward enableKeyVaultRbacAuthorization only when set; cla…
flanakin Oct 16, 2026
2f96200
Align RBAC help text and reject Key Vault RBAC on pre-15.0 versions
flanakin Oct 19, 2026
e6724e2
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 27, 2026
ca0e272
chore: release prep for v15
flanakin Oct 27, 2026
066cf06
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 27, 2026
828e4f7
fix(powershell): Use normalized template version for Key Vault RBAC gate
flanakin Oct 27, 2026
f905a22
fix(ci): Register PSGallery before installing Pester in Open Data CI
flanakin Oct 27, 2026
a05990a
chore: add v15 deployment files from packaging
flanakin Oct 28, 2026
ec9862d
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 28, 2026
0f3d520
docs: package Power BI from the prep branch and draft the release early
flanakin Oct 28, 2026
f1b26ec
Merge flanakin/v15-prep into flanakin/1067-keyvault-rbac
flanakin Oct 28, 2026
d31f9c3
Merge origin/dev into flanakin/1067-keyvault-rbac
flanakin Oct 29, 2026
008e918
chore: Update ms.date in docs-mslearn files
github-actions[bot] Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs-mslearn/toolkit/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: FinOps toolkit changelog
description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more.
author: MSBrett
ms.author: brettwil
ms.date: 10/28/2026
ms.date: 10/29/2026
ms.topic: reference
ms.service: finops
ms.subservice: finops-toolkit
Expand Down Expand Up @@ -62,6 +62,7 @@ _Released October 2026_
- Generated at build time from the open data CSV files, so the local hub stays in sync with published open data.
- Added VNet and private network modes, including opt-in NAT Gateway support for private mode ([#2161](https://github.com/microsoft/finops-toolkit/issues/2161)).
- NAT Gateway adds cost when enabled.
- Added an opt-in `enableKeyVaultRbacAuthorization` 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)).
Comment thread
flanakin marked this conversation as resolved.
Resolved
- **Changed**
- Updated the hub database guide to explain how columns are organized and corrected the `Costs()` column reference to match the deployed schema ([#2325](https://github.com/microsoft/finops-toolkit/issues/2325), [#2329](https://github.com/microsoft/finops-toolkit/issues/2329)).
- Documents the organization chain and the two parallel service chains.
Expand Down
18 changes: 18 additions & 0 deletions src/powershell/Public/Deploy-FinOpsHub.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
.PARAMETER EnablePurgeProtection
Optional. Enable purge protection for the Key Vault. Default = false.

.PARAMETER EnableKeyVaultRbacAuthorization
Optional. Switch the Key Vault from access policies to Azure RBAC authorization. Required for CAF/Enterprise-Scale landing zone compliance. Switching an existing vault from access policies to RBAC removes all access policies immediately; any access granted outside the template must be recreated as role assignments. Default = false.

.PARAMETER RemoteHubStorageUri
Optional. Storage account to push data to for ingestion into a remote hub.

Expand Down Expand Up @@ -145,6 +148,10 @@ function Deploy-FinOpsHub
[switch]
$EnablePurgeProtection,

[Parameter()]
[switch]
$EnableKeyVaultRbacAuthorization,

[Parameter()]
[string]
$RemoteHubStorageUri,
Expand Down Expand Up @@ -274,6 +281,12 @@ function Deploy-FinOpsHub
throw "NAT Gateway / private network mode requires hub template version 15.0 or later. Current version: $Version"
}

# Key Vault RBAC authorization requires hub template version 15.0 or later
if ($EnableKeyVaultRbacAuthorization -and $Version -ne 'latest' -and $templateVersion -lt '15.0')
Comment thread
flanakin marked this conversation as resolved.
Resolved
{
throw "-EnableKeyVaultRbacAuthorization requires hub template version 15.0 or later. Current version: $Version"
}

# Download template
if (Test-ShouldProcess $PSCmdlet $Version 'DownloadTemplate')
{
Expand Down Expand Up @@ -343,6 +356,11 @@ function Deploy-FinOpsHub
$parameterSplat.TemplateParameterObject.Add('enablePurgeProtection', $EnablePurgeProtection.IsPresent)
}

if ($EnableKeyVaultRbacAuthorization -and ($Version -eq 'latest' -or $templateVersion -ge '15.0'))
Comment thread
flanakin marked this conversation as resolved.
Resolved
{
$parameterSplat.TemplateParameterObject.Add('enableKeyVaultRbacAuthorization', $true)
}

# Only pass enableNatGateway when private mode is requested. This keeps public/vnet
# deployments compatible with template versions that predate the parameter.
if ($effectiveNetworkMode -eq 'private' -and ($Version -eq 'latest' -or $templateVersion -ge '15.0'))
Expand Down
28 changes: 28 additions & 0 deletions src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,34 @@ InModuleScope 'FinOpsToolkit' {
$TemplateParameterObject.enableNatGateway -eq $true
} -Times 1
}

It 'Should pass enableKeyVaultRbacAuthorization when the switch is set on version <Version>' -TestCases @(
Comment thread
flanakin marked this conversation as resolved.
Resolved
@{ Version = '15.0' }
@{ Version = '15' }
@{ Version = 'v15' }
) {
param($Version)
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -EnableKeyVaultRbacAuthorization -Version $Version } | Should -Not -Throw
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
$TemplateParameterObject.enableKeyVaultRbacAuthorization -eq $true
} -Times 1
}

It 'Should throw a friendly error when -EnableKeyVaultRbacAuthorization targets version <Version>' -TestCases @(
Comment thread
flanakin marked this conversation as resolved.
Resolved
@{ Version = '14.0' }
@{ Version = '14' }
) {
param($Version)
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -EnableKeyVaultRbacAuthorization -Version $Version } | Should -Throw -ExpectedMessage '*15*'
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -Times 0
}

It 'Should not pass enableKeyVaultRbacAuthorization when the switch is not set' {
{ Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -Version 'latest' } | Should -Not -Throw
Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter {
-not $TemplateParameterObject.ContainsKey('enableKeyVaultRbacAuthorization')
} -Times 1
}
}

Context 'Version parsing' {
Expand Down
10 changes: 9 additions & 1 deletion src/templates/finops-hub/createUiDefinition.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "enableKeyVaultRbacAuthorization",
"type": "Microsoft.Common.CheckBox",
"label": "Enable Key Vault RBAC authorization",
Comment thread
RolandKrummenacher marked this conversation as resolved.
Resolved
"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. Switching an existing vault to RBAC removes all access policies immediately; any access granted outside this template must be recreated as role assignments.",
"visible": "[equals(basics('analyticsBackend').analyticsEngine, 'remote')]"
}
],
Expand Down Expand Up @@ -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)]",
"enableKeyVaultRbacAuthorization": "[if(equals(basics('analyticsBackend').analyticsEngine, 'remote'), coalesce(basics('analyticsBackend').enableKeyVaultRbacAuthorization, false), false)]",
"tagsByResource": "[steps('tags').tagsByResource]"
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/templates/finops-hub/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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 enableKeyVaultRbacAuthorization bool = false
Comment thread
flanakin marked this conversation as resolved.
Resolved

@description('Optional. Storage account to push data to for ingestion into a remote hub.')
param remoteHubStorageUri string = ''

Expand Down Expand Up @@ -179,6 +182,7 @@ module hub 'modules/hub.bicep' = {
storageSku: storageSku
enableInfrastructureEncryption: enableInfrastructureEncryption
enablePurgeProtection: enablePurgeProtection
enableKeyVaultRbacAuthorization: enableKeyVaultRbacAuthorization
enableManagedExports: enableManagedExports
enableRecommendations: enableRecommendations
enableAHBRecommendations: enableAHBRecommendations
Expand Down
23 changes: 20 additions & 3 deletions src/templates/finops-hub/modules/fx/hub-app.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,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
Expand All @@ -94,6 +95,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
Expand Down Expand Up @@ -486,7 +491,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
Expand All @@ -497,6 +502,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) {
Comment thread
flanakin marked this conversation as resolved.
Resolved
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'
Expand Down
6 changes: 6 additions & 0 deletions src/templates/finops-hub/modules/fx/hub-types.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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, the script and Data Explorer subnets route outbound traffic through the NAT Gateway.'
privateRouting: 'Indicates whether private network routing is enabled.'
Expand All @@ -98,6 +99,7 @@ type HubProperties = {
enableTelemetry: bool
keyVaultSku: string
keyVaultEnablePurgeProtection: bool
keyVaultEnableRbacAuthorization: bool
networkAddressPrefix: string
natGateway: bool
privateRouting: bool
Expand Down Expand Up @@ -194,6 +196,7 @@ func newHubInternal(
storageSku string,
keyVaultSku string,
keyVaultEnablePurgeProtection bool,
keyVaultEnableRbacAuthorization bool,
enableInfrastructureEncryption bool,
enablePublicAccess bool,
enableNatGateway bool,
Expand All @@ -215,6 +218,7 @@ func newHubInternal(
enableTelemetry: isTelemetryEnabled ?? true
keyVaultSku: keyVaultSku
keyVaultEnablePurgeProtection: keyVaultEnablePurgeProtection
keyVaultEnableRbacAuthorization: keyVaultEnableRbacAuthorization
networkAddressPrefix: networkAddressPrefix
natGateway: !enablePublicAccess && enableNatGateway
privateRouting: !enablePublicAccess
Expand Down Expand Up @@ -256,6 +260,7 @@ func newHub(
storageSku string,
keyVaultSku string,
keyVaultEnablePurgeProtection bool,
keyVaultEnableRbacAuthorization bool,
Comment thread
flanakin marked this conversation as resolved.
Resolved
enableInfrastructureEncryption bool,
enablePublicAccess bool,
enableNatGateway bool,
Expand All @@ -271,6 +276,7 @@ func newHub(
storageSku,
keyVaultSku,
keyVaultEnablePurgeProtection,
keyVaultEnableRbacAuthorization,
enableInfrastructureEncryption,
enablePublicAccess,
enableNatGateway,
Expand Down
6 changes: 5 additions & 1 deletion src/templates/finops-hub/modules/hub.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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 enableKeyVaultRbacAuthorization bool = false

@description('Optional. Remote storage account for ingestion dataset.')
param remoteHubStorageUri string = ''

Expand Down Expand Up @@ -195,6 +198,7 @@ var hub = newHub(
storageSku,
keyVaultSku,
enablePurgeProtection,
enableKeyVaultRbacAuthorization,
enableInfrastructureEncryption,
enablePublicAccess,
enableNatGateway,
Expand Down
Loading