Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The goal of this exercise is to establish foundational sovereign cloud governanc

## Actions

- Create and assign Azure Policy controls to restrict deployments to the lab-approved European regions (Norway East, Germany North, North Europe, West Europe). West Europe accommodates Azure Local management resources when LocalBox is registered there.
- Create and assign Azure Policy controls using the lab-approved European regions (Norway East, Germany North, North Europe, West Europe), plus **Australia East as a lab-only exception** for Azure Local management resources. This exception is not a European data-residency recommendation; keep the exercise assignments in **DoNotEnforce** mode.
- Enforce resource tagging requirements for data classification and compliance tracking.
- Block public IP resource creation and evaluate storage public-network-access restrictions. Disabling public network access does not create or verify a private endpoint.
- Assign least-privilege RBAC roles for the SovereignOps team.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Optional tenant-specific object ID of the Microsoft.AzureStackHCI enterprise
application. The script resolves it through Microsoft Graph when omitted.
.PARAMETER AzureLocalInstanceLocation
Azure Local registration region, separate from the Azure host region.
Defaults to West Europe to align with the Challenge 1 location allowlist.
Defaults to Australia East for the lab. The upstream staging storage account
also uses this region, independently of the Azure host region.
.PARAMETER NoWait
Submit the deployment without waiting for ARM completion.
#>
Expand Down Expand Up @@ -57,7 +58,7 @@ param(
[string]$AzureLocalResourceProviderObjectId,

[ValidateSet('australiaeast', 'southcentralus', 'eastus', 'westeurope', 'southeastasia', 'canadacentral', 'japaneast', 'centralindia')]
[string]$AzureLocalInstanceLocation = 'westeurope',
[string]$AzureLocalInstanceLocation = 'australiaeast',

[switch]$NoWait
)
Expand All @@ -68,14 +69,24 @@ $PSNativeCommandUseErrorActionPreference = $true
function Invoke-AzJson {
param([Parameter(Mandatory = $true)][string[]]$Arguments)

$output = & az @Arguments --only-show-errors --output json
if ($LASTEXITCODE -ne 0) {
throw "Azure CLI command failed: az $($Arguments -join ' ')"
# Preserve Azure's error details for the caller's regional fallback classifier.
$PSNativeCommandUseErrorActionPreference = $false
$output = @(& az @Arguments --only-show-errors --output json 2>&1)
$exitCode = $LASTEXITCODE
$stderr = @($output | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] })
$stdout = ($output | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] }) -join "`n"
$operation = ($Arguments | Select-Object -First 3) -join ' '
if ($exitCode -ne 0) {
throw "Azure CLI 'az $operation' failed (exit code ${exitCode}): $($stderr -join "`n")"
}
if ([string]::IsNullOrWhiteSpace(($output -join "`n"))) {
foreach ($message in $stderr) { Write-Warning "$message" }
if ([string]::IsNullOrWhiteSpace($stdout)) {
return $null
}
return ($output -join "`n") | ConvertFrom-Json
try { return $stdout | ConvertFrom-Json -ErrorAction Stop }
catch {
throw "Azure CLI 'az $operation' returned invalid JSON. Raw stdout and command arguments are omitted because they may contain sensitive data. Inspect deployment state before retrying."
}
}

function New-LocalBoxPassword {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ else {
-ResourceGroupName $localBoxResourceGroupName `
-Location $localBoxLocation `
-AzureLocalResourceProviderObjectId $azureLocalResourceProviderObjectIds[0] `
-AzureLocalInstanceLocation 'westeurope' `
-AzureLocalInstanceLocation 'australiaeast' `
-UseConsoleCredentials `
-NoWait

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,34 @@ organizer's post-provisioning preparation.

### LocalBox registration region and location policies

If LocalBox validation reports `RequestDisallowedByAzure` with
`locationineligible` ("the selected region is currently not accepting new
customers"), identify the rejected resource and its actual region before
changing the host-region selection. This is distinct from an exercise policy's
`RequestDisallowedByPolicy`. The upstream
[LocalBox template](https://github.com/microsoft/azure_arc/blob/main/azure_jumpstart_localbox/bicep/main.bicep)
passes `azureLocalInstanceLocation` to the staging storage module, whose account
name is `localbox` plus a generated suffix. In the 4 October 2026 hosted test,
this account was rejected in West Europe while the host was selected in Spain
Central. **Host-region fallback cannot resolve a rejection in the fixed
registration/staging-storage region.** The shared setup tries
the next configured preferred host region for recognized regional failures;
it does not change the Azure Local registration region or delete the shared
resource group. The CLI wrapper retains Azure's error details in the exception
so PowerShell's generic native-command exit error cannot hide them from the
fallback check. Policy denials, authorization failures, and unrecognized errors
still stop the run. If all preferred regions fail, the final error is reported;
do not assume a region is available merely because its SKU/quota checks passed.

There are three separate locations to check: the participant resource group's
metadata location, the Azure region hosting the LocalBox simulator, and the
Azure Local/custom-location registration region. The hosted
[shared hook](../../labautomation/shared-deploy-lab.ps1) explicitly passes
`AzureLocalInstanceLocation = westeurope`; the
`AzureLocalInstanceLocation = australiaeast`; the
[manual deployment](../manual-setup/localbox/deploy-localbox.ps1) defaults to
`westeurope`. Host-region fallback does not change that registration parameter.
The hosted deployer's default also matches West Europe. This changes registration
for fresh deployments, not the Azure host-region selection. Inspect the deployed
`australiaeast`. Host-region fallback does not change that registration parameter.
The hosted deployer's default also matches Australia East. This changes registration
and staging storage for fresh deployments, not the Azure host-region selection. Inspect the deployed
custom location's **JSON View** for its actual `location`.

Azure Local VM management resources use the custom location's region even when
Expand All @@ -64,16 +83,17 @@ exception. Do not disable unrelated policies or automatically broaden the
subscription allowlist. The hosted control tags below do not override arbitrary
location-deny policies.

[Microsoft's current Azure Local region list](https://learn.microsoft.com/azure/azure-local/concepts/system-requirements-23h2#azure-requirements)
includes West Europe as its only European region for hyperconverged deployments.
The current Challenge 1 exercise allowlists include West Europe, but older
assignments may still use the original three-region list. This does not prove
that West Europe is allowed by the event subscription's inherited policies, and
it does not permit Australia East. Existing Azure assignments are not updated
automatically. Earlier hosted test environments registered in Australia East
are not relocated by this change; validate the next event using a fresh deployment
from the updated content. Existing-environment cleanup is a separate organizer
action, not part of the registration-region change.
The lab returns to Australia East after this West Europe rejection; this is
not evidence that West Europe is universally unavailable for Azure Local.
[Service region support](https://learn.microsoft.com/azure/azure-local/concepts/system-requirements-23h2#azure-requirements)
does not guarantee subscription eligibility for every dependent resource.
Challenge 1 retains its four European regions and adds Australia East as an
explicit **lab-only exception**, not a European data-residency recommendation.
Earlier assignments must be updated by their owners; inherited policies may
still deny the region, and control tags do not bypass them. Existing Azure
assignments and resources are not updated or relocated automatically. Validate
the next event using a fresh deployment and the participant VM exercise.
Existing-environment cleanup is a separate organizer action.

### Hosted MCAPS control-tag initiative

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
.PARAMETER Location
Azure region for deployment (default: swedencentral)

.PARAMETER AzureLocalInstanceLocation
Azure Local registration and staging storage region (default: australiaeast).
This lab default is independent of the Azure host region.

.PARAMETER WindowsAdminUsername
Admin username for Windows VMs (default: arcdemo)

Expand Down Expand Up @@ -80,7 +84,7 @@ param(

[Parameter(Mandatory = $false)]
[ValidateSet('australiaeast', 'southcentralus', 'eastus', 'westeurope', 'southeastasia', 'canadacentral', 'japaneast', 'centralindia')]
[string]$AzureLocalInstanceLocation = "westeurope"
[string]$AzureLocalInstanceLocation = "australiaeast"
)

Write-Host "`n=== Azure Arc Jumpstart LocalBox Deployment ===" -ForegroundColor Cyan
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Run the [manual LocalBox entry point](localbox/deploy-localbox.ps1) from an auth
./localbox/deploy-localbox.ps1 -ResourceGroupName 'rg-localbox-shared' -Location 'swedencentral'
```

Choose an allowed Azure Local registration region with `-AzureLocalInstanceLocation` when needed. It can differ from the Azure host region. Wait for the nested Azure Local deployment, then connect to `LocalBox-Client` and follow the shared [post-provisioning guide](../localbox/readme.md). Supply an existing Entra security-group object ID for AKS; its intended administrators must be members.
The Azure Local registration and staging storage region defaults to **Australia East** for this lab, independently of the Azure host region. Choose another supported and subscription-eligible region with `-AzureLocalInstanceLocation` when needed; this lab default is not a European data-residency recommendation. Wait for the nested Azure Local deployment, then connect to `LocalBox-Client` and follow the shared [post-provisioning guide](../localbox/readme.md). Supply an existing Entra security-group object ID for AKS; its intended administrators must be members.

Run the [health checks](../tests/readme.md) before participants begin. Verify scoped participant access and approved Defender for Servers settings using the [manual preparation and readiness reference](../localbox/manual-preparation.md).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ Describe 'Console LocalBox credential isolation' {
}

Describe 'LocalBox registration region contract' {
It 'defaults <Path> to West Europe independently of the host region' -TestCases @(
It 'defaults <Path> to Australia East independently of the host region' -TestCases @(
@{ Path = 'labautomation/deploy-localbox.ps1' }
@{ Path = 'resources/manual-setup/localbox/deploy-localbox.ps1' }
) {
Expand All @@ -709,32 +709,129 @@ Describe 'LocalBox registration region contract' {
"$PSScriptRoot/../../$Path", [ref]$null, [ref]$errors)
$errors.Count | Should -Be 0
$registration = $ast.ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'AzureLocalInstanceLocation' }
$registration.DefaultValue.SafeGetValue() | Should -Be 'westeurope'
$registration.DefaultValue.SafeGetValue() | Should -Be 'australiaeast'
$hostRegion = $ast.ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'Location' }
$hostRegion.DefaultValue.SafeGetValue() | Should -Be 'swedencentral'
$ast.Extent.Text | Should -Match 'azureLocalInstanceLocation\s*=\s*@\{\s*value\s*=\s*\$AzureLocalInstanceLocation\s*\}'
$ast.Extent.Text | Should -Match 'location\s*=\s*@\{\s*value\s*=\s*\$Location\s*\}'
}
It 'uses West Europe registration while preserving shared host-region selection' {
It 'uses Australia East registration while preserving shared host-region selection' {
$errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
"$PSScriptRoot/../../labautomation/shared-deploy-lab.ps1", [ref]$null, [ref]$errors)
$errors.Count | Should -Be 0
$ast.Extent.Text | Should -Match "-AzureLocalInstanceLocation 'westeurope'"
$ast.Extent.Text | Should -Match "-AzureLocalInstanceLocation 'australiaeast'"
$ast.Extent.Text | Should -Match '-Location \$localBoxLocation'
$ast.Extent.Text | Should -Not -Match "-AzureLocalInstanceLocation 'australiaeast'"
$ast.Extent.Text | Should -Not -Match "-AzureLocalInstanceLocation 'westeurope'"
}
It 'includes the registration region in all four Challenge 1 policy parameter lists' {
$guide = Get-Content "$PSScriptRoot/../../walkthrough/challenge-01/solution-01.md" -Raw
$lists = [regex]::Matches($guide, '"listOfAllowedLocations"\s*:\s*\{\s*"value"\s*:\s*(\[[^\]]*\])')
$lists.Count | Should -Be 4
foreach ($list in $lists) {
$regions = @($list.Groups[1].Value | ConvertFrom-Json)
$regions.Count | Should -Be 4
foreach ($region in @('norwayeast', 'germanynorth', 'northeurope', 'westeurope')) {
$regions.Count | Should -Be 5
foreach ($region in @('norwayeast', 'germanynorth', 'northeurope', 'westeurope', 'australiaeast')) {
$regions | Should -Contain $region
}
$regions | Should -Not -Contain 'australiaeast'
}
}
}

Describe 'Console LocalBox CLI failure propagation' {
BeforeAll {
$deployer = [Management.Automation.Language.Parser]::ParseFile(
"$PSScriptRoot/../../labautomation/deploy-localbox.ps1", [ref]$null, [ref]$null)
$helper = $deployer.Find({
param($node)
$node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Invoke-AzJson'
}, $true)
. ([scriptblock]::Create($helper.Extent.Text))
$shared = [Management.Automation.Language.Parser]::ParseFile(
"$PSScriptRoot/../../labautomation/shared-deploy-lab.ps1", [ref]$null, [ref]$null)
$loop = $shared.Find({
param($node)
$node -is [Management.Automation.Language.ForEachStatementAst] -and
$node.Variable.Extent.Text -eq '$localBoxLocation'
}, $true)
$fallback = Join-Path $TestDrive 'fallback.ps1'
$loop.Extent.Text | Set-Content $fallback
$nativeExecutable = (Get-Process -Id $PID).Path
function az { & $nativeExecutable -NoProfile -NonInteractive -Command $cliState.Command }
}
BeforeEach {
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$cliState = @{ Command = '[Console]::Error.WriteLine("RequestDisallowedByAzure: locationineligible"); exit 1'; Attempts = 0 }
}
It 'retains regional error details with native error preference <Preference>' -ForEach @(
@{ Preference = $true }
@{ Preference = $false }
) {
$PSNativeCommandUseErrorActionPreference = $Preference
{ Invoke-AzJson @('deployment', 'group', 'validate', '--parameters', 'mock-secret') } |
Should -Throw '*exit code 1*RequestDisallowedByAzure*locationineligible*'
$PSNativeCommandUseErrorActionPreference | Should -Be $Preference
}
It 'parses successful JSON without mixing in native stderr' {
$cliState.Command = '[Console]::Error.WriteLine("notice"); [Console]::Out.WriteLine(''{"state":"ready"}''); exit 0'
Mock Write-Warning {}
(Invoke-AzJson @('account', 'show')).state | Should -Be 'ready'
Should -Invoke Write-Warning -Times 1 -Exactly -ParameterFilter { $Message -eq 'notice' }
}
It 'preserves empty successful output' {
$cliState.Command = 'exit 0'
Invoke-AzJson @('account', 'set') | Should -BeNullOrEmpty
}
It 'does not print command arguments or invalid JSON stdout on failure' {
$cliState.Command = '[Console]::Out.WriteLine("mock-sensitive-output"); exit 0'
$failure = try { Invoke-AzJson @('deployment', 'group', 'create', '--parameters', 'mock-secret') } catch { $_ }
$failure.Exception.Message | Should -Match 'invalid JSON'
$failure.Exception.Message | Should -Not -Match 'mock-sensitive-output|mock-secret'
}
It 'does not accept valid JSON from a command that failed' {
$cliState.Command = '[Console]::Out.WriteLine(''{"state":"ready"}''); exit 17'
{ Invoke-AzJson @('deployment', 'group', 'validate') } | Should -Throw '*exit code 17*'
}
Context 'Shared region loop' {
BeforeEach {
@'
param($Location)
Invoke-AzJson @('deployment', 'group', 'validate', '--location', $Location) | Out-Null
[pscustomobject]@{ ProvisioningState = 'Submitted'; Location = $Location }
'@ | Set-Content (Join-Path $TestDrive 'deploy-localbox.ps1')
$localBoxLocations = @('first-region', 'second-region')
$azureLocalResourceProviderObjectIds = @('test-object-id')
$localBoxDeployment = $null
Mock az {
$cliState.Attempts++
if ($cliState.Attempts -eq 2) {
& $nativeExecutable -NoProfile -NonInteractive -Command 'exit 0'
}
else {
& $nativeExecutable -NoProfile -NonInteractive -Command $cliState.Command
}
}
}
It 'tries the next region after the actual native regional failure' {
. $fallback
$localBoxDeployment.ProvisioningState | Should -Be 'Submitted'
$localBoxDeployment.Location | Should -Be 'second-region'
Should -Invoke az -Times 2 -Exactly
}
It 'stops without retrying <Code>' -ForEach @(
@{ Code = 'RequestDisallowedByPolicy' }
@{ Code = 'AuthorizationFailed' }
@{ Code = 'UnexpectedFailure' }
) {
$cliState.Command = "[Console]::Error.WriteLine('$Code'); exit 1"
{ . $fallback } | Should -Throw "*$Code*"
Should -Invoke az -Times 1 -Exactly
}
It 'preserves the regional failure when no fallback regions remain' {
$localBoxLocations = @('first-region')
{ . $fallback } | Should -Throw '*RequestDisallowedByAzure*locationineligible*'
Should -Invoke az -Times 1 -Exactly
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ authorized test of real network access through the load-balancer address.

The LocalBox registration-region contract is checked offline with
`Invoke-Pester ./prepare-localbox.tests.ps1 -FullName 'LocalBox registration region contract*' -Output Detailed`.
It verifies that both deployment entry points default registration to West Europe,
It verifies that both deployment entry points default registration to Australia East,
the shared hook passes that region independently of host-region selection, and
all four Challenge 1 policy parameter examples include it. These checks make no
all four Challenge 1 policy parameter examples include the Australia East lab-only
exception while retaining the four European regions. These checks make no
Azure changes; a fresh Console deployment and participant VM creation remain
required to validate regional service availability and effective inherited policies.

Expand Down
Loading
Loading