diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/labautomation/lab-defaults.json b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/labautomation/lab-defaults.json
index e650ed256..220905345 100644
--- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/labautomation/lab-defaults.json
+++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/labautomation/lab-defaults.json
@@ -3,7 +3,7 @@
"groups": [],
"deploymentType": "resourcegroup",
"labsPerSubscription": 2,
- "preferredLocation": "swedencentral, spaincentral",
+ "preferredLocation": "spaincentral, swedencentral",
"estimatedDailyCostsUsd": 45.0,
"estimatedSharedDeploymentDailyCostsUsd": 110.0
}
diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/img/reference-vm-defender-recommendations.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/img/reference-vm-defender-recommendations.png
new file mode 100644
index 000000000..566712eb9
Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/img/reference-vm-defender-recommendations.png differ
diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/img/reference-vm-mde-extension.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/img/reference-vm-mde-extension.png
new file mode 100644
index 000000000..dbc74628e
Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/img/reference-vm-mde-extension.png differ
diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/img/reference-vm-update-assessment.png b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/img/reference-vm-update-assessment.png
new file mode 100644
index 000000000..89672d765
Binary files /dev/null and b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/img/reference-vm-update-assessment.png differ
diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/readme.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/readme.md
index c74989a08..9e38c9026 100644
--- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/readme.md
+++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/localbox/readme.md
@@ -34,7 +34,7 @@ If the nested account was rotated, pass a current `PSCredential` through `-NodeC
The script prompts for the Entra group object ID, or accepts `-AksAdminGroupObjectId`. Subscription, resource group and configuration path default from Jumpstart's machine environment. Explicit scope values must match this Client's deployment. Run only one preparation session at a time.
-The reference VM is **enabled by default**; pass `-IncludeReferenceVm:$false` to skip its preparation. Operation confirmation prompts are off under PowerShell's default confirmation preference, including storage changes and recovery of a matching Failed image. Use `-Confirm` to request confirmations, or `-Confirm:$false` if your session has a stricter preference. `-WhatIf` and all configuration/health safety checks remain active. Group-ID and first-creation credential prompts are separate; supply `-AksAdminGroupObjectId` and `-ReferenceVmCredential` to avoid those inputs.
+The reference VM is **enabled by default**; pass `-IncludeReferenceVm:$false` to skip its preparation. Operation confirmation prompts are off under PowerShell's default confirmation preference, including storage changes and recovery of a matching Failed image. Use `-Confirm` to request confirmations, or `-Confirm:$false` if your session has a stricter preference. `-WhatIf` and all configuration/health safety checks remain active. Supply `-AksAdminGroupObjectId` to avoid the group-ID prompt; reference-VM credentials are generated without prompting.
Preparation runs in this order: **storage -> logical networks -> AKS, proxy access and MetalLB -> VM image -> reference VM (unless disabled)**. A late Marketplace image failure therefore leaves the networks and AKS resources in place for the next run to reuse; it does not roll them back. The final manifest is written only after the requested preparation finishes.
@@ -155,11 +155,13 @@ On an **existing LocalBox**, copy the updated [prepare-localbox.ps1](../prepare-
Supply the existing AKS group ID when prompted, or pass `-AksAdminGroupObjectId`. Preserve any original nondefault image/network/AKS settings. This is an idempotent preparation rerun, **not** a new LocalBox deployment. It still validates/reconciles the normal shared resources; do not use `-SkipAks` just to bypass those checks.
-Creation prompts for a **separate local administrator credential**, not the nested domain administrator. Retain it securely for organizer maintenance; alternatively supply `-ReferenceVmCredential (Get-Credential -UserName 'localadmin')`. The password is passed through a restricted temporary ARM parameter file, never through CLI arguments or the manifest, and the file is removed after submission. Do not share the credential with participants.
+First creation automatically generates a strong random password for **`localadmin`**, separate from the nested domain administrator, and saves the credential to `C:\LocalBox\credentials\sov-ref-vm\credential.clixml` on LocalBox-Client **before** submitting the VM. The file uses Windows DPAPI encryption in a restricted directory; decryption requires the same Windows account on the same Client. A creation retry reuses this saved credential, rather than rotating it. Unreadable files or records for a different resource stop creation without replacing the saved credential.
+
+No password input is needed. For an explicit organizer-managed credential, supply `-ReferenceVmCredential`; supplied credentials are not saved automatically. The password is passed through a restricted temporary ARM parameter file, never through CLI arguments, logs or the manifest, and that temporary file is removed after submission. Do not share the credential with participants.
The script requests guest management and **periodic update assessment** (`AutomaticByPlatform`); it does not install patches, toggle Defender or force MDE installation. Defender for Servers P2 and Endpoint protection must already be enabled, and the VM needs outbound access to Arc, Defender and its configured update source. Resources carry `SecurityControl=Ignore`, `CostControl=Ignore` and `MicroHackPurpose=LocalBoxReferenceVM`.
-Matching resources are reused without password resets, resizing or redeployment. Conflicting/partial resources cause an explicit failure for organizer inspection; they are never deleted or overwritten. Reruns with `-IncludeReferenceVm:$false` neither manage nor delete the VM, and preserve its existing manifest entry (including the original observation timestamp).
+Matching resources are reused without password resets, resizing or redeployment. Reusing an existing VM does not create a credential file: its original password remains unchanged. WhatIf and opt-out also leave credential storage untouched. NIC validation checks the subnet and single IP configuration, without requiring the optional IP-configuration name that Azure Local can omit from responses. Rerun preparation to refresh the manifest after the older `Missing resource.properties.ipConfigurations[0].name` error; do not delete the running VM. Conflicting/partial resources cause an explicit failure for organizer inspection; they are never deleted or overwritten. Reruns with `-IncludeReferenceVm:$false` neither manage nor delete the VM, and preserve its existing manifest entry (including the original observation timestamp).
### Reference readiness and participant use
@@ -174,6 +176,22 @@ Before the workshop, verify:
The script intentionally leaves Defender assessment and update-result readiness as `NotVerified`; these portal checks remain required even after automated health tests pass. See the [Defender readiness checklist](manual-preparation.md#defender-for-servers-readiness-organizer).
+The following examples show three separate readiness milestones on `sov-ref-vm`.
+
+
+
+**Extension installation:** `MDE.Windows` shows **Succeeded** after automatic provisioning. Installation is asynchronous and may finish after the preparation script. This confirms successful extension installation, not ongoing Defender device reporting or completed security/update assessments.
+
+
+
+**Populated security findings:** the reference VM's Security page shows **Microsoft Defender for Servers On**, **153 recommendation results** and **zero security alerts**. Recommendations identify configuration or security improvements; they are not the same as threat alerts. The count and findings are examples, not a required lab outcome, and this view does not prove that every assessment has completed or that Update Manager has assessed the VM.
+
+The successful extension screenshot was shared on **21 October 2026**, and populated recommendations were observed the following morning, **22 October**. These are observation times, not a measured processing duration: the findings may have appeared earlier between checks. Prepare the reference VM the day before the event and verify the checklist rather than assuming a fixed wait guarantees readiness.
+
+
+
+**Update assessment results:** the Updates page shows **Periodic assessment: Yes** and **5 available updates**: **0 critical**, **2 security** and **3 other**. The displayed **Last assessed: 22/10/2026, 08:03:46** provides a timestamp for the results; the screenshot does not identify its time zone. Counts and available updates vary with the image and assessment date. These are assessment results, not evidence that updates were installed or that automatic patch installation is configured. Inspect **History** to confirm the assessment status if needed; participants must not install or schedule updates on the shared reference VM.
+
Participants still create and assess **their own VM**. While waiting, they may inspect this reference VM's existing security and update results. **Do not modify, assess, patch, restart, remediate or delete the shared reference VM.** This is an instruction, **not an RBAC-enforced read-only boundary**: existing shared resource-group VM Contributor grants remain unchanged. Verify participant visibility before the event; do not grant access to LocalBox-Client or publish the reference administrator credential.
## Optional storage consolidation
diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/prepare-localbox.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/prepare-localbox.ps1
index 38ce54429..ca72bce6d 100644
--- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/prepare-localbox.ps1
+++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/prepare-localbox.ps1
@@ -15,7 +15,7 @@ It also installs the MetalLB Arc extension and an ARP pool from the configured
service VIP reservation. The Runtime provider object ID comes from the organizer,
not a Graph lookup using the Client managed identity.
IncludeReferenceVm defaults to true and creates sov-ref-vm for participant inspection,
-with a separate local credential, guest management and periodic update assessment.
+with a generated, locally encrypted credential, guest management and periodic update assessment.
Operations run without confirmation under the default PowerShell preference;
use Confirm to request prompts or WhatIf to preview changes.
Assumes the standard greenfield LocalBox network configuration. Configured address
@@ -276,6 +276,80 @@ function New-LocalBoxVmTemplate {
}
}
+function Initialize-LocalBoxPrivateDirectory {
+ param([Parameter(Mandatory)][string]$Path)
+ if (Test-Path -LiteralPath $Path) {
+ $item = Get-Item -LiteralPath $Path -Force -ErrorAction Stop
+ if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
+ throw "Expected a private directory, not a file or link: $Path"
+ }
+ }
+ elseif ($IsWindows) {
+ New-Item -ItemType Directory -Path $Path -ErrorAction Stop | Out-Null
+ }
+ else {
+ [IO.Directory]::CreateDirectory($Path, [IO.UnixFileMode]::UserRead -bor [IO.UnixFileMode]::UserWrite -bor [IO.UnixFileMode]::UserExecute) | Out-Null
+ }
+ if ($IsWindows) {
+ $identity = [Security.Principal.WindowsIdentity]::GetCurrent().User
+ $acl = [Security.AccessControl.DirectorySecurity]::new()
+ $acl.SetOwner($identity)
+ $acl.SetAccessRuleProtection($true, $false)
+ $rule = [Security.AccessControl.FileSystemAccessRule]::new($identity, 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow')
+ $acl.SetAccessRule($rule)
+ Set-Acl -LiteralPath $Path -AclObject $acl -ErrorAction Stop
+ }
+ else {
+ [IO.File]::SetUnixFileMode($Path, [IO.UnixFileMode]::UserRead -bor [IO.UnixFileMode]::UserWrite -bor [IO.UnixFileMode]::UserExecute)
+ }
+}
+
+function New-LocalBoxVmCredential {
+ $password = 'Lb!' + [Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(30)) + '9a'
+ try {
+ $secure = [Security.SecureString]::new()
+ foreach ($character in $password.GetEnumerator()) { $secure.AppendChar($character) }
+ $secure.MakeReadOnly()
+ return [pscredential]::new('localadmin', $secure)
+ }
+ finally { $password = $null }
+}
+
+function Get-LocalBoxReferenceVmCredential {
+ param(
+ [Parameter(Mandatory)][string]$MachineId,
+ [string]$Path = 'C:\LocalBox\credentials\sov-ref-vm\credential.clixml'
+ )
+ if (-not $IsWindows) { throw 'Reference-VM credential persistence requires Windows DPAPI on LocalBox-Client.' }
+ $directory = Split-Path -Parent $Path
+ Initialize-LocalBoxPrivateDirectory $directory
+ if (Test-Path -LiteralPath $Path) {
+ if ((Get-Item -LiteralPath $Path -Force -ErrorAction Stop).Attributes -band [IO.FileAttributes]::ReparsePoint) {
+ throw "Refusing to load a linked credential file: $Path"
+ }
+ try { $saved = Import-Clixml -LiteralPath $Path -ErrorAction Stop }
+ catch { throw "Cannot read or decrypt $Path. Use the same Windows account on this LocalBox-Client that created it. No credential was replaced." }
+ if ($saved.MachineId -ine $MachineId -or $saved.Credential -isnot [pscredential] -or
+ $saved.Credential.UserName -ne 'localadmin' -or $saved.Credential.Password.Length -eq 0) {
+ throw "Saved reference-VM credential in $Path is invalid or belongs to a different resource. No credential was replaced."
+ }
+ Write-Host "Reusing the encrypted reference-VM credential in $Path."
+ return $saved.Credential
+ }
+ $credential = New-LocalBoxVmCredential
+ $temporary = Join-Path $directory "$([guid]::NewGuid()).tmp"
+ try {
+ @{ MachineId = $MachineId; Credential = $credential } |
+ Export-Clixml -LiteralPath $temporary -Depth 5 -ErrorAction Stop
+ [IO.File]::Move($temporary, $Path, $false)
+ }
+ finally {
+ if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force -ErrorAction Stop }
+ }
+ Write-Host "Generated reference-VM administrator credential; encrypted copy saved to $Path."
+ return $credential
+}
+
function Submit-LocalBoxVM {
param([hashtable]$State, [hashtable]$Entry, [pscredential]$Credential, [hashtable]$Tags, [switch]$EnablePeriodicAssessment)
if ($Entry.Name -notmatch '^(?=.{1,15}$)(?=.*[a-zA-Z])[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$') {
@@ -283,16 +357,7 @@ function Submit-LocalBoxVM {
}
$directory = Join-Path ([IO.Path]::GetTempPath()) "lbvm-$([guid]::NewGuid())"
try {
- if ($IsWindows) {
- New-Item -ItemType Directory -Path $directory -ErrorAction Stop | Out-Null
- $acl = Get-Acl -LiteralPath $directory
- $acl.SetAccessRuleProtection($true, $false)
- $identity = [Security.Principal.WindowsIdentity]::GetCurrent().User
- $rule = [Security.AccessControl.FileSystemAccessRule]::new($identity, 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow')
- $acl.SetAccessRule($rule)
- Set-Acl -LiteralPath $directory -AclObject $acl -ErrorAction Stop
- }
- else { [IO.Directory]::CreateDirectory($directory, [IO.UnixFileMode]::UserRead -bor [IO.UnixFileMode]::UserWrite -bor [IO.UnixFileMode]::UserExecute) | Out-Null }
+ Initialize-LocalBoxPrivateDirectory $directory
$templatePath = Join-Path $directory 'vm.json'
New-LocalBoxVmTemplate -EnablePeriodicAssessment:$EnablePeriodicAssessment | ConvertTo-Json -Depth 30 |
Set-Content -LiteralPath $templatePath -ErrorAction Stop
@@ -379,7 +444,7 @@ function Sync-LocalBoxReferenceVm {
$machineExpected = @{ kind = 'HCI'; location = $Location; tags = $tags; identity = @{ type = 'SystemAssigned' }
properties = @{ osProfile = @{ windowsConfiguration = @{ patchSettings = @{ assessmentMode = 'AutomaticByPlatform' } } } } }
$nicExpected = @{ location = $Location; extendedLocation = @{ name = $CustomLocationId }; tags = $tags
- properties = @{ ipConfigurations = @(@{ name = 'ipconfig1'; properties = @{ subnet = @{ id = $NetworkId } } }) } }
+ properties = @{ ipConfigurations = @(@{ properties = @{ subnet = @{ id = $NetworkId } } }) } }
$resources = @(Invoke-LocalBoxAz @('resource', 'list', '--subscription', $Settings.SubscriptionId, '--resource-group', $Settings.ResourceGroupName))
$existing = @($resources | Where-Object id -ieq $machineId)
if ($existing.Count) {
@@ -394,7 +459,7 @@ function Sync-LocalBoxReferenceVm {
elseif ($PSCmdlet.ShouldProcess($machineId, 'Create shared reference VM (2 vCPU, 4 GiB RAM, guest management and periodic update assessment)')) {
$credential = $Settings.ReferenceVmCredential
if (-not $credential) {
- $credential = Get-Credential -UserName 'localadmin' -Message 'Separate local administrator for sov-ref-vm; retain securely and do not share with participants'
+ $credential = Get-LocalBoxReferenceVmCredential -MachineId $machineId
}
if (-not $credential) { throw 'A separate reference-VM credential is required; nested domain credentials are not reused.' }
$state = @{
diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/test-localbox-capacity.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/test-localbox-capacity.ps1
index 206fd7d8f..1bbc87f49 100644
--- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/test-localbox-capacity.ps1
+++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/test-localbox-capacity.ps1
@@ -340,12 +340,7 @@ function Invoke-LocalBoxCapacityRun {
Repair-LocalBoxCapacityPendingSubmission $state
if (-not $Settings.VmCredential -and @($state.VMs).Count -lt $Settings.VmCount) {
if ($Settings.GenerateVmCredential) {
- $password = 'Lb!' + [Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(30)) + '9a'
- $secure = [Security.SecureString]::new()
- foreach ($character in $password.GetEnumerator()) { $secure.AppendChar($character) }
- $secure.MakeReadOnly()
- $Settings.VmCredential = [pscredential]::new('localadmin', $secure)
- $password = $null
+ $Settings.VmCredential = New-LocalBoxVmCredential
Write-Host 'Generated a disposable test-VM password in memory. It is not saved; use a supplied VmCredential if interactive sign-in is needed.'
}
else { $Settings.VmCredential = Get-Credential -UserName 'localadmin' -Message 'Local administrator for disposable capacity-test VMs (not the LocalBox administrator)' }
diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/prepare-localbox.tests.ps1 b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/prepare-localbox.tests.ps1
index 4950a7b2b..122b66a0f 100644
--- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/prepare-localbox.tests.ps1
+++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/prepare-localbox.tests.ps1
@@ -506,6 +506,95 @@ Describe 'LocalBox capacity secure submission' {
}
}
+Describe 'LocalBox generated credentials' {
+ It 'generates distinct complex read-only credentials without persistence' {
+ $first = New-LocalBoxVmCredential
+ $second = New-LocalBoxVmCredential
+ $first.UserName | Should -Be 'localadmin'
+ $first.Password.IsReadOnly() | Should -BeTrue
+ $password = $first.GetNetworkCredential().Password
+ $password.Length | Should -Be 45
+ $password | Should -Match '[A-Z]'
+ $password | Should -Match '[a-z]'
+ $password | Should -Match '[0-9]'
+ $password | Should -Match '[^a-zA-Z0-9]'
+ $password | Should -Not -Be $second.GetNetworkCredential().Password
+ }
+}
+
+Describe 'LocalBox encrypted credential persistence' {
+ BeforeEach {
+ $originalWindows = $IsWindows
+ Set-Variable IsWindows $true -Force
+ $path = Join-Path $TestDrive "$([guid]::NewGuid())/credential.clixml"
+ $machineId = '/subscriptions/test/resourceGroups/shared/providers/Microsoft.HybridCompute/machines/sov-ref-vm'
+ $credential = New-LocalBoxVmCredential
+ $saved = @{ MachineId = $machineId; Credential = $credential }
+ Mock Initialize-LocalBoxPrivateDirectory { New-Item -ItemType Directory -Path $Path -Force | Out-Null }
+ Mock New-LocalBoxVmCredential { $credential }
+ Mock Export-Clixml {
+ $InputObject.MachineId | Should -Be $machineId
+ $InputObject.Credential | Should -Be $credential
+ Set-Content -LiteralPath $LiteralPath -Value 'mock encrypted record'
+ }
+ Mock Import-Clixml { $saved }
+ }
+ AfterEach { Set-Variable IsWindows $originalWindows -Force }
+ It 'persists before returning and reuses the same credential on retry' {
+ Get-LocalBoxReferenceVmCredential $machineId $path | Should -Be $credential
+ Test-Path -LiteralPath $path | Should -BeTrue
+ Get-LocalBoxReferenceVmCredential $machineId $path | Should -Be $credential
+ Should -Invoke New-LocalBoxVmCredential -Times 1 -Exactly
+ Should -Invoke Export-Clixml -Times 1 -Exactly
+ Should -Invoke Import-Clixml -Times 1 -Exactly
+ @(Get-ChildItem (Split-Path $path) -Filter '*.tmp').Count | Should -Be 0
+ }
+ It 'rejects non-Windows persistence before accessing files' {
+ Set-Variable IsWindows $false -Force
+ { Get-LocalBoxReferenceVmCredential $machineId $path } | Should -Throw '*requires Windows DPAPI*'
+ Should -Invoke Initialize-LocalBoxPrivateDirectory -Times 0
+ }
+ It 'rejects an invalid saved record without replacing it' -TestCases @(
+ @{ Invalid = 'machine' }; @{ Invalid = 'type' }; @{ Invalid = 'username' }; @{ Invalid = 'empty' }
+ ) {
+ param($Invalid)
+ Get-LocalBoxReferenceVmCredential $machineId $path | Out-Null
+ switch ($Invalid) {
+ 'machine' { $saved.MachineId = 'another-resource' }
+ 'type' { $saved.Credential = 'not-a-credential' }
+ 'username' { $saved.Credential = [pscredential]::new('wrong-user', $credential.Password) }
+ 'empty' { $saved.Credential = [pscredential]::new('localadmin', [Security.SecureString]::new()) }
+ }
+ { Get-LocalBoxReferenceVmCredential $machineId $path } | Should -Throw '*invalid or belongs to a different resource*'
+ Should -Invoke Export-Clixml -Times 1 -Exactly
+ Get-Content $path | Should -Be 'mock encrypted record'
+ }
+ It 'fails closed on decryption errors without disclosing underlying details' {
+ Get-LocalBoxReferenceVmCredential $machineId $path | Out-Null
+ Mock Import-Clixml { throw 'sensitive underlying detail' }
+ { Get-LocalBoxReferenceVmCredential $machineId $path } | Should -Throw '*same Windows account*No credential was replaced*'
+ Should -Invoke Export-Clixml -Times 1 -Exactly
+ }
+ It 'cleans partial output and surfaces persistence failure' {
+ Mock Export-Clixml {
+ Set-Content -LiteralPath $LiteralPath -Value 'partial'
+ throw 'write failed'
+ }
+ { Get-LocalBoxReferenceVmCredential $machineId $path } | Should -Throw '*write failed*'
+ Test-Path $path | Should -BeFalse
+ @(Get-ChildItem (Split-Path $path)).Count | Should -Be 0
+ }
+ It 'never overwrites a credential created concurrently' {
+ Mock Export-Clixml {
+ Set-Content -LiteralPath $LiteralPath -Value 'temporary'
+ Set-Content -LiteralPath (Join-Path (Split-Path $LiteralPath) 'credential.clixml') -Value 'existing record'
+ }
+ { Get-LocalBoxReferenceVmCredential $machineId $path } | Should -Throw
+ Get-Content $path | Should -Be 'existing record'
+ @(Get-ChildItem (Split-Path $path) -Filter '*.tmp').Count | Should -Be 0
+ }
+}
+
Describe 'LocalBox preparation defaults' {
It 'does not expose the removed manual address-reservation acknowledgement' {
(Get-Command "$PSScriptRoot/../prepare-localbox.ps1").Parameters.ContainsKey('AddressReservationsConfirmed') | Should -BeFalse
@@ -604,7 +693,7 @@ Describe 'Optional LocalBox reference VM' {
}
}
$nic = @{ location = 'westeurope'; extendedLocation = @{ name = 'custom' }; tags = $tags
- properties = @{ ipConfigurations = @(@{ name = 'ipconfig1'; properties = @{ subnet = @{ id = 'network' } } }) } }
+ properties = @{ ipConfigurations = @(@{ properties = @{ subnet = @{ id = 'network' } } }) } }
$referenceArguments = @{ Settings = $settings; Location = 'westeurope'; CustomLocationId = 'custom'; ImageId = 'image'; NetworkId = 'network'; StorageId = 'storage' }
Mock Invoke-LocalBoxAz { @() }
Mock Get-LocalBoxResource {
@@ -620,18 +709,21 @@ Describe 'Optional LocalBox reference VM' {
}
Mock Get-LocalBoxArmCollection { @() }
Mock Submit-LocalBoxVM {}
- Mock Get-Credential { [pscredential]::new('localadmin', (ConvertTo-SecureString 'Mock!Password9-not-real' -AsPlainText -Force)) }
+ Mock Get-Credential { throw 'Unexpected interactive credential prompt' }
+ Mock Get-LocalBoxReferenceVmCredential { [pscredential]::new('localadmin', (ConvertTo-SecureString 'Mock!Password9-not-real' -AsPlainText -Force)) }
}
It 'does nothing when explicitly disabled' {
$settings.IncludeReferenceVm = $false
Sync-LocalBoxReferenceVm @referenceArguments | Should -BeNullOrEmpty
Should -Invoke Invoke-LocalBoxAz -Times 0
Should -Invoke Get-Credential -Times 0
+ Should -Invoke Get-LocalBoxReferenceVmCredential -Times 0
Should -Invoke Submit-LocalBoxVM -Times 0
}
It 'previews without credentials, provisioning, polling or a success-shaped manifest' {
Sync-LocalBoxReferenceVm @referenceArguments -WhatIf | Should -BeNullOrEmpty
Should -Invoke Get-Credential -Times 0
+ Should -Invoke Get-LocalBoxReferenceVmCredential -Times 0
Should -Invoke Submit-LocalBoxVM -Times 0
Should -Invoke Wait-LocalBoxResource -Times 0
}
@@ -645,6 +737,10 @@ Describe 'Optional LocalBox reference VM' {
$reference.Readiness.DefenderAssessments | Should -Be 'NotVerified'
$reference.Readiness.UpdateAssessmentResults | Should -Be 'NotVerified'
$reference | ConvertTo-Json -Depth 30 | Should -Not -Match 'Mock!Password|adminPassword|localadmin'
+ Should -Invoke Get-LocalBoxReferenceVmCredential -Times 1 -Exactly -ParameterFilter {
+ $MachineId -eq '/subscriptions/test/resourceGroups/shared/providers/Microsoft.HybridCompute/machines/sov-ref-vm'
+ }
+ Should -Invoke Get-Credential -Times 0
Should -Invoke Submit-LocalBoxVM -Times 1 -Exactly -ParameterFilter {
$Entry.Name -eq 'sov-ref-vm' -and $Credential.UserName -eq 'localadmin' -and
$State.ResourceGroupName -eq 'shared' -and $State.ProcessorCount -eq 2 -and $State.MemoryMB -eq 4096 -and
@@ -655,6 +751,7 @@ Describe 'Optional LocalBox reference VM' {
$settings.ReferenceVmCredential = [pscredential]::new('refadmin', (ConvertTo-SecureString 'Mock!Password9-not-real' -AsPlainText -Force))
Sync-LocalBoxReferenceVm @referenceArguments | Out-Null
Should -Invoke Get-Credential -Times 0
+ Should -Invoke Get-LocalBoxReferenceVmCredential -Times 0
Should -Invoke Submit-LocalBoxVM -Times 1 -ParameterFilter { $Credential.UserName -eq 'refadmin' }
}
It 'reuses matching resources without resetting credentials or redeploying' {
@@ -662,6 +759,22 @@ Describe 'Optional LocalBox reference VM' {
Sync-LocalBoxReferenceVm @referenceArguments | Out-Null
Should -Invoke Submit-LocalBoxVM -Times 0
Should -Invoke Get-Credential -Times 0
+ Should -Invoke Get-LocalBoxReferenceVmCredential -Times 0
+ }
+ It 'does not submit when credential persistence fails' {
+ Mock Get-LocalBoxReferenceVmCredential { throw 'Credential persistence failed' }
+ { Sync-LocalBoxReferenceVm @referenceArguments } | Should -Throw '*Credential persistence failed*'
+ Should -Invoke Submit-LocalBoxVM -Times 0
+ }
+ It 'still rejects incorrect NIC subnet or extra IP configurations without requiring a name' -TestCases @(
+ @{ Conflict = 'subnet' }; @{ Conflict = 'count' }
+ ) {
+ param($Conflict)
+ Mock Invoke-LocalBoxAz { @(@{ id = $machineId }) }
+ if ($Conflict -eq 'subnet') { $nic.properties.ipConfigurations[0].properties.subnet.id = 'wrong-network' }
+ else { $nic.properties.ipConfigurations += $nic.properties.ipConfigurations[0] }
+ { Sync-LocalBoxReferenceVm @referenceArguments } | Should -Throw
+ Should -Invoke Submit-LocalBoxVM -Times 0
}
It 'rejects an unrelated machine at the stable name' {
Mock Invoke-LocalBoxAz { @(@{ id = $machineId }) }
diff --git a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/readme.md b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/readme.md
index 22970471b..7aaf77f5b 100644
--- a/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/readme.md
+++ b/03-Azure/01-03-Infrastructure/01_Sovereign_Cloud/resources/tests/readme.md
@@ -47,7 +47,9 @@ required to validate regional service availability and effective inherited polic
Run `Invoke-Pester ./prepare-localbox.tests.ps1 -Output Detailed` for the offline
preparation and capacity regression suite. Reference-VM tests cover default inclusion, explicit opt-out and WhatIf,
-the 15-character Windows name limit, separate credentials, resource reuse and
+the 15-character Windows name limit, generated credentials, encrypted-file lifecycle
+(mocked offline), retry reuse, persistence failures, no credential access on
+reuse/WhatIf/opt-out, optional NIC response names with strict subnet/count checks, resource reuse and
conflicts, periodic assessment configuration, and pending/failed MDE status.
The shared VM submission tests check secure parameter-file transport and cleanup,
error redaction, and an ARM preview restricted to three new VM resources.
@@ -58,8 +60,17 @@ connection, MDE installation and periodic assessment mode. Older manifests witho
that optional entry retain the normal test set. These checks do **not** verify
ongoing Defender device reporting or completed Defender/Update Manager results;
perform the [organizer reference readiness checks](../localbox/readme.md#reference-readiness-and-participant-use)
-before the workshop. The new reference-VM path still requires a live Windows/LocalBox
-integration test; offline mocks are not deployment evidence.
+before the workshop.
+
+On 2026-10-21, read-only validation against an existing running reference VM passed
+with the IP-configuration name absent from the actual NIC response. Arc was
+Connected and periodic assessment was enabled; MDE was still Pending. Separately,
+Azure Run Command on LocalBox-Client verified real Windows DPAPI credential
+round-trip, restricted directory/file ACLs, unchanged retry reuse and rejection
+of a different resource ID. This test ran as SYSTEM against a disposable test
+file and removed it afterwards; it did not reset the reference VM's password.
+An interactive organizer-account run and fresh creation using the generated
+credential remain end-to-end integration checks.
## Preparation recovery and ordering checks