windows/seal: per-VM account on the config medium, not baked
username/password move from the sealed image to makeConfigMedium, so two
VMs get distinct logins (and, as before, distinct SIDs). The sealed
image bakes only a generic bootstrap account ("vmixsetup") whose sole job
is to carry OOBE to a logon; post-oobe then creates the real account from
the config CD, switches autologon to it, and reboots so it builds its own
SID-bound profile on D:\Users\<username>. A one-shot cleanup (RunOnce,
first logon of the real account) retires the bootstrap and its profile
and applies the per-user tint. So nothing about the account is shared or
baked, and the on-disk profile folder matches the real username.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117qMyjpuXsjpVAcpJbFD8g
This commit is contained in:
parent
ee002586e5
commit
1510b6c5ff
3 changed files with 62 additions and 1 deletions
|
|
@ -20,6 +20,12 @@
|
||||||
{
|
{
|
||||||
name ? "vmix-config",
|
name ? "vmix-config",
|
||||||
hostname ? "",
|
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\<username>, and the bootstrap
|
||||||
|
# is retired. Distinct per VM -- nothing about the account is shared/baked.
|
||||||
|
username ? "",
|
||||||
|
password ? "",
|
||||||
# { address; prefixLength; gateway; dns = [ ... ]; }
|
# { address; prefixLength; gateway; dns = [ ... ]; }
|
||||||
staticIP ? null,
|
staticIP ? null,
|
||||||
timezone ? null,
|
timezone ? null,
|
||||||
|
|
@ -37,6 +43,8 @@ let
|
||||||
configPs1 = pkgs.writeText "vmix-config.ps1" ''
|
configPs1 = pkgs.writeText "vmix-config.ps1" ''
|
||||||
# vmix per-VM config -- generated, read by the sealed image's baked scripts.
|
# vmix per-VM config -- generated, read by the sealed image's baked scripts.
|
||||||
$VmixHostname = '${hostname}'
|
$VmixHostname = '${hostname}'
|
||||||
|
${lib.optionalString (username != "") "$VmixUsername = '${username}'"}
|
||||||
|
${lib.optionalString (username != "") "$VmixPassword = '${password}'"}
|
||||||
${lib.optionalString (staticIP != null) ''
|
${lib.optionalString (staticIP != null) ''
|
||||||
$VmixIpAddress = '${staticIP.address}'
|
$VmixIpAddress = '${staticIP.address}'
|
||||||
$VmixPrefixLength = ${toString staticIP.prefixLength}
|
$VmixPrefixLength = ${toString staticIP.prefixLength}
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,16 @@ in rec {
|
||||||
# delivered at deploy time on a config medium (helpers/makeConfigMedium.nix).
|
# 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
|
# 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:).
|
# 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 ({
|
seal = templateArgs: generalize ({
|
||||||
delayOobeRun = true;
|
delayOobeRun = true;
|
||||||
configMedium = true;
|
configMedium = true;
|
||||||
|
username = "vmixsetup";
|
||||||
|
password = "vmixsetup";
|
||||||
profilesDirectory = "D:\\Users";
|
profilesDirectory = "D:\\Users";
|
||||||
dataDisk = { driveLetter = "D"; label = "data"; };
|
dataDisk = { driveLetter = "D"; label = "data"; };
|
||||||
} // templateArgs);
|
} // templateArgs);
|
||||||
|
|
|
||||||
|
|
@ -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\<username>, 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.
|
# Thin launcher, so the scheduled task has a cmd to point at.
|
||||||
staticIPScript = pkgs.writeText "vmix-static-ip.cmd" ''
|
staticIPScript = pkgs.writeText "vmix-static-ip.cmd" ''
|
||||||
@echo off
|
@echo off
|
||||||
|
|
@ -545,6 +583,13 @@ in
|
||||||
reg delete "HKLM\SYSTEM\Setup" /v OOBEInProgress /f 2>nul
|
reg delete "HKLM\SYSTEM\Setup" /v OOBEInProgress /f 2>nul
|
||||||
reg delete "HKLM\SYSTEM\Setup" /v CmdLine /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\<username>.
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-create-user.ps1 > C:\Windows\Temp\vmix-create-user.log 2>&1
|
||||||
|
''}
|
||||||
:: Clean up
|
:: Clean up
|
||||||
del /q C:\oobe-unattend.xml 2>nul
|
del /q C:\oobe-unattend.xml 2>nul
|
||||||
del /q C:\vmix-audit-script.cmd 2>nul
|
del /q C:\vmix-audit-script.cmd 2>nul
|
||||||
|
|
@ -671,6 +716,8 @@ in {
|
||||||
++ lib.optionals configMedium [
|
++ lib.optionals configMedium [
|
||||||
{ source = loadConfigScript; dest = "/vmix-load-config.cmd"; }
|
{ source = loadConfigScript; dest = "/vmix-load-config.cmd"; }
|
||||||
{ source = applyConfigScript; dest = "/vmix-apply-config.ps1"; }
|
{ 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) [
|
++ lib.optionals (staticIP != null || configMedium) [
|
||||||
{ source = staticIPScript; dest = "/vmix-static-ip.cmd"; }
|
{ source = staticIPScript; dest = "/vmix-static-ip.cmd"; }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue