From f7405b8e70b1c6e6527bfffe3782aec531ccf1fa Mon Sep 17 00:00:00 2001 From: Git Sagar Date: Thu, 10 Sep 2026 20:05:27 -0300 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_0117qMyjpuXsjpVAcpJbFD8g --- lib/images/windows/templates/generalize.nix | 28 ++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/images/windows/templates/generalize.nix b/lib/images/windows/templates/generalize.nix index b38811b..d5eafae 100644 --- a/lib/images/windows/templates/generalize.nix +++ b/lib/images/windows/templates/generalize.nix @@ -42,6 +42,16 @@ in # delayOobeRun = true: sysprep only, OOBE + activation on real hardware # delayOobeRun = false: sysprep + OOBE + activation in build VM 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 # Convert "8e8cd8" hex to "142 140 216" decimal RGB for Windows registry hexToRgbStr = hex: let @@ -229,6 +239,17 @@ in ${profilesDirectory} ''; + # 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) ''