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 @@ -3,7 +3,7 @@
"groups": [],
"deploymentType": "resourcegroup",
"labsPerSubscription": 2,
"preferredLocation": "swedencentral, spaincentral",
"preferredLocation": "spaincentral, swedencentral",
"estimatedDailyCostsUsd": 45.0,
"estimatedSharedDeploymentDailyCostsUsd": 110.0
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand All @@ -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`.

<a href="./img/reference-vm-mde-extension.png"><img src="./img/reference-vm-mde-extension.png" alt="sov-ref-vm Extensions page showing MDE.Windows with provisioning status Succeeded." width="800" /></a>

**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.

<a href="./img/reference-vm-defender-recommendations.png"><img src="./img/reference-vm-defender-recommendations.png" alt="sov-ref-vm Security page showing Defender for Servers On, zero security alerts, and a populated recommendations table with 153 results." width="800" /></a>

**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.

<a href="./img/reference-vm-update-assessment.png"><img src="./img/reference-vm-update-assessment.png" alt="sov-ref-vm Updates page showing periodic assessment enabled, five available updates comprising zero critical, two security and three other updates, and a last-assessed timestamp." width="800" /></a>

**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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -276,23 +276,88 @@ 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])?$') {
throw 'VM computer name must contain 1-15 letters, digits or internal hyphens, and cannot be all numeric.'
}
$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
Expand Down Expand Up @@ -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) {
Expand All @@ -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 = @{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)' }
Expand Down
Loading
Loading