generalize: keepMachineSid, for a profile that survives an OS rebuild
sysprep /generalize regenerates the machine SID on every build, so an account built by one image does not match a profile left on a persistent disk by an earlier one -- different SID, so file ACLs, the NTUSER.DAT hive and ProfileList all mismatch, and the profile will not load. keepMachineSid drops /generalize and uses /oobe alone. The SID is then inherited from the cached base install derivation, which is content-addressed and so identical across every rebuild of the layers above it; the account, always RID 1000, comes out the same each time. A profile kept on a data disk then matches exactly, with no ownership or ProfileList fixups. Without /generalize the specialize pass does not run, so the profile relocation cannot ride the unattend there. It is written to the registry offline instead, before the build's OOBE, which virt-win-reg applies ahead of the Audit Mode boot. And because /generalize is also what strips MountedDevices, dropping it means the data disk keeps its drive letter into the shipped image -- the letterless-first-boot race that sent profiles temporary goes away at the root. Default is unchanged (/generalize), correct for an image deployed to many hosts; keepMachineSid is for an image that is always the same one machine. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117qMyjpuXsjpVAcpJbFD8g
This commit is contained in:
parent
5251bf7290
commit
f7405b8e70
1 changed files with 27 additions and 1 deletions
|
|
@ -42,6 +42,16 @@ in
|
||||||
# delayOobeRun = true: sysprep only, OOBE + activation on real hardware
|
# delayOobeRun = true: sysprep only, OOBE + activation on real hardware
|
||||||
# delayOobeRun = false: sysprep + OOBE + activation in build VM
|
# delayOobeRun = false: sysprep + OOBE + activation in build VM
|
||||||
delayOobeRun ? false,
|
delayOobeRun ? false,
|
||||||
|
# Skip sysprep's SID reset (use /oobe without /generalize), so every rebuild
|
||||||
|
# of the layers above the cached base install carries the same machine SID --
|
||||||
|
# and therefore the same account SID. A profile kept on a persistent disk then
|
||||||
|
# matches on file ACLs, its NTUSER.DAT hive, and ProfileList across rebuilds,
|
||||||
|
# with no ownership fixups. As a side effect MountedDevices survives too, so
|
||||||
|
# the data disk keeps its drive letter without a boot-time reassign.
|
||||||
|
#
|
||||||
|
# Correct only for an image that is always this one machine; a fleet that
|
||||||
|
# deploys the same image to many hosts wants the default generalization.
|
||||||
|
keepMachineSid ? false,
|
||||||
}: let
|
}: let
|
||||||
# Convert "8e8cd8" hex to "142 140 216" decimal RGB for Windows registry
|
# Convert "8e8cd8" hex to "142 140 216" decimal RGB for Windows registry
|
||||||
hexToRgbStr = hex: let
|
hexToRgbStr = hex: let
|
||||||
|
|
@ -229,6 +239,17 @@ in
|
||||||
<ProfilesDirectory>${profilesDirectory}</ProfilesDirectory>
|
<ProfilesDirectory>${profilesDirectory}</ProfilesDirectory>
|
||||||
</FolderLocations>'';
|
</FolderLocations>'';
|
||||||
|
|
||||||
|
# ProfilesDirectory as an offline .reg merge, for the keepMachineSid path
|
||||||
|
# where the specialize pass (and its FolderLocations) does not run. virt-win-reg
|
||||||
|
# applies this before the Audit Mode boot, so it is in place when OOBE creates
|
||||||
|
# the account. Backslashes are doubled for .reg syntax.
|
||||||
|
profileListRegistry = lib.optionalString (keepMachineSid && profilesDirectory != null) ''
|
||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList]
|
||||||
|
"ProfilesDirectory"="${builtins.replaceStrings [''\''] [''\\''] profilesDirectory}"
|
||||||
|
'';
|
||||||
|
|
||||||
dataDiskXml = lib.optionalString (dataDisk != null) ''
|
dataDiskXml = lib.optionalString (dataDisk != null) ''
|
||||||
<!-- Runs during specialize, before the first profile is created -->
|
<!-- Runs during specialize, before the first profile is created -->
|
||||||
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64"
|
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64"
|
||||||
|
|
@ -434,6 +455,11 @@ ${folderLocationsXml}
|
||||||
in {
|
in {
|
||||||
name = if delayOobeRun then "generalize-delay-oobe" else "generalize";
|
name = if delayOobeRun then "generalize-delay-oobe" else "generalize";
|
||||||
inherit nicModel;
|
inherit nicModel;
|
||||||
|
# With keepMachineSid the specialize pass never runs (see the sysprep line),
|
||||||
|
# so the profile relocation cannot ride the unattend there. It is written to
|
||||||
|
# the registry offline instead, before the build's OOBE creates the profile,
|
||||||
|
# so the account still lands on the data volume. Empty otherwise.
|
||||||
|
windowsRegistry = profileListRegistry;
|
||||||
# The blank disk is attached for the Audit Mode boot itself, so the disk-init
|
# The blank disk is attached for the Audit Mode boot itself, so the disk-init
|
||||||
# command and the profile relocation both happen under OOBE in the build VM.
|
# command and the profile relocation both happen under OOBE in the build VM.
|
||||||
# That is what makes delayOobeRun unnecessary: nothing is left to do on real
|
# That is what makes delayOobeRun unnecessary: nothing is left to do on real
|
||||||
|
|
@ -472,7 +498,7 @@ in {
|
||||||
:: clears MountedDevices.
|
:: clears MountedDevices.
|
||||||
call C:\vmix-init-data-disk.cmd
|
call C:\vmix-init-data-disk.cmd
|
||||||
''}
|
''}
|
||||||
C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe ${if delayOobeRun then "/shutdown" else "/reboot"} /quiet /unattend:C:\oobe-unattend.xml
|
C:\Windows\System32\Sysprep\sysprep.exe ${lib.optionalString (!keepMachineSid) "/generalize "}/oobe ${if delayOobeRun then "/shutdown" else "/reboot"} /quiet /unattend:C:\oobe-unattend.xml
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue