ConfigurationBinder appends configured collection items to an existing list, so the pre-filled [None, Sign, SignAndEncrypt] default made "drop None" a no-op: every deployment that listed only the secure profiles still advertised an unsigned/unencrypted None endpoint. EnabledSecurityProfiles is now null by default; ResolveEnabledSecurityProfiles() applies the unchanged three-profile default only when nothing is configured. A set-but-empty list fails startup validation and BuildSecurityPolicies no longer falls back to a None endpoint on empty input (it throws). Sweep of all options classes found no other non-empty pre-filled collection default. Tests: options binding (indexed, JSON, env forms, default, empty, bad name), a booted-server wire test (no None endpoint, None connection refused with BadSecurityPolicyRejected, secure connection works; default case is the control). Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
7.9 KiB
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.
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
$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 -Label s1 -ApplyHost pwsh # apply with 7, re-apply with 5.1 and 7
& $g -Phase Boot -Label c3; & $g -Phase Checks -Label c3; & $g -Phase IsolationVerify -Label c3; & $g -Phase Stop -Label c3
& $g -Phase IsolationUninstall -Label s1 -ApplyHost pwsh
# Optional second pass with the hosts swapped (apply with 5.1): IsolationInstall/Verify/Uninstall -Label s2 -ApplyHost powershell.exe
& $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.
Re-running only the isolation step. -Phase Prepare -PrepareFrom candidate installs the candidate directly (no baseline,
upgrade or rollback), so baseline-host and install-baseline are not needed:
Prepare, Boot b0 -AllowNotReady, Stop b0, Seed, Trust, Boot/Checks/Stop c1, then the isolation phases, then Cleanup.
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 withBadCertificateUntrusted. - 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 probe's 65-character key is expected to be refused. The probe uses the SDK's own UserName handler, and SDK 2.0.1 sends any password over 64 bytes asRsaEncryptedSecret, which 1.5.378 rejects withBadIdentityTokenInvalid(lmxopcua#880). That makes the probe the SDK-default negative control. The production SDK 2 CLI forces the legacy format since #880, socli-sdk2-encrypt-key-65gates against both servers. A CLI built before the fix fails it against the baseline; the 2026-11-12 evidence predates the fix. The 54-character key and the SDK 1.5 CLI with the 65-character key remain the positive controls. - CLIs. The CLI that matches each SDK line reads
ServerStatusover 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/IsolationUninstallrunInstall-ScriptWorkerIsolation.ps1from<Root>\upload\install-candidate. The script runs under PowerShell 7 and Windows PowerShell 5.1, so both must work:-ApplyHostpicks the host that applies (and rolls back first), the other host re-applies (every step must reportunchanged) and does the second, idempotent uninstall. The dry run happens first, on both hosts. The first run of this gate (2026-11-12,evidence/2026-11-12-windev/) found the installer failing on both hosts (lmxopcua#883); it was exercised there withisolation-installer-883.diff, which is now merged.scripts/install/tests/Test-ScriptWorkerIsolation.Windows.ps1is the standalone Windows-executed apply / re-apply / uninstall test (scratch names, no OtOpcUaHost needed).IsolationVerifyneeds 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-runnerhealth and the PE cache.
- Findings, not gating. The script records which endpoints the server advertises, and the result of a session over the
Noneendpoint (lmxopcua#879). The #879 fix landed after the 2026-11-12 windev run: a build with it must advertise noNoneendpoint when only the secure profiles are configured, and theNonesession must be refused.
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.