docs(deploy): QA canary, #890 re-apply, live #794 worker check and #780 runaway drill on vq03/vq04
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
- #890: installer at f75ee7d0 re-applied on both nodes with -DenyPaths root, pki, data and
C:\ProgramData\ZB\kek (the Secrets KEK sits outside the install root); second apply 12/12 unchanged.
- Script-runner canary authored in the AdminUI and deployed (Calculation tag + VirtualTag, 1 s timers):
Good and advancing on both endpoints; kept on QA as a runner monitor.
- #794 step 5: the live worker token is restricted (SeChangeNotify only; identity/Everyone/Users/RESTRICTED)
and is denied read and write on pki, data, app, logs and the KEKs; network sockets none. Finding #891.
- #780: QA runaway drill passed (killed at the 4000 ms hard deadline twice, quarantined, siblings Good,
ServiceLevel 250/240 unchanged); drill removed, redeploy back to the canary revision 82dd0598.
- #780 P4 recorded: InProcess stays the default and the rollback; StuckOrphanQuarantineFactor documented
as in-process only.
Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# QA wonder-app-vq03/vq04 upgrade `b8b4c18f` → master `fea7de0a` (SDK 2.0.1), with the DEV rehearsal
|
||||
|
||||
> **Status: EXECUTED 2026-11-12.** DEV vd03 runs `fea7de0a` (from `5fb4b4db`). The QA pair vq03 + vq04 runs
|
||||
> **Status: EXECUTED 2026-11-12.** Follow-up run the same evening: the #890 re-apply, a script-runner canary, the live #794 step 5 check and the #780 runaway drill (last section). DEV vd03 runs `fea7de0a` (from `5fb4b4db`). The QA pair vq03 + vq04 runs
|
||||
> `fea7de0a` (from `b8b4c18f`) with the out-of-process script runner on (Calculation + VirtualTag seams) and the
|
||||
> ScriptWorker isolation installed. Tracking: #801 (SDK 2 live gate), #793 (ConfigDb preflight), #794 (worker
|
||||
> isolation install), #780 (P3 runner flip). Template: [`vd03-upgrade-b8b4c18f-to-master.md`](vd03-upgrade-b8b4c18f-to-master.md).
|
||||
@@ -152,7 +152,7 @@ siblings `E:\ApiInstall\OtOpcUa\pki` (including `own\private\*.pfx`) and `data\`
|
||||
`BUILTIN\Users:ReadAndExecute`. Users is one of the worker token's restricting SIDs, so those folders are not covered
|
||||
by the deny. File-level ACLs were not inspected. Follow-up: #890.
|
||||
|
||||
**#890 fix, PENDING re-apply on vq03 and vq04.** Not run on QA yet. This is a deploy step for a later, user-requested window.
|
||||
**#890 fix: re-applied on vq03 and vq04 later the same day** (see "Canary, live isolation check and runaway drill" below, which also adds the KEK folder to the list). The original instructions follow.
|
||||
The installer now takes `-DenyPaths` and records them. Once the scripts at that commit are staged on each node, run the
|
||||
following in an elevated Windows PowerShell 5.1 session. The services can stay up: only folder ACLs change for the worker
|
||||
identity, and no worker runs on QA today.
|
||||
@@ -207,3 +207,114 @@ Rollback stays `… -ConfigureHostService -Uninstall`. It now also removes the d
|
||||
`OtOpcUaConfig_pre-fea7de0a_20261112.bak` is the DB fallback.
|
||||
- **DEV:** `E:\ApiInstall\_stage-otopcua-fea7de0a\rollback.ps1` (→ `5fb4b4db`).
|
||||
- SDK 1.5 tooling caveat after a rollback: see the rollout runbook §Rollback item 4 (#880).
|
||||
|
||||
## Canary, live isolation check and runaway drill (2026-11-12, 18:49–19:07 EST)
|
||||
|
||||
Owner request the same day: "add the canary tag and finish QA". WinRM was used only for the on-box steps below
|
||||
(installer, SQL role grant, live-token probe, log check of the drill). Everything else ran over HTTP / opc.tcp / SMB
|
||||
from the Mac. No service restart; the pair stayed at **vq03 250 (Primary) / vq04 240** throughout.
|
||||
|
||||
### 1. #890 re-apply (both nodes, services up)
|
||||
|
||||
Scripts `scripts/install/{Install-ScriptWorkerIsolation.ps1,ScriptWorkerIsolation.psm1}` at `f75ee7d0` (SHA-256 `E0C11AEA…`,
|
||||
`F9425E4C…`), staged over SMB to `E:\psrem-stage\otopcua-890-f75ee7d0\install`, hashes checked on each box.
|
||||
|
||||
- **Path check (service env):** `Secrets__SqlitePath=E:\ApiInstall\OtOpcUa\data\secrets.db` and
|
||||
`LocalDb__Path=E:\ApiInstall\OtOpcUa\data\otopcua-localdb.db` are inside. **`Secrets__MasterKey__FilePath=C:\ProgramData\ZB\kek\otopcua.kek`
|
||||
is outside**, so `C:\ProgramData\ZB\kek` was added. That folder is already protected (SYSTEM + Administrators only),
|
||||
so the deny there is defence in depth.
|
||||
- `$deny = 'E:\ApiInstall\OtOpcUa','E:\ApiInstall\OtOpcUa\pki','E:\ApiInstall\OtOpcUa\data','C:\ProgramData\ZB\kek'`, then
|
||||
`-WhatIf` (12 operations, nothing changed) → apply → apply.
|
||||
- **First apply: 12 steps, 7 `changed`:** `DenyPathRecord`, `InstallRootDeny` (`0x1` → `0xD0157`), four `SensitiveDirDeny`,
|
||||
and **`HostServiceEnv`**. The last one is a reorder only: the earlier P3 flip had appended the `Scripting__Runner*`
|
||||
entries after `Scripting__Worker__Identity`/`PeCacheDir`, and the step rewrites the list with its two entries last.
|
||||
Same values, env count unchanged (46 / 45). **Second apply: 12/12 `unchanged`** on both nodes.
|
||||
- **Coverage:** vq03 `5 denied folder(s); 0 protected item(s)`. vq04 WARNs that `C:\ProgramData\ZB\kek\historiangw.kek` and
|
||||
`mxgateway.kek` have protected DACLs. The live probe below shows the worker cannot read either.
|
||||
- **State, both nodes:** `icacls` on `E:\ApiInstall\OtOpcUa`, `pki`, `data`, `app` and `C:\ProgramData\ZB\kek` shows
|
||||
`NT SERVICE\OtOpcUaScriptWorker:(OI)(CI)(DENY)(W,D,WDAC,WO,RD,DC)`; `app\script-worker` keeps the explicit `(OI)(CI)(RX)`
|
||||
ahead of the inherited deny. `reg query …\OtOpcUaScriptWorker\Parameters /v DenyPaths` lists the four folders. Anchor
|
||||
DISABLED, firewall rule Outbound/Block on the worker exe. `/health/ready` 200 on both afterwards.
|
||||
|
||||
### 2. Canary configuration (authored in the AdminUI, deployed with "Deploy current configuration")
|
||||
|
||||
The QA AdminUI cannot be used from a browser over plain HTTP (its auth cookie is `Secure`), and QA had no LDAP
|
||||
group → role grant. For this run only: a local forwarder `localhost:19003 → wonder-app-vq03:9000` (Chrome treats
|
||||
`http://localhost` as secure), and a **temporary** `LdapGroupRoleMapping` row (`SCADA-Admins` → `Administrator`,
|
||||
system-wide, on-box SQL with the service's own connection) for the box GLAuth test login. **The row was deleted at the
|
||||
end** (QA again has 0 role mappings; the same login now gets AccessDenied).
|
||||
|
||||
| Object | Value |
|
||||
|---|---|
|
||||
| Scripts | `ScriptRunnerCanary-Calc` (`SC-82e11d9323ff`): `return (int)DateTime.UtcNow.TimeOfDay.TotalSeconds;` · `ScriptRunnerCanary-VT` (`SC-c2ec9b4c4c53`): `return DateTime.UtcNow.Second;` |
|
||||
| Raw | Calculation driver `script-runner-canary` (under the QA cluster) → `Device1` → tag `canary-calc-seconds`, Int32, Read, `{"scriptId":"SC-82e11d9323ff","changeTriggered":true,"timerIntervalMs":1000}` → `ns=2;s=script-runner-canary/Device1/canary-calc-seconds` |
|
||||
| UNS | area `canary` (`area-canary`) → line `script-runner` (`line-canary`) → equipment `script-runner-canary` (`EQ-ec3979550d90`, MachineCode `SCRIPT_RUNNER_CANARY`) → virtual tag `canary-vt-second` (`VT-script-runner-canary`), Int32, timer 1000 ms → `ns=3;s=EQ-ec3979550d90/canary-vt-second` |
|
||||
| Deployment | `bcaba714` rev `82dd0598c37f…` Sealed, both nodes in sync (the first deployment ever on QA) |
|
||||
|
||||
**Result:** both values **Good and advancing once a second on both endpoints** (`opc.tcp://10.220.158.2:4840` and
|
||||
`.3:4840`, anonymous, read by the OtOpcUa CLI). Both nodes run the drivers, so no failover was needed to see it.
|
||||
Workers started on the first evaluation: `otopcua_script_worker_up{shard=0,1}` = 1, `runner_state` 0, two PE entries
|
||||
in `C:\ProgramData\OtOpcUa\script-pe-cache\pe\` on each node (listed over SMB).
|
||||
|
||||
**Kept on QA as a runner monitor** (the design and #780 say nothing about removing it): a Bad or frozen
|
||||
`canary-calc-seconds` / `canary-vt-second`, or `otopcua_script_worker_up` = 0, means the out-of-process runner is sick.
|
||||
|
||||
### 3. #794 step 5: the live worker under the real service session (both nodes)
|
||||
|
||||
Probe: an admin PowerShell on the box duplicated the **live worker's own token** (opened under the host's LocalSystem
|
||||
token, since an admin cannot open it directly) and impersonated it for file opens. The same reads were then repeated
|
||||
without impersonation as the positive control (all READ-OK).
|
||||
|
||||
| Check | vq03 and vq04 (identical) |
|
||||
|---|---|
|
||||
| Worker processes | 2 per node, `…\app\script-worker\ZB.MOM.WW.OtOpcUa.ScriptWorker.exe`, parent = `OtOpcUa.Host.exe` |
|
||||
| Token | user `NT AUTHORITY\SYSTEM`, **restricted = True**, privileges **only `SeChangeNotifyPrivilege`**, restricting SIDs `NT SERVICE\OtOpcUaScriptWorker, Everyone, BUILTIN\Users, NT AUTHORITY\RESTRICTED` (host token: unrestricted, 28 privileges) |
|
||||
| Window-station grant | works under the real service session: both shards run and evaluate (no `ERROR_DLL_INIT_FAILED`) |
|
||||
| **#890 paths, read** | **DENIED:** `pki\own\private\*.pfx`, `pki\trusted\certs\*.der`, `data\secrets.db`, `data\otopcua-localdb.db`, `app\appsettings.json`, `app\OtOpcUa.Host.dll`, `app\logs\scripts-*.log`, `app.bak.…\appsettings.json`, `C:\ProgramData\ZB\kek\{otopcua,historiangw,mxgateway}.kek`, `script-pe-cache\pe-cache.key` |
|
||||
| **#890 paths, write** | **DENIED:** create a file in `pki\trusted\certs`, `data`, `app`, `app\script-worker` |
|
||||
| Positive controls | READ-OK: `app\script-worker\ZB.MOM.WW.OtOpcUa.ScriptWorker.dll`, `C:\Windows\System32\kernel32.dll` |
|
||||
| Network | 0 TCP / 0 UDP endpoints owned by either worker pid; rule `OtOpcUa-ScriptWorker-BlockOutbound` Outbound/Block/Any/enabled and its program equals the running image path |
|
||||
| PE cache | filled on first use (above); after the drill's worker restarts `otopcua_script_compile{cache="hit"}` = 4 on each node |
|
||||
|
||||
Not done: the dropped-packet firewall log. The worker has no code path that opens a socket, so the log would be empty
|
||||
whether or not the rule worked. Turning on profile-wide logging was not worth it for that.
|
||||
|
||||
**Finding (outside #890's scope, filed as #891):** the worker token **can** read some Users-readable files outside
|
||||
OtOpcUa's folders: `E:\psrem-stage\otopcua-fea7de0a\backup\OtOpcUaHost-pre-fea7de0a.reg` (a registry export of the
|
||||
OtOpcUaHost environment, which holds secrets), and `E:\ApiInstall\ScadaBridge\app\appsettings.json` /
|
||||
`E:\ApiInstall\HistorianGateway\app\appsettings.json`. `C:\ProgramData\ZB\qa-build\*` is denied (protected ACL). Those
|
||||
folders belong to other apps or to deploy staging, so no deny was added there in this run. Follow-up: #891.
|
||||
|
||||
### 4. #780 runaway drill (live gate: "a QA flip with the same drill")
|
||||
|
||||
A second script `ScriptRunnerCanary-RunawayDrill` = `while (true) { }` bound to VT `canary-runaway-drill` (Int32, timer
|
||||
1000 ms) on the canary equipment, deployed as `e746cc5f` (rev `43bd8917…`). Seams: Calculation + VirtualTag.
|
||||
|
||||
| Observed | vq03 | vq04 |
|
||||
|---|---|---|
|
||||
| Drill VT | `BadInternalError` from the first sample (00:02:59Z) | same |
|
||||
| Kills | 19:03:03.32 and 19:03:08.31 EST: `exceeded its hard deadline (4000 ms); killing ScriptWorker[0]` → `ended: "KilledDeadline" … culprit 1:VT-script-runner-canary-runaway, 1 evaluation(s) … in flight` | 19:03:03.32 and 19:03:08.27, same lines |
|
||||
| Quarantine | `Script "1:VT-script-runner-canary-runaway" quarantined: "it exceeded its hard deadline and its worker was killed (2 time(s) in the last hour)…"`; `otopcua_script_quarantined_total{reason="killed"}` 1, `worker_exits_total{reason="killed_deadline",shard="0"}` 2, `run_refused` 4 | same |
|
||||
| Shard 0 restart | back in about 0.5 s each time | same |
|
||||
| Siblings | `canary-vt-second` 77 updates, `canary-calc-seconds` 88 updates over the drill, **all Good** | 74 / 88, all Good |
|
||||
| Node | `/health/ready` 200, `runner_state` 0, `/health/active` 200 | `/health/ready` 200, `/health/active` 503 (Secondary) |
|
||||
| ServiceLevel | **250** on all 21 samples | **240** on all 21 samples |
|
||||
|
||||
**Cleanup:** the drill VT and its script were deleted in the AdminUI and the configuration redeployed as `5905003e`. Its
|
||||
revision is **`82dd0598…`, the same as the canary-only deployment `bcaba714`**. The drill node now reads `BadNodeIdUnknown`
|
||||
and both canary values are Good on both nodes.
|
||||
|
||||
**ScriptedAlarm seam: left off.** No canary scripted alarm was added: alarms go last (§14 #10), and the alarm drill
|
||||
already passed on docker-dev (§12.4).
|
||||
|
||||
### Final QA state (19:07 EST)
|
||||
|
||||
- vq03 **250** Primary, `/health/active` 200. vq04 **240** Secondary, 503. Both `/health/ready` 200 with 5/5 components.
|
||||
Overview: OtOpcUa Up, 2 members / 0 unreachable.
|
||||
- `Scripting__Runner=OutOfProcess`, seams Calculation + VirtualTag. Isolation with the #890 denies on both nodes.
|
||||
- ConfigDb: the canary only (1 driver, 1 device, 1 tag, 1 VT, 2 scripts, 1 area/line/equipment, 0 scripted alarms, 0 role
|
||||
mappings, 0 API keys). Three sealed deployments; `5905003e` is current.
|
||||
- ScadaBridge, mxgw and HistorianGateway on QA were not touched.
|
||||
- **Rollback of this run:** remove the canary by deleting its rows in the AdminUI and deploying (or restore
|
||||
`OtOpcUaConfig_pre-fea7de0a_20261112.bak`). The isolation rollback is unchanged: `… -ConfigureHostService -Uninstall`
|
||||
removes the four recorded denies.
|
||||
|
||||
Reference in New Issue
Block a user