Repository navigation
Add opt-in Key Vault RBAC authorization for CAF/landing zone compliance - #2249
Michael Flanakin (flanakin) wants to merge 6 commits into
Conversation
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
Review — PR #2249
Approving the approach. The consumer trace is the part that matters most here and it holds up: the ADF AzureKeyVault linked service is the only data-plane secret reader, the keyVault_secret module write is management-plane and genuinely unaffected by the vault's authorization model, and no other hub app touches this vault. Granting exactly one Key Vault Secrets User assignment is the right scope — not a broader role, not a second assignment for the deploying principal.
Opt-in defaulting to false is also right, and for the reason stated: flipping enableRbacAuthorization on an existing vault silently stops honoring access policies with no rollback short of recreating the vault, and this template is explicitly redeploy-over-existing. Forcing it on would break deployed hubs at upgrade time. Following the enablePurgeProtection precedent keeps that consistent.
Two notes, neither blocking.
newHub() positional-argument fragility
keyVaultEnableRbacAuthorization is inserted mid-signature in both newHubInternal and newHub, and both are called positionally. There's exactly one caller today (modules/hub.bicep:190), so this is safe as written — I checked. But the signature is now 15+ positional bools and strings, and the next insertion is one where a mis-ordered pair of same-typed args compiles clean and misconfigures a hub silently. Not this PR's job to fix, but worth an issue: these should take a single object parameter, or at minimum new options should append rather than insert.
RBAC propagation vs. access-policy immediacy
Access policy grants take effect essentially immediately; RBAC role assignments can take a few minutes to propagate. ADF reads the secret at pipeline runtime rather than at deployment time, so in the normal case the assignment has long since propagated by the time anything needs it. The edge case is a pipeline triggered immediately after a first deployment with the flag on — that could see a transient 403 that looks like a misconfiguration rather than a timing artifact.
Not worth adding a dependsOn dance over, but if the deployment surfaces a "hub is ready" message anywhere, a sentence in the docs noting that RBAC-authorized vaults may need a few minutes before the first ingestion run succeeds would save someone a support round-trip.
Small thing
The enablePurgeProtection tooltip gained "This cannot be disabled once enabled" — good addition. The enableRbacAuthorization tooltip doesn't carry the equivalent warning, and its irreversibility is arguably the sharper one of the two (purge protection costs you a 90-day wait; a botched RBAC switch costs you vault access). Consider mirroring the wording from the main.bicep parameter description, which does say it.
Addresses PR #2249 review feedback: the top-level parameter name in main.bicep/hub.bicep was too generic given RBAC applies across many Azure resource types, unlike the already-scoped keyVaultEnableRbacAuthorization used internally. Renamed the parameter, its createUiDefinition.json element/output binding, and the changelog reference for consistency. The ARM schema property name in hub-app.bicep (enableRbacAuthorization on Microsoft.KeyVault/vaults) is unchanged since it belongs to the resource type, not this parameter. No code change was needed for the existing-deployments question: traced that accessPolicies is a direct property on the always-redeclared Microsoft.KeyVault/vaults resource, so redeploying with RBAC enabled fully replaces (not merges with) the prior access policy array -- no dual-auth state results. 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: flanakin <flanakin@users.noreply.github.com> Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
Re-review — PR #2249
One new commit since my 2026-08-18 review (3d9b06ab, the rename), plus the two replies. Approach and implementation still hold up; one gap worth closing before merge.
Resolved
Rename enableRbacAuthorization → enableKeyVaultRbacAuthorization — complete. Grepped the branch: the only surviving enableRbacAuthorization is the ARM schema property on Microsoft.KeyVault/vaults in hub-app.bicep, correctly left alone. Parameter declaration, newHub() call site, createUiDefinition element name and output binding, and the changelog entry all agree.
Dual-auth question — the answer is correct, and I verified the mechanism rather than taking it on faith. accessPolicies is a direct property on the vault resource, not the additive Microsoft.KeyVault/vaults/accessPolicies child type, and the vault is redeclared on every deploy. Incremental mode's "leave absent things alone" guarantee covers resources, not properties of a resource that is declared — so the [] array genuinely replaces the live policy. Clean cutover, no dual-auth state. Resolving that thread.
One gap: the PowerShell deployment path
Deploy-FinOpsHub exposes purge protection but not this. src/powershell/Public/Deploy-FinOpsHub.ps1 declares -EnablePurgeProtection as a switch (line 146), documents it (line 34), and forwards it under a >= '13.0' version gate (line 322). There is no equivalent for enableKeyVaultRbacAuthorization, so anyone deploying through the PowerShell module can't enable RBAC at all — portal and raw Bicep only.
The PR follows "the exact pattern already used for enablePurgeProtection," and that pattern spans four surfaces: main.bicep, hub.bicep, createUiDefinition.json, and the cmdlet. Three of four are done. The fix is small, with a template right beside it:
[Parameter()]
[switch]
$EnableKeyVaultRbacAuthorization,plus a >= '15.0' gate block (matching how enableNatGateway is gated — branch is 15.0.0-dev.0) and a .PARAMETER help entry. I'd treat this as blocking-ish: the module is the scripted-deployment path for exactly the CAF/landing-zone customers this feature targets. Commented inline on the param declaration since the file itself isn't in the diff.
Still open from last review
Details inline: the tooltip irreversibility warning (actionable), RBAC propagation timing (cosmetic), and newHub() positional fragility (separate issue, not this PR's job).
Housekeeping
PR shows CONFLICTING against flanakin/v15-prep. I ran the merge locally — the only conflict is changelog.md. Details inline.
Verdict: sound work and a clean rename. Add the Deploy-FinOpsHub switch and the tooltip sentence, merge the base in, and this is ready to go.
…oltip irreversibility warning Addresses PR #2249 review feedback: the PowerShell deployment path was missing the enableKeyVaultRbacAuthorization switch that main.bicep/hub.bicep/createUiDefinition.json already exposed, and the portal checkbox tooltip was missing the irreversibility warning that the sibling enablePurgeProtection checkbox already carries. Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
|
🤖 [AI][Claude] PR Update Summary Addressed: 5 thread(s)
Implemented the two concrete, low-risk gaps Roland flagged: added the missing Left for human judgment: the |
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
Everything from my last round is addressed: the parameter is only forwarded when the switch is set, the Pester cases cover it, the tooltip is fixed, and the changelog conflict is resolved. Without the switch the template default false applies, so existing hubs keep their access policy and nothing flips on redeploy.
Non-blocking:
Deploy-FinOpsHub.ps1:38: the.PARAMETERhelp still says the switch "cannot be rolled back without recreating the vault". Please use the tooltip wording.Deploy-FinOpsHub.ps1:332: with-Version< 15.0 the switch is silently ignored, while private network mode throws in the same case. A throw or warning plus a test would help.- A hub that opted into RBAC goes back to access policies if it's redeployed without the switch. That's worth a sentence in the docs, as a follow-up.
I'll open the separate issue for moving newHub() to an object parameter.
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
Inline anchors for the points in my earlier review.
Address PR #2249 feedback. Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
Re-reviewed 40b4151. Both non-blocking points from my last round are addressed: the .PARAMETER help now matches the portal tooltip, and -EnableKeyVaultRbacAuthorization throws on -Version below 15.0 the same way private network mode does, with a Pester case that also asserts no deployment is attempted. The forwarding logic is unchanged, so -Version latest still only sends the parameter when the switch is set.
The newHub() object-parameter cleanup is tracked in #2354. Still a follow-up, not blocking: a sentence in the docs that a hub opted into RBAC reverts to access policies when redeployed without the switch.
Optional: the PowerShell module v15 section of the changelog doesn't mention the new Deploy-FinOpsHub -EnableKeyVaultRbacAuthorization switch (comment inline).
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
Addresses PR #2249 review feedback: the top-level parameter name in main.bicep/hub.bicep was too generic given RBAC applies across many Azure resource types, unlike the already-scoped keyVaultEnableRbacAuthorization used internally. Renamed the parameter, its createUiDefinition.json element/output binding, and the changelog reference for consistency. The ARM schema property name in hub-app.bicep (enableRbacAuthorization on Microsoft.KeyVault/vaults) is unchanged since it belongs to the resource type, not this parameter. No code change was needed for the existing-deployments question: traced that accessPolicies is a direct property on the always-redeclared Microsoft.KeyVault/vaults resource, so redeploying with RBAC enabled fully replaces (not merges with) the prior access policy array -- no dual-auth state results. 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: flanakin <flanakin@users.noreply.github.com> Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
…oltip irreversibility warning Addresses PR #2249 review feedback: the PowerShell deployment path was missing the enableKeyVaultRbacAuthorization switch that main.bicep/hub.bicep/createUiDefinition.json already exposed, and the portal checkbox tooltip was missing the irreversibility warning that the sibling enablePurgeProtection checkbox already carries. Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
Address PR #2249 feedback. Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
cad4472 to
6d91ddc
Compare
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
Addresses PR #2249 review feedback: the top-level parameter name in main.bicep/hub.bicep was too generic given RBAC applies across many Azure resource types, unlike the already-scoped keyVaultEnableRbacAuthorization used internally. Renamed the parameter, its createUiDefinition.json element/output binding, and the changelog reference for consistency. The ARM schema property name in hub-app.bicep (enableRbacAuthorization on Microsoft.KeyVault/vaults) is unchanged since it belongs to the resource type, not this parameter. No code change was needed for the existing-deployments question: traced that accessPolicies is a direct property on the always-redeclared Microsoft.KeyVault/vaults resource, so redeploying with RBAC enabled fully replaces (not merges with) the prior access policy array -- no dual-auth state results. 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: flanakin <flanakin@users.noreply.github.com> Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
…oltip irreversibility warning Addresses PR #2249 review feedback: the PowerShell deployment path was missing the enableKeyVaultRbacAuthorization switch that main.bicep/hub.bicep/createUiDefinition.json already exposed, and the portal checkbox tooltip was missing the irreversibility warning that the sibling enablePurgeProtection checkbox already carries. Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
6d91ddc to
89c83f4
Compare
Address PR #2249 feedback. Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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
Addresses PR #2249 review feedback: the top-level parameter name in main.bicep/hub.bicep was too generic given RBAC applies across many Azure resource types, unlike the already-scoped keyVaultEnableRbacAuthorization used internally. Renamed the parameter, its createUiDefinition.json element/output binding, and the changelog reference for consistency. The ARM schema property name in hub-app.bicep (enableRbacAuthorization on Microsoft.KeyVault/vaults) is unchanged since it belongs to the resource type, not this parameter. No code change was needed for the existing-deployments question: traced that accessPolicies is a direct property on the always-redeclared Microsoft.KeyVault/vaults resource, so redeploying with RBAC enabled fully replaces (not merges with) the prior access policy array -- no dual-auth state results. 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: flanakin <flanakin@users.noreply.github.com> Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
…oltip irreversibility warning Addresses PR #2249 review feedback: the PowerShell deployment path was missing the enableKeyVaultRbacAuthorization switch that main.bicep/hub.bicep/createUiDefinition.json already exposed, and the portal checkbox tooltip was missing the irreversibility warning that the sibling enablePurgeProtection checkbox already carries. Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
…rify RBAC tooltip Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Address PR #2249 feedback. Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
89c83f4 to
2f96200
Compare
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
Re-reviewed after the rebase. The template, UI definition and tests carry over cleanly. One regression from the merge with dev: the two new version gates still cast $Version directly, so the single-number release format that dev now supports breaks when the RBAC switch is set.
| } | ||
|
|
||
| # Key Vault RBAC authorization requires hub template version 15.0 or later | ||
| if ($EnableKeyVaultRbacAuthorization -and $Version -ne 'latest' -and [version]$Version -lt '15.0') |
There was a problem hiding this comment.
[version]$Version throws for -Version 15 (or v15 once the prefix is stripped): Version string portion was too short or too long. Dev added $templateVersion a few lines up for exactly this case. Suggest $templateVersion -lt '15.0', matching the NAT Gateway gate above. Then -Version 14 also gets the friendly error instead of a cast error.
| $parameterSplat.TemplateParameterObject.Add('enablePurgeProtection', $EnablePurgeProtection.IsPresent) | ||
| } | ||
|
|
||
| if ($EnableKeyVaultRbacAuthorization -and ($Version -eq 'latest' -or [version]$Version -ge '15.0')) |
There was a problem hiding this comment.
Same here: $templateVersion -ge '15.0'. Could you also add a test case with -EnableKeyVaultRbacAuthorization -Version '15' and '14' next to the existing '15.0' / '14.0' cases?
|
@Michael Flanakin (@flanakin): you have some new feedback! Please review and resolve all comments and I'll let reviewers know by removing the |
Summary
Adds an
enableRbacAuthorizationparameter to the FinOps hub Bicep templates that switches the remote-hub Key Vault from the legacy access-policy model to Azure RBAC. This is required by Cloud Adoption Framework (CAF) / Enterprise-Scale landing zone Azure Policy guardrails (e.g., "Enforce recommended guardrails for Azure Key Vault"), which flag vaults that use access policies instead of RBAC.Fixes #1067
What changed
src/templates/finops-hub/modules/fx/hub-app.bicep— the Key Vault resource now setsenableRbacAuthorization: app.hub.options.keyVaultEnableRbacAuthorizationinstead of a hardcodedfalse. When RBAC is enabled:accessPoliciesis forced to an empty array (Azure rejects a non-emptyaccessPoliciesarray whenenableRbacAuthorization: true).keyVaultRoleAssignmentresource grants the Data Factory managed identity the built-in Key Vault Secrets User role (4633458b-17de-408a-b874-0445c86b69e6) on the vault — the RBAC equivalent of thesecrets: ['get']access policy it previously relied on. This is the only identity that reads secrets from this vault (verified below), so it's the only equivalent role assignment needed.src/templates/finops-hub/modules/fx/hub-types.bicep— addedkeyVaultEnableRbacAuthorizationtoHubProperties.optionsand threaded it throughnewHubInternal/newHub.src/templates/finops-hub/modules/hub.bicepandmain.bicep— added anenableRbacAuthorization bool = falseparameter, following the exact pattern already used forenablePurgeProtection.createUiDefinition.json— added a matching "Enable Key Vault RBAC authorization" checkbox next to the existing purge protection checkbox, gated to the remote-hub analytics engine path (the only path that deploys this vault).docs-mslearn/toolkit/changelog.md— added an entry under Unreleased.Purge protection (
enablePurgeProtection/enableSoftDelete) was already fully implemented on this branch (not something I needed to add) —enableSoftDelete: trueis hardcoded, andenablePurgeProtectionis already wired end-to-end as an opt-in parameter defaulting tofalse, following the exact convention I followed for RBAC here.Investigation: what currently depends on access policies
I traced every consumer of the Key Vault (
Microsoft.KeyVault/vaultsinsrc/templates/finops-hub/modules/fx/hub-app.bicep:471, only deployed when theRemoteHubapp requests the'KeyVault'feature —modules/Microsoft.FinOpsHubs/RemoteHub/app.bicep:52):AzureKeyVaultlinked service (hub-app.bicep:201,linkedService_keyVault) — ADF authenticates as its own system-assigned managed identity at runtime to read theremoteHubStorageconnection secret viaAzureKeyVaultSecret. This is the only data-plane secret reader, and it's exactly what the pre-existingkeyVaultAccessPoliciesvar (secrets: ['get']) granted. Now granted via RBAC (Key Vault Secrets User) whenenableRbacAuthorization: true.keyVault_secretmodule (fx/hub-vault.bicep, invoked fromRemoteHub/app.bicep:59) writes the storage key as a nested ARMMicrosoft.KeyVault/vaults/secretsresource. This is a management-plane (ARM) write authorized by the deploying principal's Azure RBAC role on the resource group (e.g., Contributor), not by the vault's own access-policy/RBAC-authorization setting — so it is unaffected either way.Core,Exports,Analytics,AzureResourceGraph, etc.) references this Key Vault at all.So the only identity needing a new role assignment is Data Factory's managed identity, which I added.
PR #1349 (closed, not merged)
Read
gh pr diff 1349before starting. It only addedenablePurgeProtection(12 lines acrossmain.bicep/hub.bicep/keyVault.bicep) against the pre-reorganization template layout — those exact file paths (modules/keyVault.bicep) no longer exist; the Key Vault resource now lives inmodules/fx/hub-app.bicepas part of the namespace-basedfx/Microsoft.FinOpsHubsrestructuring. It didn't touch RBAC at all, and its purge-protection change has since been superseded by a more complete implementation already ondev/v15-prep. I did not build on it — nothing to build on for RBAC, and the purge-protection piece it targeted was already done more thoroughly elsewhere.Design decision: opt-in, not forced-on
Both
enableRbacAuthorizationandenablePurgeProtectionare irreversible per Azure's API contract:enablePurgeProtection: trueis set on a vault, it cannot be reverted.enableRbacAuthorizationfromfalsetotrueon an existing vault immediately stops honoring access policies; any identity not covered by an equivalent RBAC role assignment loses access with no rollback path (short of recreating the vault).Because the FinOps hub template supports redeploying over existing hub instances (upgrade scenario), forcing either property on unconditionally would silently break already-deployed hubs that don't opt in, with no way back. I followed the existing precedent set by
enablePurgeProtection(already opt-in, defaulting tofalse, surfaced identically inmain.bicep/hub.bicep/createUiDefinition.json) and applied the same pattern toenableRbacAuthorization. Organizations that need CAF/Enterprise-Scale compliance can set both parameters totrueexplicitly; organizations upgrading an existing hub are not force-migrated into a breaking, irreversible change they didn't ask for.I believe this is the safer default, but it does mean the compliance gap in #1067 isn't closed by default — only when explicitly enabled. If maintainers prefer defaulting new deployments to
truewhile keeping upgrades safe, that would need a way to distinguish first-deploy from redeploy, which Bicep can't do natively (no reliable "does this resource already exist" check without aexistinglookup that fails hard on first deploy). Flagging this as an open discussion point for reviewers.Verification
bicep build src/templates/finops-hub/main.bicep --stdout— clean, no errors (2 pre-existing unrelated warnings inRecommendations/app.bicep, confirmed present before this change viagit stash).bicep buildonhub.bicep,hub-app.bicep, andhub-types.bicepindividually — all clean.pwsh -Command "./src/scripts/Test-PowerShell.ps1 -Lint"— 3418/3418 passed, includingms.datefrontmatter checks (already current at08/12/2026, no update needed).az deployment ... what-if— no Azure credentials available in this environment. Everything else that could be verified statically was.Open questions for reviewers
enableRbacAuthorization/enablePurgeProtectiontotruewhile upgrades stay opt-in? Flagged above as not straightforwardly expressible in Bicep.Key Vault Secrets Useris the right role scope (read-onlyget/liston secrets) rather thanKey Vault Secrets Officer— I matched the existing access policy'ssecrets: ['get']scope exactly, soSecrets User(read-only) seemed correct, but worth a second look given ADF also needs to enumerate the linked service's secret at authoring/refresh time.Test plan