@a11ign/screenreader-fleet 0.0.0-reserved.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +94 -2
- package/dist/capture-client.d.mts +49 -0
- package/dist/capture-client.d.mts.map +1 -0
- package/dist/capture-client.mjs +352 -0
- package/dist/capture-client.mjs.map +1 -0
- package/dist/check-worker-code.d.mts +34 -0
- package/dist/check-worker-code.d.mts.map +1 -0
- package/dist/check-worker-code.mjs +173 -0
- package/dist/check-worker-code.mjs.map +1 -0
- package/dist/cli-flags.d.mts +71 -0
- package/dist/cli-flags.d.mts.map +1 -0
- package/dist/cli-flags.mjs +207 -0
- package/dist/cli-flags.mjs.map +1 -0
- package/dist/code-drift.d.mts +140 -0
- package/dist/code-drift.d.mts.map +1 -0
- package/dist/code-drift.mjs +284 -0
- package/dist/code-drift.mjs.map +1 -0
- package/dist/command-line-census.d.mts +33 -0
- package/dist/command-line-census.d.mts.map +1 -0
- package/dist/command-line-census.mjs +96 -0
- package/dist/command-line-census.mjs.map +1 -0
- package/dist/compare-workers.d.mts +3 -0
- package/dist/compare-workers.d.mts.map +1 -0
- package/dist/compare-workers.mjs +332 -0
- package/dist/compare-workers.mjs.map +1 -0
- package/dist/control-plane-isolation.d.mts +45 -0
- package/dist/control-plane-isolation.d.mts.map +1 -0
- package/dist/control-plane-isolation.mjs +67 -0
- package/dist/control-plane-isolation.mjs.map +1 -0
- package/dist/deploy-worker.d.mts +3 -0
- package/dist/deploy-worker.d.mts.map +1 -0
- package/dist/deploy-worker.mjs +333 -0
- package/dist/deploy-worker.mjs.map +1 -0
- package/dist/doctor.d.mts +216 -0
- package/dist/doctor.d.mts.map +1 -0
- package/dist/doctor.mjs +962 -0
- package/dist/doctor.mjs.map +1 -0
- package/dist/fleet-consistency.d.mts +235 -0
- package/dist/fleet-consistency.d.mts.map +1 -0
- package/dist/fleet-consistency.mjs +436 -0
- package/dist/fleet-consistency.mjs.map +1 -0
- package/dist/fleet-env.d.mts +228 -0
- package/dist/fleet-env.d.mts.map +1 -0
- package/dist/fleet-env.mjs +509 -0
- package/dist/fleet-env.mjs.map +1 -0
- package/dist/fleet-scripts.d.mts +11 -0
- package/dist/fleet-scripts.d.mts.map +1 -0
- package/dist/fleet-scripts.mjs +41 -0
- package/dist/fleet-scripts.mjs.map +1 -0
- package/dist/git-safe-env.d.mts +10 -0
- package/dist/git-safe-env.d.mts.map +1 -0
- package/dist/git-safe-env.mjs +44 -0
- package/dist/git-safe-env.mjs.map +1 -0
- package/dist/guest-run.d.mts +26 -0
- package/dist/guest-run.d.mts.map +1 -0
- package/dist/guest-run.mjs +164 -0
- package/dist/guest-run.mjs.map +1 -0
- package/dist/host-address.d.mts +33 -0
- package/dist/host-address.d.mts.map +1 -0
- package/dist/host-address.mjs +105 -0
- package/dist/host-address.mjs.map +1 -0
- package/dist/host-capacity.d.mts +64 -0
- package/dist/host-capacity.d.mts.map +1 -0
- package/dist/host-capacity.mjs +152 -0
- package/dist/host-capacity.mjs.map +1 -0
- package/dist/host-metrics.d.mts +116 -0
- package/dist/host-metrics.d.mts.map +1 -0
- package/dist/host-metrics.mjs +201 -0
- package/dist/host-metrics.mjs.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/local-vm.d.ts +125 -0
- package/dist/local-vm.d.ts.map +1 -0
- package/dist/local-vm.js +360 -0
- package/dist/local-vm.js.map +1 -0
- package/dist/measure-guard.d.mts +34 -0
- package/dist/measure-guard.d.mts.map +1 -0
- package/dist/measure-guard.mjs +73 -0
- package/dist/measure-guard.mjs.map +1 -0
- package/dist/normalise-fleet.d.mts +2 -0
- package/dist/normalise-fleet.d.mts.map +1 -0
- package/dist/normalise-fleet.mjs +76 -0
- package/dist/normalise-fleet.mjs.map +1 -0
- package/dist/npm-cli-executable.d.mts +42 -0
- package/dist/npm-cli-executable.d.mts.map +1 -0
- package/dist/npm-cli-executable.mjs +159 -0
- package/dist/npm-cli-executable.mjs.map +1 -0
- package/dist/probe-outcome.d.mts +89 -0
- package/dist/probe-outcome.d.mts.map +1 -0
- package/dist/probe-outcome.mjs +104 -0
- package/dist/probe-outcome.mjs.map +1 -0
- package/dist/protocol-guard.d.mts +34 -0
- package/dist/protocol-guard.d.mts.map +1 -0
- package/dist/protocol-guard.mjs +121 -0
- package/dist/protocol-guard.mjs.map +1 -0
- package/dist/source-walk.d.mts +12 -0
- package/dist/source-walk.d.mts.map +1 -0
- package/dist/source-walk.mjs +56 -0
- package/dist/source-walk.mjs.map +1 -0
- package/dist/transient-fault.d.mts +6 -0
- package/dist/transient-fault.d.mts.map +1 -0
- package/dist/transient-fault.mjs +86 -0
- package/dist/transient-fault.mjs.map +1 -0
- package/dist/utm-deprecated.d.mts +6 -0
- package/dist/utm-deprecated.d.mts.map +1 -0
- package/dist/utm-deprecated.mjs +23 -0
- package/dist/utm-deprecated.mjs.map +1 -0
- package/dist/worker-code-check.d.mts +29 -0
- package/dist/worker-code-check.d.mts.map +1 -0
- package/dist/worker-code-check.mjs +78 -0
- package/dist/worker-code-check.mjs.map +1 -0
- package/dist/worker-health.d.mts +56 -0
- package/dist/worker-health.d.mts.map +1 -0
- package/dist/worker-health.mjs +73 -0
- package/dist/worker-health.mjs.map +1 -0
- package/dist/worker-http.d.mts +103 -0
- package/dist/worker-http.d.mts.map +1 -0
- package/dist/worker-http.mjs +277 -0
- package/dist/worker-http.mjs.map +1 -0
- package/dist/worker-stats.d.mts +66 -0
- package/dist/worker-stats.d.mts.map +1 -0
- package/dist/worker-stats.mjs +143 -0
- package/dist/worker-stats.mjs.map +1 -0
- package/package.json +96 -4
- package/src/local-worker/autounattend.xml +280 -0
- package/src/local-worker/build-vm.sh +218 -0
- package/src/local-worker/clone-worker.sh +141 -0
- package/src/local-worker/create-utm-vm.sh +202 -0
- package/src/local-worker/fetch-windows-iso.sh +238 -0
- package/src/local-worker/first-boot.cmd +58 -0
- package/src/local-worker/worker-ctl.sh +442 -0
- package/src/provisioning/README.md +28 -0
- package/src/provisioning/apply-foreground-lock-timeout.ps1 +71 -0
- package/src/provisioning/bare-metal/README.md +213 -0
- package/src/provisioning/bare-metal/a11y-bootstrap.service +58 -0
- package/src/provisioning/bare-metal/autounattend.xml +428 -0
- package/src/provisioning/bare-metal/serve-bootstrap.sh +86 -0
- package/src/provisioning/bootstrap-control-plane.sh +463 -0
- package/src/provisioning/bootstrap-windows-worker.ps1 +649 -0
- package/src/provisioning/build-lean-worker-image.ps1 +275 -0
- package/src/provisioning/diagnose-nvda-worker.ps1 +174 -0
- package/src/provisioning/provision-nvda-worker.ps1 +827 -0
- package/src/provisioning/set-display-mode.ps1 +411 -0
- package/src/provisioning/stamp-provision-revision.ps1 +184 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Unattended install for a BARE-METAL x64 capture worker, served from the PXE box.
|
|
4
|
+
|
|
5
|
+
The sibling at ../../local-worker/autounattend.xml is the proven recipe, and this is that recipe
|
|
6
|
+
adapted for real hardware on a real LAN. Four differences, each of which matters:
|
|
7
|
+
|
|
8
|
+
1. NO PLAINTEXT PASSWORD. The arm64 file embeds `witness`/`witness` and justifies it as "a disposable
|
|
9
|
+
local VM behind QEMU user-mode networking, reachable solely through forwarded ports". None of that
|
|
10
|
+
is true of a mini PC on your LAN, so the justification does not travel with the file. The account
|
|
11
|
+
is created with a BLANK password instead, which is the same design the rest of the fleet already
|
|
12
|
+
depends on: `LimitBlankPasswordUse=1` (a Windows default) confines a blank-password account to
|
|
13
|
+
CONSOLE logon, so it cannot be reached over SMB, RDP or password SSH at all. That makes the account
|
|
14
|
+
strictly narrower than a password would, not wider — and nothing secret is ever committed here.
|
|
15
|
+
Provisioning re-asserts and VERIFIES that invariant afterwards; see provision-nvda-worker.ps1.
|
|
16
|
+
|
|
17
|
+
2. NO VIRTIO DRIVERS. Those paths point at UTM's support ISO (`E:\Drivers\...`) and exist because a
|
|
18
|
+
QEMU guest has no inbox driver for its own disk or NIC. Real hardware does. Leaving them in would
|
|
19
|
+
fail the windowsPE pass looking for a drive that is not there.
|
|
20
|
+
|
|
21
|
+
3. ComputerName is `*`, not `A11Y-WORKER`. Twelve machines answering to one NetBIOS name is a name
|
|
22
|
+
conflict on a real network; the arm64 file could hardcode it because there is one VM behind NAT.
|
|
23
|
+
Workers are identified by address in ansible/inventory.yml, so a generated name costs nothing.
|
|
24
|
+
|
|
25
|
+
4. The TPM/SecureBoot/CPU bypasses are KEPT, and here they are load-bearing rather than a VM
|
|
26
|
+
workaround: the 6th and 7th generation boxes in this fleet (EliteDesk 800 G2/G3) are not on
|
|
27
|
+
Windows 11's supported-CPU list at all, and would refuse to install. They are harmless on the
|
|
28
|
+
8th-gen machines that would pass anyway.
|
|
29
|
+
|
|
30
|
+
============================ THIS WIPES DISK 0, WITHOUT ASKING ============================
|
|
31
|
+
`WillWipeDisk` on DiskID 0 is what makes the install hands-off, and it is why this file must only
|
|
32
|
+
ever be served to a machine you intend to become a worker. Point your PXE server's boot entry at
|
|
33
|
+
the machines you have set aside, not at a default-for-everything entry.
|
|
34
|
+
==========================================================================================
|
|
35
|
+
|
|
36
|
+
UAC stays at its DEFAULT, deliberately, exactly as the arm64 file explains: with UAC off every
|
|
37
|
+
process runs at high integrity, and the capture pipeline depends on NVDA (nvda_noUIAccess.exe) and
|
|
38
|
+
the browser sitting at the SAME integrity level. Provisioning gets elevation from a RunLevel Highest
|
|
39
|
+
scheduled task, which elevates with no prompt — and a prompt would be fatal, since UAC dialogs render
|
|
40
|
+
on the secure desktop where no automation can reach them.
|
|
41
|
+
-->
|
|
42
|
+
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
|
43
|
+
|
|
44
|
+
<settings pass="windowsPE">
|
|
45
|
+
<!-- The locale MUST be one the INSTALL MEDIA actually carries, and this is the defect that made
|
|
46
|
+
every unattended install fall back to a fully interactive one.
|
|
47
|
+
|
|
48
|
+
Our media is Win11_23H2_EnglishInternational_x64v2, whose volume label is
|
|
49
|
+
CCCOMA_X64FRE_EN-GB_DV9: it ships en-GB resources and no en-US ones. Asking for en-US
|
|
50
|
+
therefore fails at the point Setup tries to load them, and Setup's response is not an error
|
|
51
|
+
but a FALLBACK to the language-selection page. From the console that is indistinguishable
|
|
52
|
+
from the answer file never being read, which is how it was misdiagnosed repeatedly: the
|
|
53
|
+
edition name, the boot mode, iVentoy's injection and the payload server were each blamed and
|
|
54
|
+
each was fine.
|
|
55
|
+
|
|
56
|
+
The proof is in Setup's own log, which reports success right up to this point:
|
|
57
|
+
|
|
58
|
+
Found usable unattend file for pass [windowsPE] at [C:\autounattend.xml]
|
|
59
|
+
Successfully deserialized and validated unattend file
|
|
60
|
+
Product key is valid ... User accepted the EULA in the unattend file
|
|
61
|
+
SetSetupKeyboardLayout: Failed to set the key board layout to [en-US] <- here
|
|
62
|
+
CLanguages::v_InitLanguage - Failed to load en-US resources with error 2
|
|
63
|
+
|
|
64
|
+
Error 2 is FILE NOT FOUND. Check the media before changing these: the volume label encodes
|
|
65
|
+
the language, and `Callback_Locale_*` in X:\Windows\Panther\setupact.log names the failure. -->
|
|
66
|
+
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
67
|
+
<SetupUILanguage>
|
|
68
|
+
<UILanguage>en-GB</UILanguage>
|
|
69
|
+
</SetupUILanguage>
|
|
70
|
+
<InputLocale>en-GB</InputLocale>
|
|
71
|
+
<SystemLocale>en-GB</SystemLocale>
|
|
72
|
+
<UILanguage>en-GB</UILanguage>
|
|
73
|
+
<UserLocale>en-GB</UserLocale>
|
|
74
|
+
</component>
|
|
75
|
+
|
|
76
|
+
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
77
|
+
<Diagnostics>
|
|
78
|
+
<OptIn>false</OptIn>
|
|
79
|
+
</Diagnostics>
|
|
80
|
+
|
|
81
|
+
<!-- UEFI/GPT layout: ESP, MSR, then Windows across the remainder. -->
|
|
82
|
+
<DiskConfiguration>
|
|
83
|
+
<WillShowUI>OnError</WillShowUI>
|
|
84
|
+
<Disk wcm:action="add">
|
|
85
|
+
<DiskID>0</DiskID>
|
|
86
|
+
<WillWipeDisk>true</WillWipeDisk>
|
|
87
|
+
<CreatePartitions>
|
|
88
|
+
<CreatePartition wcm:action="add">
|
|
89
|
+
<Order>1</Order>
|
|
90
|
+
<Type>EFI</Type>
|
|
91
|
+
<Size>300</Size>
|
|
92
|
+
</CreatePartition>
|
|
93
|
+
<CreatePartition wcm:action="add">
|
|
94
|
+
<Order>2</Order>
|
|
95
|
+
<Type>MSR</Type>
|
|
96
|
+
<Size>16</Size>
|
|
97
|
+
</CreatePartition>
|
|
98
|
+
<CreatePartition wcm:action="add">
|
|
99
|
+
<Order>3</Order>
|
|
100
|
+
<Type>Primary</Type>
|
|
101
|
+
<Extend>true</Extend>
|
|
102
|
+
</CreatePartition>
|
|
103
|
+
</CreatePartitions>
|
|
104
|
+
<ModifyPartitions>
|
|
105
|
+
<ModifyPartition wcm:action="add">
|
|
106
|
+
<Order>1</Order>
|
|
107
|
+
<PartitionID>1</PartitionID>
|
|
108
|
+
<Format>FAT32</Format>
|
|
109
|
+
<Label>System</Label>
|
|
110
|
+
</ModifyPartition>
|
|
111
|
+
<ModifyPartition wcm:action="add">
|
|
112
|
+
<Order>2</Order>
|
|
113
|
+
<PartitionID>2</PartitionID>
|
|
114
|
+
</ModifyPartition>
|
|
115
|
+
<ModifyPartition wcm:action="add">
|
|
116
|
+
<Order>3</Order>
|
|
117
|
+
<PartitionID>3</PartitionID>
|
|
118
|
+
<Format>NTFS</Format>
|
|
119
|
+
<Label>Windows</Label>
|
|
120
|
+
<Letter>C</Letter>
|
|
121
|
+
</ModifyPartition>
|
|
122
|
+
</ModifyPartitions>
|
|
123
|
+
</Disk>
|
|
124
|
+
</DiskConfiguration>
|
|
125
|
+
|
|
126
|
+
<!-- /IMAGE/NAME must match an image NAME in the media's own install.wim EXACTLY, and the
|
|
127
|
+
value is a property of the ISO rather than of Windows. Read it, never assume it:
|
|
128
|
+
|
|
129
|
+
mount -o loop,ro <iso> /mnt
|
|
130
|
+
tail -c 200000 /mnt/sources/install.wim | tr -d '\0' | grep -oE '<NAME>[^<]*</NAME>'
|
|
131
|
+
|
|
132
|
+
This said "Windows 11 Professional". On Win11_23H2_EnglishInternational_x64v2 both NAME
|
|
133
|
+
and DESCRIPTION are "Windows 11 Pro", and "Professional" is Windows 7-era naming that
|
|
134
|
+
appears nowhere in that WIM, so it named no image at all.
|
|
135
|
+
|
|
136
|
+
HONESTY ABOUT WHAT THIS DID AND DID NOT CAUSE: it was found while chasing an interactive
|
|
137
|
+
install, and written up at the time as the cause of it. It was not — the locale mismatch
|
|
138
|
+
above was, proven afterwards from Setup's own log. This value was genuinely wrong and had
|
|
139
|
+
to be fixed, but it was never demonstrated to produce a symptom, because the locale bug
|
|
140
|
+
stopped Setup before the image was ever selected. Treat it as a latent defect corrected,
|
|
141
|
+
not as a diagnosis.
|
|
142
|
+
|
|
143
|
+
NOTE FOR THE NEXT EDITOR: an XML comment may not contain two consecutive hyphens. Using
|
|
144
|
+
them as a dash here broke the file a third time in this repo's history, and a malformed
|
|
145
|
+
answer file fails in exactly the same silent way as a wrong edition name. Validate with
|
|
146
|
+
`python3 -c "import xml.dom.minidom as m; m.parse('autounattend.xml')"` before pushing.
|
|
147
|
+
|
|
148
|
+
The generic key below is Microsoft's public KMS key for Pro, so this file always meant
|
|
149
|
+
Pro; only the spelling was wrong.
|
|
150
|
+
|
|
151
|
+
Keep this block byte-for-byte simple: substituting /IMAGE/INDEX here, or adding a
|
|
152
|
+
comment inside <InstallFrom>, made Setup reject the whole answer file the same way. -->
|
|
153
|
+
<ImageInstall>
|
|
154
|
+
<OSImage>
|
|
155
|
+
<InstallFrom>
|
|
156
|
+
<MetaData wcm:action="add">
|
|
157
|
+
<Key>/IMAGE/NAME</Key>
|
|
158
|
+
<Value>Windows 11 Pro</Value>
|
|
159
|
+
</MetaData>
|
|
160
|
+
</InstallFrom>
|
|
161
|
+
<InstallTo>
|
|
162
|
+
<DiskID>0</DiskID>
|
|
163
|
+
<PartitionID>3</PartitionID>
|
|
164
|
+
</InstallTo>
|
|
165
|
+
<WillShowUI>OnError</WillShowUI>
|
|
166
|
+
</OSImage>
|
|
167
|
+
</ImageInstall>
|
|
168
|
+
|
|
169
|
+
<!-- Not a VM workaround here: the 6th/7th-gen boxes in this fleet are not on Windows 11's
|
|
170
|
+
supported-CPU list and Setup refuses them outright without these. Harmless on 8th-gen. -->
|
|
171
|
+
<RunSynchronous>
|
|
172
|
+
<RunSynchronousCommand wcm:action="add">
|
|
173
|
+
<Order>1</Order>
|
|
174
|
+
<Path>reg add HKLM\System\Setup\LabConfig /v BypassCPUCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
175
|
+
</RunSynchronousCommand>
|
|
176
|
+
<RunSynchronousCommand wcm:action="add">
|
|
177
|
+
<Order>2</Order>
|
|
178
|
+
<Path>reg add HKLM\System\Setup\LabConfig /v BypassRAMCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
179
|
+
</RunSynchronousCommand>
|
|
180
|
+
<RunSynchronousCommand wcm:action="add">
|
|
181
|
+
<Order>3</Order>
|
|
182
|
+
<Path>reg add HKLM\System\Setup\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
183
|
+
</RunSynchronousCommand>
|
|
184
|
+
<RunSynchronousCommand wcm:action="add">
|
|
185
|
+
<Order>4</Order>
|
|
186
|
+
<Path>reg add HKLM\System\Setup\LabConfig /v BypassTPMCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
187
|
+
</RunSynchronousCommand>
|
|
188
|
+
<RunSynchronousCommand wcm:action="add">
|
|
189
|
+
<Order>5</Order>
|
|
190
|
+
<Path>reg add HKLM\System\Setup\LabConfig /v BypassDiskCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
191
|
+
</RunSynchronousCommand>
|
|
192
|
+
<!-- BypassStorageCheck is the name Setup actually reads; BypassDiskCheck was inherited from the
|
|
193
|
+
arm64 file and appears in no Microsoft documentation. Both are kept: an ignored registry
|
|
194
|
+
value costs nothing, and a missing one costs the install. -->
|
|
195
|
+
<RunSynchronousCommand wcm:action="add">
|
|
196
|
+
<Order>6</Order>
|
|
197
|
+
<Path>reg add HKLM\System\Setup\LabConfig /v BypassStorageCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
198
|
+
</RunSynchronousCommand>
|
|
199
|
+
<!-- The install-time bypasses above do not cover the UPGRADE path, which is what a later feature
|
|
200
|
+
update takes. Without this a box installs happily and then refuses 25H2 with "this PC doesn't
|
|
201
|
+
meet the minimum requirements", months later, on a machine nobody is watching. -->
|
|
202
|
+
<RunSynchronousCommand wcm:action="add">
|
|
203
|
+
<Order>7</Order>
|
|
204
|
+
<Path>reg add HKLM\System\Setup\MoSetup /v AllowUpgradesWithUnsupportedTPMOrCPU /t REG_DWORD /d 0x00000001 /f</Path>
|
|
205
|
+
</RunSynchronousCommand>
|
|
206
|
+
</RunSynchronous>
|
|
207
|
+
|
|
208
|
+
<UserData>
|
|
209
|
+
<AcceptEula>true</AcceptEula>
|
|
210
|
+
<FullName>a11ign</FullName>
|
|
211
|
+
<Organization>a11ign</Organization>
|
|
212
|
+
<!-- NO product key, deliberately, and the element is present-but-empty rather than absent.
|
|
213
|
+
`/IMAGE/NAME` above already selects the edition, which is the only job a key was doing
|
|
214
|
+
here: we do not want activation, and an unactivated Windows is fully functional for
|
|
215
|
+
capture.
|
|
216
|
+
|
|
217
|
+
It used to carry the generic Windows 11 Pro KMS client key
|
|
218
|
+
(W269N-WFGWX-YVC9B-4J6C9-T83GX). That worked while the media shipped a single
|
|
219
|
+
install.wim, and started failing with "the unattended answer file contains an invalid
|
|
220
|
+
product key" once install.wim was SPLIT into install.swm parts to fit the ISO's file-size
|
|
221
|
+
limit. Setup validates the key against the edition it resolved from the split image, and
|
|
222
|
+
the two stopped agreeing. Naming the edition and omitting the key removes the
|
|
223
|
+
disagreement rather than trying to satisfy both halves of it.
|
|
224
|
+
|
|
225
|
+
The `Key` element must be PRESENT and EMPTY, exactly like the blank password below.
|
|
226
|
+
Omitting it entirely fails differently and just as fatally: Setup stops with "Windows
|
|
227
|
+
cannot read the <ProductKey> setting from the unattend answer file". Empty means "no key,
|
|
228
|
+
do not ask"; absent means "malformed". `WillShowUI=Never` is what suppresses the "enter
|
|
229
|
+
your product key" page, which on a headless fleet is a hang nobody is there to clear. -->
|
|
230
|
+
<ProductKey>
|
|
231
|
+
<Key></Key>
|
|
232
|
+
<WillShowUI>Never</WillShowUI>
|
|
233
|
+
</ProductKey>
|
|
234
|
+
</UserData>
|
|
235
|
+
</component>
|
|
236
|
+
</settings>
|
|
237
|
+
|
|
238
|
+
<settings pass="specialize">
|
|
239
|
+
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
240
|
+
<!-- `*` generates a unique name. Twelve machines called A11Y-WORKER is a NetBIOS conflict on a
|
|
241
|
+
real network; the workers are addressed by IP from ansible/inventory.yml regardless. -->
|
|
242
|
+
<ComputerName>*</ComputerName>
|
|
243
|
+
<TimeZone>GMT Standard Time</TimeZone>
|
|
244
|
+
</component>
|
|
245
|
+
<!-- The windowsPE LabConfig keys live in the INSTALLER's registry and do not survive into the
|
|
246
|
+
installed system. Written again here so the running OS carries them, which is what a feature
|
|
247
|
+
update and any in-place repair will consult. Without this the box installs on unsupported
|
|
248
|
+
hardware and then quietly stops being able to update itself.
|
|
249
|
+
|
|
250
|
+
BypassNRO removes the "you must connect to the internet and sign in with a Microsoft account"
|
|
251
|
+
wall that 24H2 reintroduced at OOBE. The LocalAccount + HideOnlineAccountScreens below usually
|
|
252
|
+
carry it, but usually is not a property you want on a fleet you cannot see the screen of. -->
|
|
253
|
+
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
254
|
+
<RunSynchronous>
|
|
255
|
+
<RunSynchronousCommand wcm:action="add">
|
|
256
|
+
<Order>1</Order>
|
|
257
|
+
<Description>Persist the hardware bypasses into the installed OS</Description>
|
|
258
|
+
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassTPMCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
259
|
+
</RunSynchronousCommand>
|
|
260
|
+
<RunSynchronousCommand wcm:action="add">
|
|
261
|
+
<Order>2</Order>
|
|
262
|
+
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
263
|
+
</RunSynchronousCommand>
|
|
264
|
+
<RunSynchronousCommand wcm:action="add">
|
|
265
|
+
<Order>3</Order>
|
|
266
|
+
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassCPUCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
267
|
+
</RunSynchronousCommand>
|
|
268
|
+
<RunSynchronousCommand wcm:action="add">
|
|
269
|
+
<Order>4</Order>
|
|
270
|
+
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassRAMCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
271
|
+
</RunSynchronousCommand>
|
|
272
|
+
<RunSynchronousCommand wcm:action="add">
|
|
273
|
+
<Order>5</Order>
|
|
274
|
+
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassStorageCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
275
|
+
</RunSynchronousCommand>
|
|
276
|
+
<RunSynchronousCommand wcm:action="add">
|
|
277
|
+
<Order>6</Order>
|
|
278
|
+
<Description>Allow feature updates on unsupported TPM/CPU</Description>
|
|
279
|
+
<Path>reg add HKLM\SYSTEM\Setup\MoSetup /v AllowUpgradesWithUnsupportedTPMOrCPU /t REG_DWORD /d 0x00000001 /f</Path>
|
|
280
|
+
</RunSynchronousCommand>
|
|
281
|
+
<RunSynchronousCommand wcm:action="add">
|
|
282
|
+
<Order>7</Order>
|
|
283
|
+
<Description>No Microsoft account / network wall at OOBE</Description>
|
|
284
|
+
<Path>reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE /v BypassNRO /t REG_DWORD /d 0x00000001 /f</Path>
|
|
285
|
+
</RunSynchronousCommand>
|
|
286
|
+
<!-- THE BLANK PASSWORD MUST NOT BE ABLE TO EXPIRE, AND THIS IS THE ONLY PASS THAT CAN SAY SO.
|
|
287
|
+
#1933, measured 2026-09-22 while the chairman built the five new boxes from USB media: the
|
|
288
|
+
first one came up at "Your password has expired and must be changed" and would not log on.
|
|
289
|
+
An empty new password was accepted, so the account survived; nothing in the tree had stopped
|
|
290
|
+
it being asked.
|
|
291
|
+
|
|
292
|
+
Windows' default max password age is 42 days and it applies to the blank `witness` password
|
|
293
|
+
like any other. `account.yml` and `provision-nvda-worker.ps1` BOTH set
|
|
294
|
+
`PasswordNeverExpires`, but both run over SSH, which needs the box up, which needs
|
|
295
|
+
auto-logon, which is the thing the expiry breaks. Every existing remedy sits on the far side
|
|
296
|
+
of the door it locks.
|
|
297
|
+
|
|
298
|
+
`FirstLogonCommands` cannot fix it either, and that is the trap worth naming: an expired
|
|
299
|
+
password makes AUTO-LOGON ITSELF fail, so the box stops at an interactive prompt and the
|
|
300
|
+
first-logon commands never run at all.
|
|
301
|
+
|
|
302
|
+
So it is set here, in `specialize`, which runs during setup before any logon exists. As
|
|
303
|
+
POLICY rather than per-account, because at this point the account does not exist yet:
|
|
304
|
+
`oobeSystem` creates it below and inherits whatever policy is in force.
|
|
305
|
+
|
|
306
|
+
PXE boxes mostly dodged this because provisioning follows the install within minutes and
|
|
307
|
+
closes the window. A USB build is done by hand, over hours or days, from media that may have
|
|
308
|
+
been written weeks earlier, so the window is wide enough to land in. -->
|
|
309
|
+
<RunSynchronousCommand wcm:action="add">
|
|
310
|
+
<Order>8</Order>
|
|
311
|
+
<Description>A blank password that can expire is a box that cannot auto-logon</Description>
|
|
312
|
+
<Path>net accounts /maxpwage:unlimited</Path>
|
|
313
|
+
</RunSynchronousCommand>
|
|
314
|
+
<!-- Belt and braces, and NOT a restatement of the line above. `maxpwage` stops the clock for
|
|
315
|
+
accounts created from here on; this clears the expiry WARNING path that a Windows security
|
|
316
|
+
baseline or an OEM image may already have stamped into the image being installed. The first
|
|
317
|
+
machine showed the symptom on day zero, which `maxpwage` alone does not explain, so the two
|
|
318
|
+
are kept separate rather than merged into one command that would hide which one mattered. -->
|
|
319
|
+
<RunSynchronousCommand wcm:action="add">
|
|
320
|
+
<Order>9</Order>
|
|
321
|
+
<Description>No password expiry warning at logon</Description>
|
|
322
|
+
<Path>reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v PasswordExpiryWarning /t REG_DWORD /d 0x00000000 /f</Path>
|
|
323
|
+
</RunSynchronousCommand>
|
|
324
|
+
</RunSynchronous>
|
|
325
|
+
</component>
|
|
326
|
+
<component name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
327
|
+
<SkipAutoActivation>true</SkipAutoActivation>
|
|
328
|
+
</component>
|
|
329
|
+
<component name="Microsoft-Windows-SQMApi" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
330
|
+
<CEIPEnabled>0</CEIPEnabled>
|
|
331
|
+
</component>
|
|
332
|
+
</settings>
|
|
333
|
+
|
|
334
|
+
<settings pass="oobeSystem">
|
|
335
|
+
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
336
|
+
<InputLocale>en-GB</InputLocale>
|
|
337
|
+
<SystemLocale>en-GB</SystemLocale>
|
|
338
|
+
<UILanguage>en-GB</UILanguage>
|
|
339
|
+
<UserLocale>en-GB</UserLocale>
|
|
340
|
+
</component>
|
|
341
|
+
|
|
342
|
+
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
343
|
+
<UserAccounts>
|
|
344
|
+
<LocalAccounts>
|
|
345
|
+
<LocalAccount wcm:action="add">
|
|
346
|
+
<Name>witness</Name>
|
|
347
|
+
<DisplayName>witness</DisplayName>
|
|
348
|
+
<Group>Administrators</Group>
|
|
349
|
+
<!-- BLANK, and empty rather than absent: Setup wants the element. See the header — a
|
|
350
|
+
blank-password account is confined to console logon by LimitBlankPasswordUse, which
|
|
351
|
+
is what makes credential-free auto-logon safe on a networked box. -->
|
|
352
|
+
<Password>
|
|
353
|
+
<Value></Value>
|
|
354
|
+
<PlainText>true</PlainText>
|
|
355
|
+
</Password>
|
|
356
|
+
</LocalAccount>
|
|
357
|
+
</LocalAccounts>
|
|
358
|
+
</UserAccounts>
|
|
359
|
+
|
|
360
|
+
<!-- The whole reason this file exists. NVDA is a GUI application: it needs a real logged-on
|
|
361
|
+
desktop session. No auto-logon means no session after a reboot, which means captures
|
|
362
|
+
silently return nothing at all — and on a headless fleet nobody is there to log in. -->
|
|
363
|
+
<AutoLogon>
|
|
364
|
+
<Enabled>true</Enabled>
|
|
365
|
+
<Username>witness</Username>
|
|
366
|
+
<Password>
|
|
367
|
+
<Value></Value>
|
|
368
|
+
<PlainText>true</PlainText>
|
|
369
|
+
</Password>
|
|
370
|
+
<LogonCount>2147483647</LogonCount>
|
|
371
|
+
</AutoLogon>
|
|
372
|
+
|
|
373
|
+
<OOBE>
|
|
374
|
+
<HideEULAPage>true</HideEULAPage>
|
|
375
|
+
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
|
376
|
+
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
|
377
|
+
<HideLocalAccountScreen>true</HideLocalAccountScreen>
|
|
378
|
+
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
|
379
|
+
<ProtectYourPC>3</ProtectYourPC>
|
|
380
|
+
</OOBE>
|
|
381
|
+
|
|
382
|
+
<FirstLogonCommands>
|
|
383
|
+
<SynchronousCommand wcm:action="add">
|
|
384
|
+
<Order>1</Order>
|
|
385
|
+
<Description>Disable hibernation</Description>
|
|
386
|
+
<CommandLine>cmd /c POWERCFG -H OFF</CommandLine>
|
|
387
|
+
</SynchronousCommand>
|
|
388
|
+
<!-- Never leave the console locked: a locked desktop makes NVDA silent. Belt and braces
|
|
389
|
+
alongside provisioning, which also turns the screensaver off. -->
|
|
390
|
+
<SynchronousCommand wcm:action="add">
|
|
391
|
+
<Order>2</Order>
|
|
392
|
+
<Description>Disable screensaver (it steals foreground mid-capture)</Description>
|
|
393
|
+
<CommandLine>reg add "HKCU\Control Panel\Desktop" /v ScreenSaveActive /t REG_SZ /d 0 /f</CommandLine>
|
|
394
|
+
</SynchronousCommand>
|
|
395
|
+
<!-- Fetch the payload over HTTP rather than looking for it on the install media.
|
|
396
|
+
==================== SET THE ADDRESS BELOW BEFORE USE ====================
|
|
397
|
+
`$h` below is 203.0.113.10 — an RFC 5737 DOCUMENTATION address, not this fleet's real
|
|
398
|
+
control-plane host (#86: a real address sat here in tracked source; RFC 5737 is reserved so
|
|
399
|
+
it can never resolve to anything, the same replacement #83 used elsewhere in this repo).
|
|
400
|
+
It WILL fail to fetch, on purpose: a box that silently boots against a stale or wrong host
|
|
401
|
+
is a worse failure than one that visibly hangs retrying an address that cannot answer.
|
|
402
|
+
Replace it with your control plane's real address before generating install media.
|
|
403
|
+
|
|
404
|
+
iVentoy injects files to X:, which is the WinPE RAM disk and is GONE by the time this runs,
|
|
405
|
+
so scanning drive letters (which the UTM path does, from a support ISO that stays attached)
|
|
406
|
+
finds nothing here. Fetching also means one small file to serve rather than a repacked ISO,
|
|
407
|
+
and this project already has scar tissue from El Torito boot catalogues.
|
|
408
|
+
|
|
409
|
+
The box PXE-booted from this host moments ago, so it can certainly reach it.
|
|
410
|
+
`serve-bootstrap.sh` on the control plane serves exactly these two files. -->
|
|
411
|
+
<SynchronousCommand wcm:action="add">
|
|
412
|
+
<Order>3</Order>
|
|
413
|
+
<Description>Fetch the bootstrap payload from the control plane</Description>
|
|
414
|
+
<CommandLine>powershell -NoProfile -ExecutionPolicy Bypass -Command "$h='http://203.0.113.10:8099'; $d='C:\a11y'; New-Item -ItemType Directory -Force $d | Out-Null; foreach($f in 'first-boot.cmd','bootstrap-windows-worker.ps1','operator-key.pub'){ for($i=0;$i -lt 60;$i++){ try{ Invoke-WebRequest -UseBasicParsing -Uri ($h+'/'+$f) -OutFile ($d+'\'+$f); break }catch{ Start-Sleep -Seconds 5 } } }"</CommandLine>
|
|
415
|
+
</SynchronousCommand>
|
|
416
|
+
<!-- Then hand off. first-boot.cmd waits for DHCP, stages the operator key, and runs the bootstrap
|
|
417
|
+
elevated via a RunLevel Highest scheduled task, because a UAC prompt here would render on the secure
|
|
418
|
+
desktop where no automation can reach it. After this the box is reachable by Ansible and needs
|
|
419
|
+
no console visit at all. -->
|
|
420
|
+
<SynchronousCommand wcm:action="add">
|
|
421
|
+
<Order>4</Order>
|
|
422
|
+
<Description>Run the worker bootstrap elevated, without a UAC prompt</Description>
|
|
423
|
+
<CommandLine>cmd /c if exist C:\a11y\first-boot.cmd start /wait C:\a11y\first-boot.cmd</CommandLine>
|
|
424
|
+
</SynchronousCommand>
|
|
425
|
+
</FirstLogonCommands>
|
|
426
|
+
</component>
|
|
427
|
+
</settings>
|
|
428
|
+
</unattend>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Serve the three files a PXE-installing worker fetches at first logon.
|
|
3
|
+
#
|
|
4
|
+
# ./serve-bootstrap.sh ~/.ssh/a11y-witness_ed25519.pub
|
|
5
|
+
# ./serve-bootstrap.sh ~/.ssh/a11y-witness_ed25519.pub 8099
|
|
6
|
+
#
|
|
7
|
+
# Run it on the machine whose address is in autounattend.xml — normally the iVentoy host, because the
|
|
8
|
+
# box PXE-booted from there moments earlier and can certainly reach it.
|
|
9
|
+
#
|
|
10
|
+
# ## Why a fetch rather than files on the media
|
|
11
|
+
#
|
|
12
|
+
# iVentoy's file injection decompresses into **X:**, which is the WinPE RAM disk and is GONE by the time
|
|
13
|
+
# FirstLogonCommands runs. The UTM path can scan drive letters because its support ISO stays attached;
|
|
14
|
+
# nothing stays attached here. Rebuilding the install ISO to carry the files is the other option, and
|
|
15
|
+
# this project has already spent a day on El Torito boot catalogues and `0xc0000225` to earn the opinion
|
|
16
|
+
# that it is not worth it for three small files.
|
|
17
|
+
#
|
|
18
|
+
# ## The key is served, never committed
|
|
19
|
+
#
|
|
20
|
+
# A public key is not a secret, but it IS specific to whoever runs this fleet, and a checked-in one
|
|
21
|
+
# grants access to anyone with the repo. It is passed in here and served for the few minutes an install
|
|
22
|
+
# takes.
|
|
23
|
+
#
|
|
24
|
+
# ## Run it by hand, or as a service
|
|
25
|
+
#
|
|
26
|
+
# This serves an SSH public key and two scripts to anyone on the LAN who asks. That is a small exposure and
|
|
27
|
+
# a real one, so run by hand it should be Ctrl-C'd once the box is up.
|
|
28
|
+
#
|
|
29
|
+
# It used to say it was "deliberately NOT a service", and the fleet outgrew that. Started by hand it is a
|
|
30
|
+
# step somebody has to remember, and forgetting produces the worst failure this path has: Windows installs
|
|
31
|
+
# fine, the fetches retry for ~15 minutes, and the box sits at a desktop with no worker on it — which reads
|
|
32
|
+
# as a bad image. `a11y-bootstrap.service` beside this file runs it with Restart=always instead.
|
|
33
|
+
#
|
|
34
|
+
# If you do run it as a service, note the staging below: the payload is snapshotted at START, so a restart
|
|
35
|
+
# is what picks up a changed first-boot.cmd or bootstrap-windows-worker.ps1.
|
|
36
|
+
set -euo pipefail
|
|
37
|
+
|
|
38
|
+
KEY="${1:-}"
|
|
39
|
+
PORT="${2:-8099}"
|
|
40
|
+
|
|
41
|
+
if [ -z "$KEY" ] || [ ! -f "$KEY" ]; then
|
|
42
|
+
echo "usage: $0 <path-to-public-key> [port]" >&2
|
|
43
|
+
echo " e.g. $0 ~/.ssh/a11y-witness_ed25519.pub" >&2
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Refuse a PRIVATE key with the loudest message available. Handing one to every machine on the LAN is
|
|
48
|
+
# not a mistake anyone recovers from quietly, and the two filenames differ by four characters.
|
|
49
|
+
if grep -q "PRIVATE KEY" "$KEY"; then
|
|
50
|
+
echo "REFUSING: $KEY is a PRIVATE key. Serve the .pub, and rotate that key now." >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
if ! grep -qE '^(ssh-|ecdsa-)' "$KEY"; then
|
|
54
|
+
echo "REFUSING: $KEY does not look like an SSH public key (expected ssh-... or ecdsa-...)." >&2
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
59
|
+
STAGE="$(mktemp -d)"
|
|
60
|
+
trap 'rm -rf "$STAGE"' EXIT
|
|
61
|
+
|
|
62
|
+
cp "$HERE/../../local-worker/first-boot.cmd" "$STAGE/first-boot.cmd"
|
|
63
|
+
cp "$HERE/../bootstrap-windows-worker.ps1" "$STAGE/bootstrap-windows-worker.ps1"
|
|
64
|
+
cp "$KEY" "$STAGE/operator-key.pub"
|
|
65
|
+
|
|
66
|
+
# CRLF for the .cmd. A LF-only batch file run by cmd.exe fails in ways that name the wrong line, which is
|
|
67
|
+
# a debugging session nobody needs at 2am beside a mini PC.
|
|
68
|
+
perl -pi -e 's/\r?\n/\r\n/' "$STAGE/first-boot.cmd" 2>/dev/null || true
|
|
69
|
+
|
|
70
|
+
cat <<EOF
|
|
71
|
+
|
|
72
|
+
Serving on port $PORT:
|
|
73
|
+
first-boot.cmd
|
|
74
|
+
bootstrap-windows-worker.ps1
|
|
75
|
+
operator-key.pub ($(ssh-keygen -lf "$KEY" 2>/dev/null | awk '{print $2, $3}'))
|
|
76
|
+
|
|
77
|
+
autounattend.xml must point at THIS machine. Check the address in it matches one of:
|
|
78
|
+
$(command -v ip >/dev/null 2>&1 && ip -4 -o addr show scope global | awk '{print " http://" substr($4, 1, index($4, "/")-1) ":'"$PORT"'"}' \
|
|
79
|
+
|| ifconfig 2>/dev/null | awk '/inet /{if ($2 != "127.0.0.1") print " http://" $2 ":'"$PORT"'"}')
|
|
80
|
+
|
|
81
|
+
Ctrl-C once the worker is up — this is not a service.
|
|
82
|
+
|
|
83
|
+
EOF
|
|
84
|
+
|
|
85
|
+
cd "$STAGE"
|
|
86
|
+
python3 -m http.server "$PORT"
|