diff --git a/lib/images/windows/helpers/makeConfigMedium.nix b/lib/images/windows/helpers/makeConfigMedium.nix index e3c10cd..8a90be1 100644 --- a/lib/images/windows/helpers/makeConfigMedium.nix +++ b/lib/images/windows/helpers/makeConfigMedium.nix @@ -20,6 +20,12 @@ { name ? "vmix-config", hostname ? "", + # The per-VM account. The sealed image carries a generic bootstrap account + # (only there to carry OOBE); on first boot this real account is created from + # here, gets the SID-bound profile on D:\Users\, and the bootstrap + # is retired. Distinct per VM -- nothing about the account is shared/baked. + username ? "", + password ? "", # { address; prefixLength; gateway; dns = [ ... ]; } staticIP ? null, timezone ? null, @@ -37,6 +43,8 @@ let configPs1 = pkgs.writeText "vmix-config.ps1" '' # vmix per-VM config -- generated, read by the sealed image's baked scripts. $VmixHostname = '${hostname}' + ${lib.optionalString (username != "") "$VmixUsername = '${username}'"} + ${lib.optionalString (username != "") "$VmixPassword = '${password}'"} ${lib.optionalString (staticIP != null) '' $VmixIpAddress = '${staticIP.address}' $VmixPrefixLength = ${toString staticIP.prefixLength} diff --git a/lib/images/windows/templates/default.nix b/lib/images/windows/templates/default.nix index a5a60fa..8bf1a10 100644 --- a/lib/images/windows/templates/default.nix +++ b/lib/images/windows/templates/default.nix @@ -43,10 +43,16 @@ in rec { # delivered at deploy time on a config medium (helpers/makeConfigMedium.nix). # One sealed store path is shared by every VM; each VM's first boot mints its # own SID and builds the whole profile on the relocated data volume (D:). - # Forces only the structural bits -- account, RDP and locale stay caller args. + # + # The baked account is a generic bootstrap that only exists to carry OOBE to a + # logon -- the real, per-VM account (username/password) comes from the config + # medium, and the bootstrap is retired on the target. So nothing per-VM is + # baked. RDP and locale stay caller args. seal = templateArgs: generalize ({ delayOobeRun = true; configMedium = true; + username = "vmixsetup"; + password = "vmixsetup"; profilesDirectory = "D:\\Users"; dataDisk = { driveLetter = "D"; label = "data"; }; } // templateArgs); diff --git a/lib/images/windows/templates/generalize.nix b/lib/images/windows/templates/generalize.nix index b710e0d..9a54b4c 100644 --- a/lib/images/windows/templates/generalize.nix +++ b/lib/images/windows/templates/generalize.nix @@ -202,6 +202,44 @@ in } ''; + # Creates the real per-VM account from the config and hands the machine over + # to it. The sealed image bakes only a generic bootstrap account (${username}) + # -- enough to carry OOBE to a logon so this can run -- and the real account + # is made here, on the target, from the CD. Autologon is switched to it and a + # one-shot cleanup is armed; the post-oobe reboot then lets the real account + # log in and build its own SID-bound profile on D:\Users\, after + # which the bootstrap is retired. So the account, like the SID, is per-VM and + # nothing about it is shared or baked. Runs as the bootstrap user in post-oobe. + createUserScript = pkgs.writeText "vmix-create-user.ps1" '' + if (-not (Test-Path C:\vmix-config.ps1)) { exit 0 } + . C:\vmix-config.ps1 + if (-not $VmixUsername) { exit 0 } + if ($VmixUsername -ieq '${username}') { exit 0 } + & net user $VmixUsername $VmixPassword /add + & net localgroup Administrators $VmixUsername /add + $w = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' + Set-ItemProperty $w -Name AutoAdminLogon -Value '1' + Set-ItemProperty $w -Name DefaultUserName -Value $VmixUsername + Set-ItemProperty $w -Name DefaultPassword -Value $VmixPassword + Remove-ItemProperty $w -Name DefaultDomainName -ErrorAction SilentlyContinue + # Fires at the real account's first logon (HKLM RunOnce = next user to log + # on), i.e. after the reboot below, once the bootstrap is no longer in use. + Set-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' ` + -Name vmixUserCleanup -Value 'cmd /c C:\vmix-user-cleanup.cmd' -Type String + ''; + + # Runs once as the real account (RunOnce, after the hand-over reboot): retire + # the bootstrap account and its profile, and apply the per-user desktop tint + # (which the bootstrap ran against on the first boot, before this account + # existed). Bootstrap is idle here, so its profile is safe to remove. + userCleanupScript = pkgs.writeText "vmix-user-cleanup.cmd" '' + @echo off + powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_UserProfile | Where-Object { $_.LocalPath -like '*\${username}' } | Remove-CimInstance -ErrorAction SilentlyContinue" + net user ${username} /delete >nul 2>&1 + powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-apply-config.ps1 >nul 2>&1 + del /q C:\vmix-user-cleanup.cmd 2>nul + ''; + # Thin launcher, so the scheduled task has a cmd to point at. staticIPScript = pkgs.writeText "vmix-static-ip.cmd" '' @echo off @@ -545,6 +583,13 @@ in reg delete "HKLM\SYSTEM\Setup" /v OOBEInProgress /f 2>nul reg delete "HKLM\SYSTEM\Setup" /v CmdLine /f 2>nul ''} + ${lib.optionalString configMedium '' + :: Runs last, as the generic bootstrap account: create the real per-VM + :: account from the config, switch autologon to it and arm the cleanup. The + :: reboot below then logs the real account in for the first time, building + :: its SID-bound profile on D:\Users\. + powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-create-user.ps1 > C:\Windows\Temp\vmix-create-user.log 2>&1 + ''} :: Clean up del /q C:\oobe-unattend.xml 2>nul del /q C:\vmix-audit-script.cmd 2>nul @@ -671,6 +716,8 @@ in { ++ lib.optionals configMedium [ { source = loadConfigScript; dest = "/vmix-load-config.cmd"; } { source = applyConfigScript; dest = "/vmix-apply-config.ps1"; } + { source = createUserScript; dest = "/vmix-create-user.ps1"; } + { source = userCleanupScript; dest = "/vmix-user-cleanup.cmd"; } ] ++ lib.optionals (staticIP != null || configMedium) [ { source = staticIPScript; dest = "/vmix-static-ip.cmd"; }