Files
lmxopcua/scripts/validation/opcua-windows-service/README.md
T
Joseph Doherty ac4c4d40dc
v2-ci / unit-tests (push) Has been cancelled
test(validation): #800 Windows service gate harness + windev results (SDK 1.5 -> 2.0.1 in place, rollback)
Adds scripts/validation/opcua-windows-service/: a phased PowerShell gate (Invoke-ServiceGate.ps1)
that installs the known-good build as the real OtOpcUaHost service via Install-Services.ps1, seeds
LocalDb through the production cache API (Seeder, built against the baseline tree), upgrades in
place with config/PKI/LocalDb carried with their ACLs, starts/stops through the SCM, runs a strict
SDK 2 probe matrix (Sign + SignAndEncrypt, valid/wrong/unknown/disabled/anonymous keys, untrusted
client and server), exercises the ScriptWorker isolation, rolls back and cleans up.

windev 2026-11-12 (efb9197a -> 438484fd -> efb9197a): app cert CEF9D91F... reused byte-for-byte
with identical PKI SDDL on every start, all gating checks pass, 0 secrets in logs. Findings filed:
#879 (None endpoint cannot be dropped), #880 (SDK 2 client 65-char key vs SDK 1.5 server:
rollback/QA risk), #883 (Install-ScriptWorkerIsolation.ps1 fails on pwsh 7 and 5.1; runtime
isolation proven with the patch in the evidence directory).

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
2026-11-12 10:28:49 -05:00

81 lines
6.6 KiB
Markdown

# Windows service gate (lmxopcua#800)
This harness checks the SDK 2 upgrade on a real Windows node with the real `OtOpcUaHost` service. It installs a known-good SDK 1.5 build with
`scripts/install/Install-Services.ps1`, then upgrades that install in place to the SDK 2 candidate. Config, LocalDb and PKI carry
over with their ACLs. The service is then started and stopped twice, the ScriptWorker isolation installer is exercised, the
known-good build is restored, and the node is cleaned up. The results of the first run are in
[`evidence/2026-11-12-windev/RESULTS.md`](evidence/2026-11-12-windev/RESULTS.md).
**Where it may run.** Use an approved Windows node that has no `OtOpcUaHost` service, from an elevated local or SSH session
in PowerShell 7. The Wonder-box hold still applies: this is not a deploy tool for vd03/vq03/vq04.
## Parts
| Part | What it is |
|---|---|
| `Invoke-ServiceGate.ps1` | One phase per invocation (see its header). Everything lives under `-Root` (default `C:\OtOpcUa800`). |
| `Seeder/` | Writes a deployment artifact into the LocalDb that the service created, through the production `IDeploymentArtifactCache`. The artifact contains: one raw folder; one UNS area/line/equipment; a timer-driven virtual tag whose script is `return 800;`; three API keys (a 65-char enabled key, a 54-char enabled key, a disabled key); and read grants. It uses only APIs that exist at both SDK lines, so it is **built against the baseline tree** (copy the folder to the same path in a baseline checkout). |
| `Probe/` | An SDK 2.0.1 client with peer auto-accept off. It can prepare a client certificate, list the advertised endpoints, or open a None/Sign/SignAndEncrypt Basic256Sha256 session and check for an expected result: `allow`, `key-deny`, `peer-deny` or `anon-deny`. On `allow` it reads `ServerStatus.State`, `ServiceLevel` and the virtual tag. The key comes from `GATE_PROBE_KEY`, never from an argument. |
| `BindRepro/` | Reproduces lmxopcua#879 (listing only the secure profiles does not remove `None`). |
## Inputs (`<Root>\upload`)
`baseline-host`, `baseline-cli`, `candidate-host`, `candidate-cli` are self-contained `win-x64` publishes, made with
`-p:PublishSingleFile=false -p:PublishTrimmed=false`. `probe` is the framework-dependent build of `Probe/`, and
`seeder-baseline` is the framework-dependent publish of `Seeder/` from the baseline tree. `install-baseline` and
`install-candidate` are `scripts/install/*.ps1,*.psm1` from each tree. Keep a manifest (`sha256 relative/path`, sorted) of
every directory and compare it on the node before you start.
## Order
```powershell
$g = 'C:\OtOpcUa800\upload\Invoke-ServiceGate.ps1'
& $g -Phase Prepare # Install-Services.ps1 -Roles driver as NT SERVICE\OtOpcUaHost, node env, ACLs
& $g -Phase Boot -Label b0 -AllowNotReady; & $g -Phase Stop -Label b0 # service creates PKI + LocalDb
& $g -Phase Seed; & $g -Phase Trust
& $g -Phase Boot -Label b1; & $g -Phase Checks -Label b1 -BaselineServer; & $g -Phase Stop -Label b1
& $g -Phase Upgrade # backup, stage separately, carry appsettings/pki/data/logs with /COPYALL, swap
& $g -Phase Boot -Label c1; & $g -Phase Checks -Label c1; & $g -Phase Stop -Label c1
& $g -Phase Boot -Label c2; & $g -Phase Checks -Label c2; & $g -Phase Stop -Label c2
& $g -Phase IsolationInstall -PatchedIsolation # see lmxopcua#883; drop the switch once the repo installer is fixed
& $g -Phase Boot -Label c3; & $g -Phase Checks -Label c3; & $g -Phase IsolationVerify -Label c3; & $g -Phase Stop -Label c3
& $g -Phase IsolationUninstall -PatchedIsolation
& $g -Phase Rollback
& $g -Phase Boot -Label r1; & $g -Phase Checks -Label r1 -BaselineServer; & $g -Phase Stop -Label r1
& $g -Phase Compare -Label b1,c1,c2,c3,r1
& $g -Phase Cleanup # then copy evidence off and remove <Root>
```
`-Phase Observe -Label L` records a start the gate did not make (an SCM auto-start after a host reboot). Every phase stops
at the first error. A `Checks` failure lists the failing cases by name. Secrets live in
`<Root>\private\secrets.json`, which only SYSTEM and Administrators can read. Every output and log is checked against every
task secret, and the evidence keeps only the redacted copies and the occurrence counts.
## What the checks mean
- **Strict channel.** These are gating cases. The probe's peer auto-accept is off. A trusted client gets a Basic256Sha256 Sign
session and a SignAndEncrypt session. An untrusted client (both modes) must be refused with `BadSecurityChecksFailed`, and a
server the client does not trust must be refused with `BadCertificateUntrusted`.
- **Credentials.** These are gating cases. The 65-character production-format key and the 54-character key must both be
admitted. A wrong secret, an unknown key id, a disabled key and anonymous must all be refused.
- **Against the SDK 1.5 server** (`-BaselineServer`), the SDK 2 client's 65-character key is expected to be refused
(lmxopcua#880). An SDK 2 client sends any password over 64 bytes as `RsaEncryptedSecret`, which 1.5.378 rejects with
`BadIdentityTokenInvalid`. The 54-character key and the SDK 1.5 CLI with the 65-character key are the positive controls.
- **CLIs.** The CLI that matches each SDK line reads `ServerStatus` over SignAndEncrypt with an API key. The CLIs auto-accept
the server certificate by design, so these reads do not count as strict-peer coverage.
- **ScriptWorker isolation.** `IsolationInstall`/`IsolationUninstall` run `Install-ScriptWorkerIsolation.ps1`. The script
runs once under PowerShell 7 and once under Windows PowerShell 5.1, so both must work. The script's dry run happens first.
Without `-PatchedIsolation` the repository copy is used, and that copy fails on both hosts (lmxopcua#883). With
`-PatchedIsolation` it uses `<Root>\upload\install-candidate-patched`, which is the repository copy plus
`evidence/2026-11-12-windev/isolation-installer-883.diff`. `IsolationVerify` needs the service running. It checks:
- worker processes are children of the host and run under a restricted token;
- the elevated administrator is refused on the worker pipe;
- `script-runner` health and the PE cache.
- **Findings, not gating.** The script records which endpoints the server advertises, and the result of a session over the
`None` endpoint (lmxopcua#879).
**Cleanup caution.** `Cleanup` deletes `OtOpcUaHost` with `sc.exe delete`. It deliberately does not run
`scripts/install/Uninstall-Services.ps1`, because that script also stops and deletes `OtOpcUaWonderwareHistorian`, which
other work may still run (windev does). A virtual-account profile (`C:\Windows\ServiceProfiles\OtOpcUaHost`) stays loaded until
the next reboot. Remove it afterwards.