diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index d1f201360..a6a4b3af8 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -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 @@ -18,14 +18,17 @@ ms.reviewer: brettwil This article summarizes the features and enhancements in each release of the FinOps toolkit. - +### [FinOps hubs](hubs/finops-hubs-overview.md) + +- **Added** + - Added an opt-in `disableStorageSharedKeyAccess` parameter (and matching `-DisableStorageSharedKeyAccess` switch in [Deploy-FinOpsHub](powershell/hubs/deploy-finopshub.md)) to disable Shared Key (storage account key) authentication on hub storage accounts. Data Factory already authenticates using managed identity and RBAC, so enabling this does not affect data ingestion. Default: false, to avoid changing the authentication model on existing deployments ([#2312](https://github.com/microsoft/finops-toolkit/issues/2312)). +
diff --git a/src/powershell/Public/Deploy-FinOpsHub.ps1 b/src/powershell/Public/Deploy-FinOpsHub.ps1 index e48f29555..46e40494d 100644 --- a/src/powershell/Public/Deploy-FinOpsHub.ps1 +++ b/src/powershell/Public/Deploy-FinOpsHub.ps1 @@ -34,6 +34,9 @@ .PARAMETER EnablePurgeProtection Optional. Enable purge protection for the Key Vault. Default = false. + .PARAMETER DisableStorageSharedKeyAccess + Optional. Disable Shared Key (storage account key) authentication on hub storage accounts. When enabled, only Microsoft Entra ID and managed identity authentication are allowed. Data Factory already authenticates using managed identity and RBAC, so this does not affect data ingestion. Do not enable this on a hub that receives data from other hubs (remote hub ingestion authenticates with the storage account key). The private-mode deployment script storage account always allows Shared Key access because deployment scripts require it. Default = false. + .PARAMETER RemoteHubStorageUri Optional. Storage account to push data to for ingestion into a remote hub. @@ -145,6 +148,10 @@ function Deploy-FinOpsHub [switch] $EnablePurgeProtection, + [Parameter()] + [switch] + $DisableStorageSharedKeyAccess, + [Parameter()] [string] $RemoteHubStorageUri, @@ -343,6 +350,11 @@ function Deploy-FinOpsHub $parameterSplat.TemplateParameterObject.Add('enablePurgeProtection', $EnablePurgeProtection.IsPresent) } + if ($Version -eq 'latest' -or [version]$Version -ge '16.0') + { + $parameterSplat.TemplateParameterObject.Add('disableStorageSharedKeyAccess', $DisableStorageSharedKeyAccess.IsPresent) + } + # 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')) diff --git a/src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 b/src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 index f73214a40..c9a9c24c4 100644 --- a/src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 +++ b/src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 @@ -435,5 +435,38 @@ InModuleScope 'FinOpsToolkit' { } -Times 1 } } + + Context 'Storage shared key access' { + BeforeAll { + Mock -CommandName 'Get-AzResourceGroup' -MockWith { return @{ ResourceGroupName = $rgName } } + Mock -CommandName 'New-AzResourceGroup' + Mock -CommandName 'Save-FinOpsHubTemplate' + Mock -CommandName 'Initialize-FinOpsHubDeployment' + $templateFile = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'FinOps/finops-hub-v1.0.0/main.bicep' + Mock -CommandName 'Get-ChildItem' -MockWith { return @{ FullName = $templateFile } } + Mock -CommandName 'New-AzResourceGroupDeployment' + } + + It 'Should default to allowing Shared Key access' { + { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -Version 'latest' } | Should -Not -Throw + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + $TemplateParameterObject.disableStorageSharedKeyAccess -eq $false + } -Times 1 + } + + It 'Should pass disableStorageSharedKeyAccess when -DisableStorageSharedKeyAccess is specified' { + { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -DisableStorageSharedKeyAccess -Version 'latest' } | Should -Not -Throw + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + $TemplateParameterObject.disableStorageSharedKeyAccess -eq $true + } -Times 1 + } + + It 'Should not pass disableStorageSharedKeyAccess when targeting a version older than 16.0' { + { Deploy-FinOpsHub -Name $hubName -ResourceGroup $rgName -Location $location -DisableStorageSharedKeyAccess -Version '15.0' } | Should -Not -Throw + Should -Invoke -CommandName 'New-AzResourceGroupDeployment' -ParameterFilter { + -not $TemplateParameterObject.ContainsKey('disableStorageSharedKeyAccess') + } -Times 1 + } + } } } \ No newline at end of file diff --git a/src/templates/finops-hub/createUiDefinition.json b/src/templates/finops-hub/createUiDefinition.json index 77ba01053..d5c8d4e65 100644 --- a/src/templates/finops-hub/createUiDefinition.json +++ b/src/templates/finops-hub/createUiDefinition.json @@ -878,6 +878,28 @@ ], "visible": true }, + { + "name": "storageSecurity", + "type": "Microsoft.Common.Section", + "label": "Storage account security", + "elements": [ + { + "name": "disableStorageSharedKeyAccessIntro", + "type": "Microsoft.Common.TextBlock", + "visible": true, + "options": { + "text": "Data Factory always connects to hub storage accounts using its managed identity, so disabling Shared Key access does not affect data ingestion. Do not enable this on a hub that receives data from other hubs. This can be changed after deployment." + } + }, + { + "name": "disableStorageSharedKeyAccess", + "type": "Microsoft.Common.CheckBox", + "label": "Disable Shared Key access", + "toolTip": "Disables Shared Key (storage account key) authentication on hub storage accounts, requiring Microsoft Entra ID or managed identity authentication instead. Recommended for compliance requirements that prohibit key-based storage access." + } + ], + "visible": true + }, { "name": "managedExports", "type": "Microsoft.Common.Section", @@ -998,6 +1020,7 @@ "fabricQueryUri": "[if(equals(basics('analyticsBackend').analyticsEngine, 'fabric'), basics('analyticsBackend').fabricQueryUri, '')]", "fabricCapacityUnits": "[if(equals(basics('analyticsBackend').analyticsEngine, 'fabric'), basics('analyticsBackend').fabricCapacity, '')]", "enableInfrastructureEncryption": "[steps('advanced').storage.enableInfrastructureEncryption]", + "disableStorageSharedKeyAccess": "[steps('advanced').storageSecurity.disableStorageSharedKeyAccess]", "enableManagedExports": "[steps('advanced').managedExports.enableManagedExports]", "enableRecommendations": "[steps('recommendations').enableRecommendations]", "enableAHBRecommendations": "[steps('recommendations').optional.enableAHBRecommendations]", diff --git a/src/templates/finops-hub/main.bicep b/src/templates/finops-hub/main.bicep index 03be52372..cf88e72b9 100644 --- a/src/templates/finops-hub/main.bicep +++ b/src/templates/finops-hub/main.bicep @@ -26,6 +26,9 @@ param storageSku string = 'Premium_LRS' @description('Optional. Enable infrastructure encryption on the storage account. Default = false.') param enableInfrastructureEncryption bool = false +@description('Optional. Disable Shared Key (storage account key) authentication on hub storage accounts. When enabled, only Microsoft Entra ID and managed identity authentication are allowed. Data Factory already authenticates using managed identity and RBAC, so this does not affect data ingestion. Do not enable this on a hub that receives data from other hubs (remote hub ingestion authenticates with the storage account key). The private-mode deployment script storage account always allows Shared Key access because deployment scripts require it. Default = false.') +param disableStorageSharedKeyAccess bool = false + @description('Optional. Enable purge protection for the Key Vault. Default: false.') param enablePurgeProtection bool = false @@ -178,6 +181,7 @@ module hub 'modules/hub.bicep' = { // eventGridLocation: eventGridLocation storageSku: storageSku enableInfrastructureEncryption: enableInfrastructureEncryption + disableStorageSharedKeyAccess: disableStorageSharedKeyAccess enablePurgeProtection: enablePurgeProtection enableManagedExports: enableManagedExports enableRecommendations: enableRecommendations diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep index 4c9934b94..12ec98118 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep @@ -378,7 +378,7 @@ resource scriptStorageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = i tags: getHubTags(hub, 'Microsoft.Storage/storageAccounts') properties: { supportsHttpsTrafficOnly: true - allowSharedKeyAccess: true + allowSharedKeyAccess: true // Required: deployment scripts mount this storage account in Azure Container Instances using the account key. See https://learn.microsoft.com/azure/azure-resource-manager/bicep/deployment-script-develop#use-an-existing-storage-account isHnsEnabled: false minimumTlsVersion: 'TLS1_2' allowBlobPublicAccess: false diff --git a/src/templates/finops-hub/modules/fx/hub-app.bicep b/src/templates/finops-hub/modules/fx/hub-app.bicep index b176c05a5..d7e3638c4 100644 --- a/src/templates/finops-hub/modules/fx/hub-app.bicep +++ b/src/templates/finops-hub/modules/fx/hub-app.bicep @@ -372,7 +372,7 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = if (use properties: { ...storageInfrastructureEncryptionProperties supportsHttpsTrafficOnly: true - allowSharedKeyAccess: true + allowSharedKeyAccess: !app.hub.options.disableStorageSharedKeyAccess isHnsEnabled: true minimumTlsVersion: 'TLS1_2' allowBlobPublicAccess: false diff --git a/src/templates/finops-hub/modules/fx/hub-types.bicep b/src/templates/finops-hub/modules/fx/hub-types.bicep index f3727bf0b..ce514d138 100644 --- a/src/templates/finops-hub/modules/fx/hub-types.bicep +++ b/src/templates/finops-hub/modules/fx/hub-types.bicep @@ -77,6 +77,7 @@ type HubRoutingProperties = { 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.' publisherIsolation: 'Indicates whether FinOps hub resources should be separated by publisher for advanced security.' + disableStorageSharedKeyAccess: 'Indicates whether Shared Key (storage account key) authentication is disabled on hub storage accounts. When enabled, only Microsoft Entra ID and managed identity authentication are allowed. This does not affect Data Factory, which already authenticates via managed identity and RBAC. Does not apply to the private-mode deployment script storage account, which requires Shared Key access.' storageInfrastructureEncryption: 'Indicates whether infrastructure encryption is enabled for the storage account.' storageSku: 'Storage account SKU. Allowed values: "Premium_LRS", "Premium_ZRS".' } @@ -102,6 +103,7 @@ type HubProperties = { natGateway: bool privateRouting: bool publisherIsolation: bool + disableStorageSharedKeyAccess: bool storageInfrastructureEncryption: bool storageSku: string } @@ -195,6 +197,7 @@ func newHubInternal( keyVaultSku string, keyVaultEnablePurgeProtection bool, enableInfrastructureEncryption bool, + disableStorageSharedKeyAccess bool, enablePublicAccess bool, enableNatGateway bool, networkName string, @@ -219,6 +222,7 @@ func newHubInternal( natGateway: !enablePublicAccess && enableNatGateway privateRouting: !enablePublicAccess publisherIsolation: false // TODO: Expose publisher isolation option + disableStorageSharedKeyAccess: disableStorageSharedKeyAccess storageInfrastructureEncryption: enableInfrastructureEncryption storageSku: storageSku } @@ -257,6 +261,7 @@ func newHub( keyVaultSku string, keyVaultEnablePurgeProtection bool, enableInfrastructureEncryption bool, + disableStorageSharedKeyAccess bool, enablePublicAccess bool, enableNatGateway bool, networkAddressPrefix string, @@ -272,6 +277,7 @@ func newHub( keyVaultSku, keyVaultEnablePurgeProtection, enableInfrastructureEncryption, + disableStorageSharedKeyAccess, enablePublicAccess, enableNatGateway, '${safeStorageName(name)}-vnet-${location}', // networkName, cSpell:ignore vnet diff --git a/src/templates/finops-hub/modules/hub.bicep b/src/templates/finops-hub/modules/hub.bicep index d829a0bed..4eb84f2a1 100644 --- a/src/templates/finops-hub/modules/hub.bicep +++ b/src/templates/finops-hub/modules/hub.bicep @@ -27,6 +27,9 @@ param storageSku string = 'Premium_LRS' @description('Optional. Enable infrastructure encryption on the storage account. Default = false.') param enableInfrastructureEncryption bool = false +@description('Optional. Disable Shared Key (storage account key) authentication on hub storage accounts. When enabled, only Microsoft Entra ID and managed identity authentication are allowed. Data Factory already authenticates using managed identity and RBAC, so this does not affect data ingestion. Do not enable this on a hub that receives data from other hubs (remote hub ingestion authenticates with the storage account key). The private-mode deployment script storage account always allows Shared Key access because deployment scripts require it. Default = false.') +param disableStorageSharedKeyAccess bool = false + @description('Optional. SKU to use for the KeyVault instance, if enabled. Allowed values: "standard", "premium". Default: "premium".') @allowed([ 'premium' @@ -196,6 +199,7 @@ var hub = newHub( keyVaultSku, enablePurgeProtection, enableInfrastructureEncryption, + disableStorageSharedKeyAccess, enablePublicAccess, enableNatGateway, virtualNetworkAddressPrefix,