fix(install): worker isolation survives a folder swap: recorded settings, -Reapply, -Verify, swap script (#901)
v2-ci / unit-tests (push) Has been cancelled

An upgrade that swaps the install folder dropped the explicit ScriptWorker isolation ACEs on script-worker\, the
install folder and a copied-back pki\ (vd03 2026-11-13). Re-applying needed the operator to repeat the arguments.

- Install-ScriptWorkerIsolation.ps1 records its effective settings (InstallRoot, HostServiceAccount, PeCacheDir,
  HostServiceName, ConfigureHostService) beside DenyPaths under ...\Services\OtOpcUaScriptWorker\Parameters (new plan
  step SettingsRecord) and locks that key to SYSTEM + Administrators (protected DACL; no secrets recorded).
- -Reapply: re-apply from the record, no arguments; prints a summary line (changed/unchanged).
- -Verify: check only, [ok]/[MISSING] per step, exit 1 when anything is missing. -Uninstall also works from the record.
  A node isolated by a pre-#901 installer (DenyPaths only) must run the full-argument install once; both modes say so.
- Invoke-OtOpcUaInstallSwap.ps1: preflight -> stop -> move live/candidate -> carry appsettings/pki/logs ->
  -Reapply -> -Verify -> start; a failed -Verify leaves the service stopped. Also the rollback (-NoCarry).
- Tests: dry-run harness 101/101 (record + argument rules; record-key SDDL mutation turns 2 red). The Windows test adds
  swap -> Verify fails (exactly the 3 lost ACEs) -> Reapply (3 changed) -> Verify ok -> Reapply 12/12 unchanged, PE-cache
  and firewall negative controls, the swap script, and the pre-#901 refusal; 52/52 on windev under 5.1 and 7, with a
  scratch local host account and with SYSTEM. Evidence: scripts/validation/opcua-windows-service/evidence/2026-11-13-windev-901-reapply-verify.
- Docs: ServiceHosting isolation section; vd03 pack (swap step, rollback, post-deploy gate, one-time full-argument run),
  the 2026-11-13 vd03 record, and the QA pack (QA swap preceded the isolation install; ACLs checked intact over SMB on
  vq03/vq04 2026-11-13; next-deploy procedure).

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
Joseph Doherty
2026-11-13 02:06:10 -05:00
parent 2470f82eea
commit 2cb8a2c317
10 changed files with 748 additions and 36 deletions
@@ -0,0 +1,50 @@
# lmxopcua#901: re-apply and verify the ScriptWorker isolation after a folder swap (windev, 2026-11-13)
**Result: PASS on both PowerShell hosts.** `scripts/install/tests/Test-ScriptWorkerIsolation.Windows.ps1` (elevated, scratch
folder tree under `%TEMP%`, scratch worker/host service names, never touches `OtOpcUaHost`; windev has no OtOpcUa service and
no real isolation installed):
| Host | Host account | Assertions |
|---|---|---|
| Windows PowerShell 5.1.19041.6456 | scratch local user `.\otopcuaIsoTestAcct` (`-LocalHostAccount`, random password, deleted) | 52 passed, 0 failed |
| PowerShell 7 | scratch local user (`-LocalHostAccount`) | 52 passed, 0 failed |
| Windows PowerShell 5.1 | `NT AUTHORITY\SYSTEM` | 52 passed, 0 failed |
The scratch layout mirrors vd03: `pki\` inside the install folder and in `-DenyPaths`, the deployment folder around it denied
too. The "swap" is the deploy packs' sequence: build the candidate beside the live folder, move live aside, move the
candidate in, robocopy `pki\` (no ACLs) and copy `appsettings.json` back.
Transcript of the #901 part (Windows PowerShell 5.1, SYSTEM; the scratch path is shown as `<scratch>`):
```
Swap -> Verify fails -> Reapply -> Verify passes -> Reapply unchanged (lmxopcua#901)
Verify (installed) exit 0; 12 ok
Verify (after the swap) exit 1; MISSING: WorkerFolderAcl <scratch>\install\script-worker | InstallRootDeny <scratch>\install | SensitiveDirDeny <scratch>\install\pki
Reapply (no arguments) exit 0; 9 unchanged, 3 changed
Verify (after Reapply) exit 0; 12 ok
Reapply again exit 0; 12 unchanged
Negative control: Verify catches each kind of loss
Verify (cache ACL + rule broken) exit 1; MISSING: PeCacheDir <scratch>\pe-cache | FirewallBlockOutbound OtOpcUa-ScriptWorker-BlockOutbound
Verify (after Reapply) exit 0; 12 ok
Invoke-OtOpcUaInstallSwap.ps1: swap + Reapply + Verify in one step (no service control in the scratch test)
Invoke-OtOpcUaInstallSwap exit 0; VERIFY OK after its Reapply
Swap refuses a pre-#901 record (DenyPaths only) before changing anything
Invoke-OtOpcUaInstallSwap (pre-#901) exit 1; refused: settings not recorded
Reapply (pre-#901 record) exit 1;
Uninstall WITHOUT -DenyPaths (the record drives it), twice
```
- After the swap `-Verify` exits **1** and lists exactly the three ACEs the 2026-11-13 vd03 swap lost: `WorkerFolderAcl`
(`script-worker\`), `InstallRootDeny` (install folder), `SensitiveDirDeny` (`pki\`).
- `-Reapply` with no settings argument changes exactly those three (9 unchanged); `-Verify` then exits 0; a second `-Reapply`
reports 12/12 unchanged (`summary: 12 step(s), 0 changed, 12 unchanged`).
- Negative controls: a Users grant on the PE-cache root and a deleted firewall rule each show as `[MISSING]` (exit 1).
- `Invoke-OtOpcUaInstallSwap.ps1` (`-NoServiceControl`) swaps, carries `appsettings*.json` + `pki`, re-applies and verifies in
one run (exit 0, `VERIFY OK`); with a pre-#901 record (no `InstallRoot` value) it refuses before moving anything (exit 1,
candidate still in place, no previous-install folder), and `-Reapply` refuses with the "run the full-argument install once" message.
- The record key's DACL is `D:PAI(A;CI;KA;;;SY)(A;CI;KA;;;BA)`; the five settings read back.
- Cleanup verified afterwards: no firewall rule, no scratch services, no scratch local user, no scratch folder, no scratch
registry key; the staged scripts were removed from windev.
The dry-run harness (`pwsh -File scripts/install/tests/Test-ScriptWorkerIsolation.ps1`, macOS) passes 101/101; a mutation of the
record-key SDDL (unprotected + a Users read ACE) turns 2 assertions red.