Fix private networking deployment by dropping defaultOutboundAccess - #2259
Conversation
Deploying with enablePublicAccess = false currently fails on dev at the first deployment script: DeploymentScriptStorageAccountWithServiceEndpointEnabled Storage account '<script storage>' has firewall settings enabled which are not supported for deployment scripts. The shipped v14 template deploys the same scenario successfully, so this is a regression in the current cycle. The error message is misleading: the script storage account is configured identically in the working v14 deployment (publicNetworkAccess Enabled, defaultAction Deny, bypass AzureServices, one virtual network rule). The firewall is not the cause. The cause is defaultOutboundAccess being set explicitly on the subnets. Setting it -- at any value, on any subnet in the virtual network -- makes the Deployment Scripts service reject the script storage account. Isolated over nine deployments (storage-only, same region, same subscription): failure occurs exactly when the property is set, without exception. Two runs differing in nothing but this property gave opposite results; the successes reproduce; removing the property only from the script subnet, or leaving that subnet untouched entirely, still fails while other subnets carry it. A final run with the NAT Gateway attached and the property removed everywhere succeeds, which clears the NAT Gateway. Attaching the NAT Gateway is what routes outbound traffic, so the property is redundant. The feature is unaffected. Verified: rebuilt from source and deployed with enableNatGateway = true and enablePublicAccess = false -- deployment succeeds and all 7 deployment scripts run. Fixes #2258 Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
Fixes a regression in the FinOps hub private networking deployment (when enablePublicAccess = false) by removing an Azure subnet property that causes Deployment Scripts to fail even though the script storage account configuration is unchanged.
Changes:
- Removed
defaultOutboundAccessfrom all three subnet definitions to prevent Deployment Scripts from rejecting the script storage account. - Updated the NAT Gateway section comment to document the deployment-script failure mode and why
defaultOutboundAccessmust not be reintroduced.
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Michael Flanakin (flanakin)
left a comment
There was a problem hiding this comment.
🤖 [AI][Claude] PR Review
Summary: This is a well-diagnosed, well-validated fix — removing defaultOutboundAccess from the three subnet definitions resolves a real deployment regression where the Deployment Scripts service rejects the script storage account whenever that property is set on any subnet in the vNet. The Bicep change itself is correct, minimal, and builds cleanly, and the comment explaining why the property must not come back is a nice touch. No blockers.
The only gap: this PR only touches infrastructure.bicep, but three other files still describe the old defaultOutboundAccess behavior that no longer exists, so they're now inaccurate. None of these lines are part of this PR's diff, so I can't attach inline comments — listing them here instead.
⚠️ Should fix (3) — stale references to the removed property
src/powershell/Public/Deploy-FinOpsHub.ps1(NetworkModeparameter help text) — currently says private mode gives you "private endpoints + NAT Gateway, subnets locked down with defaultOutboundAccess=false." That property is gone now. Suggest something like: "private endpoints + NAT Gateway for controlled outbound access — required when the 'Subnets should be private' policy is enforced."src/templates/finops-hub/modules/fx/hub-types.bicep(natGatewayoption description) — says "When enabled, subnets disable Azure default outbound access and route through the NAT Gateway." The first half is no longer true; only the NAT Gateway attachment happens now. Suggest: "When enabled, the script and Data Explorer subnets route outbound traffic through the NAT Gateway."src/templates/finops-hub/createUiDefinition.json(enableNatGatewaycheckbox label) — this is customer-facing Azure Portal text and currently reads "Deploy NAT Gateway and disable default outbound access," which no longer matches what the checkbox does. Suggest: "Deploy NAT Gateway for controlled outbound access."
💡 Suggestions (2)
src/scripts/Deploy-Hub.ps1— private mode help text says "subnets locked down," which still implies the removed behavior even though it doesn't name the property directly. Worth tightening for consistency with whatever wording is chosen forDeploy-FinOpsHub.ps1, though lower priority since it's not explicitly incorrect.- Changelog — no entry added. Defensible: the feature that introduced the bug (NAT Gateway for private subnets, #2163) is only in the commented-out "Unreleased" section of the changelog, not a released version, so this is arguably fixing pre-release work rather than a shipped regression. Worth a quick check with maintainers on whether the #2163 changelog entry (once uncommented for release) should just reflect the corrected behavior directly, with no separate "Fixed" line needed.
… text The property was removed from the subnet definitions because Deployment Scripts rejects the script storage account whenever it is set, but six parameter descriptions and one portal label still described the old behavior. Attaching the NAT Gateway is what routes outbound traffic now, so the text describes that instead. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
Thanks — all three "should fix" items and the Fixed (6 strings)
The last two are the I used "the script and Data Explorer subnets" rather than a bare "subnets" because only those two are attached to the NAT Gateway; the private endpoint subnet is not.
On the changelogI've left it out. The #2163 entry is still inside the commented-out Unreleased block, and its text is "Added VNet and private network modes, including opt-in NAT Gateway support for private mode" — it never described the Validation
|
Brett Wilson (MSBrett)
left a comment
There was a problem hiding this comment.
The current change removes a policy-required subnet setting. NAT Gateway attachment and Azure Policy compliance are separate requirements. The deployment-script storage path must change so both requirements can be met.
Findings and test evidenceNAT Gateway attachment is necessary for outbound traffic, but it does not meet the Azure Policy requirement by itself. Policy The Deployment Scripts conflict comes from the Storage service-endpoint design. The fix keeps Requirements traceability matrix
The fix and review remediation are pushed through commit |
Use an Azure Files private endpoint for deployment-script storage while retaining explicit defaultOutboundAccess settings and NAT routing. Co-authored-by: Copilot App <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep:28
- The comment says Azure Policy requires private subnets to set
defaultOutboundAccessto false explicitly, but the subnet definitions setdefaultOutboundAccess: !hub.options.natGateway(which evaluates to true when NAT Gateway is disabled). This is misleading and may cause incorrect assumptions about policy compliance; please reword to describe the actual conditional behavior.
// Azure Policy requires private subnets to set defaultOutboundAccess to false explicitly.
docs-mslearn/toolkit/changelog.md:36
- This changelog entry says private subnets now set
defaultOutboundAccesstofalse, but the template setsdefaultOutboundAccess: !hub.options.natGateway(false only when NAT is enabled). Please update the wording so the release note matches the actual behavior.
- Fixed private-network deployments that Azure Policy blocked when `defaultOutboundAccess` was omitted. Private subnets now set it to `false`, while an Azure Files private endpoint supports deployment-script storage and the NAT Gateway provides required container egress ([#2258](https://github.com/microsoft/finops-toolkit/issues/2258), [#2259](https://github.com/microsoft/finops-toolkit/pull/2259)).
src/powershell/Tests/Unit/HubsPrivateNetworking.Tests.ps1:35
- This test unconditionally runs
bicep buildinBeforeAll. Other unit tests in this repo skip when the bicep CLI isn't available (e.g., HubsIngestionQueries.Tests.ps1), but this test will fail hard in environments where bicep isn't installed. Please add aGet-Command bicepguard and skip the test when the CLI is missing.
BeforeAll {
$repoRoot = (Resolve-Path "$PSScriptRoot/../../../..").Path
$bicepPath = Join-Path $repoRoot 'src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Core/infrastructure.bicep'
$templatePath = Join-Path ([System.IO.Path]::GetTempPath()) "finops-hub-infrastructure-$([guid]::NewGuid()).json"
bicep build $bicepPath --outfile $templatePath
if ($LASTEXITCODE -ne 0)
{
throw 'Failed to compile the FinOps hub infrastructure template.'
}
Clarify that defaultOutboundAccess is disabled in private mode and skip the compiled-ARM regression test when the Bicep CLI is unavailable. Co-authored-by: Copilot App <[email protected]>
|
Addressed the three suppressed suggestions from review 4973971190 in commit $(264e5463d08eb30a2707c6d7ca6c5bb42e614cf0.Substring(0,8)):
Validation: both public Bicep entry points compiled; the focused test passed with Bicep installed and skipped cleanly with Bicep absent. |
Live validation of this branchDeployed the current branch head end to end on a disposable test subscription, in both modes affected by the regression. This closes the one row marked Incomplete in the traceability matrix above.
Storage-only ( The 7/7 is the part that matters. In all five failing runs from the original investigation, the deployment aborted at the first deployment script, before any container group started — so "Succeeded" alone would not have been sufficient evidence. Here the container groups actually ran, and the For completeness: the compiled-ARM regression tests in One observation, not a blockerIn On the original approach in this PRRemoving 🤖 Generated with Claude Code |
Fixes #2258
🐛 Problem
Deploying with
enablePublicAccess = falsefails ondevat the first deployment script:The shipped v14 template deploys the same scenario successfully, so this is a regression in the current cycle. Both private network modes are affected, since
privateRoutingis!enablePublicAccess.The error message is misleading. The script storage account it names is configured identically in the working v14 deployment:
🔍 Cause
defaultOutboundAccessis now set explicitly on all three subnets. Setting it — at any value, on any subnet in the virtual network — makes the Deployment Scripts service reject the script storage account.🔧 Solution
Drop the property from the three subnet definitions. Attaching the NAT Gateway is what routes outbound traffic, so the explicit setting is redundant and the feature is unaffected.
A comment above the NAT Gateway block records why the property must not come back, including the fact that the Azure error points at the storage account rather than the subnet.
🧪 Validation
Nine deployments on a test subscription (storage-only, same region):
Failure occurs exactly when the property is set, without exception. #3 vs #4 differ in nothing else. #5/#8 show the successes reproduce. #6/#7 rule out a script-subnet-only explanation. #9 was run as a falsifiable prediction and cleared the NAT Gateway.
This branch: rebuilt from source and deployed with
enableNatGateway: trueandenablePublicAccess: false— succeeded in 18 minutes, all 7 deployment scripts ran, NAT Gateway attached to the script and Data Explorer subnets, property absent everywhere.Single-file template change; no PowerShell, workbook or query code is touched.
🤖 Generated with Claude Code