ScriptWorker isolation: InstallRootDeny does not cover sibling pki\ and data\ on the QA layout (Users-readable) #890
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found during the QA SDK 2 deploy on 2026-11-12 (#794, #780). Record:
docs/deploy/qa-upgrade-b8b4c18f-to-fea7de0a.md.Install-ScriptWorkerIsolation.ps1denies the worker identity ReadData only on-InstallRoot(the folder withOtOpcUa.Host.exe). On the QA layout, the node's secrets live in siblings of that folder, not inside it:E:\ApiInstall\OtOpcUa\pki, which includesown\private\*.pfx, the application certificate's private key;E:\ApiInstall\OtOpcUa\data, which holdssecrets.dband the LocalDb store.Both folders inherit
BUILTIN\Users:(RX)fromE:\. The worker runs under a restricted token, and an access must pass for the host account (LocalSystem) and the restricting set, which is identity + Everyone + Users + RESTRICTED. A folder that grants Users read is therefore probably readable by the worker. Only the directory ACLs were inspected; the file-level ACLs (the SDK may write the pfx with its own DACL) were not checked.The analyzer's System.IO bans are still the first line of defence; this is about the defence-in-depth layer.
Options:
-DenyPaths(or derives them fromOpcUa:PkiStoreRoot,LocalDb:PathandSecrets:SqlitePath) and applies the same inherited deny.Not urgent: QA has no scripts today, so no worker runs.
The fix is on master as
f75ee7d0. The QA re-apply has NOT been done, so this issue stays open andblocked.What changed
-DenyPaths <dir>,…onInstall-ScriptWorkerIsolation.ps1.NT SERVICE\OtOpcUaScriptWorkerthat the install root already gets (newSensitiveDirDenysteps).script-worker\and the PE cache. Every listed folder must exist; this is checked before anything changes.a,b,cstring works, which is howpowershell -Filepasses an array.0xD0157.icaclsshows it as(OI)(CI)(DENY)(W,D,WDAC,WO,RD,DC).script-worker\.pki\trusted\certs.InstallRootDeny, which was0x1.HKLM\SYSTEM\CurrentControlSet\Services\OtOpcUaScriptWorker\Parameters, valueDenyPaths(stepDenyPathRecord).-Uninstallremoves exactly the recorded denies, so you don't need to pass-DenyPathsagain.-WhatIfand-PlanOnlyshow every step.-DenyPaths, a WARN for anypki/data/logs/backup/config/secretsfolder beside InstallRoot.docs/ServiceHosting.md, design §12.5, and the QA recorddocs/deploy/qa-upgrade-b8b4c18f-to-fea7de0a.md.Tests
Test-ScriptWorkerIsolation.ps1, pwsh on macOS): 86 passed, up from 46.script-worker\not refused.Test-ScriptWorkerIsolation.Windows.ps1, Windows PowerShell 5.1 on windev, run for real in a scratch folder: 34/34.-DenyPaths: 11/11 changed. Re-apply: 11/11unchanged.-DenyPaths(driven by the record): 10 changed. Second uninstall: 7/7unchanged.InstallerLayoutIsolationTests(ScriptWorker.Tests; needsOTOPCUA_ISOLATION_INSTALLER_E2E=1and an elevated Windows session), on windev:C:\otopcua-iso-e2e-*\{app\script-worker (the real built worker), pki\own\private\*.pfx, pki\trusted\certs, data\secrets.db, logs, backup}, with a data-drive ACL: Users RX plus create-files in subfolders.-DenyPaths root,pki,data):app\appsettings.json, and cannot plant the certificate;script-worker\;-DenyPathsdenies set to a no-op mask, which is the old app-only coverage, the test goes red atpki: the worker token must not read …\pki\own\private\otopcua.pfx. That reproduces this issue.QA re-apply (not done; a later deploy step, on both vq03 and vq04)
Run from the staged
scripts\installatf75ee7d0or later, 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. First check thatSecrets__SqlitePath, its key file andLocalDb__Pathin the service environment all sit underE:\ApiInstall\OtOpcUa; add any folder outside it to$deny.Expected:
changed:DenyPathRecord,InstallRootDeny(mask 0x1 → 0xD0157) and the 3SensitiveDirDenysteps.unchanged.icaclsonE:\ApiInstall\OtOpcUa,…\pki,…\dataand…\appshowsNT SERVICE\OtOpcUaScriptWorker:(OI)(CI)(DENY)(W,D,WDAC,WO,RD,DC).reg query HKLM\SYSTEM\CurrentControlSet\Services\OtOpcUaScriptWorker\Parameters /v DenyPathslists the 3 folders.Rollback is unchanged:
… -ConfigureHostService -Uninstall. It now also removes the recorded denies.Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
Re-applied on QA vq03 and vq04, 2026-11-12, and verified against the live worker. Closing. Record:
docs/deploy/qa-upgrade-b8b4c18f-to-fea7de0a.md, section "Canary, live isolation check and runaway drill" (a17ffb72).Secrets__SqlitePathandLocalDb__Pathare underE:\ApiInstall\OtOpcUa\data.Secrets__MasterKey__FilePathisC:\ProgramData\ZB\kek\otopcua.kek, outside the install root, soC:\ProgramData\ZB\kekwas added to the list. That folder was already SYSTEM + Administrators only.-DenyPaths E:\ApiInstall\OtOpcUa, …\pki, …\data, C:\ProgramData\ZB\kek.-WhatIfshowed 12 operations. The first apply changed 7 of 12 steps, and the second apply reported 12/12unchangedon both nodes.changedbeyond what the record expected isHostServiceEnv, and it is a reorder only. The P3 flip had appended theScripting__Runner*entries after the two isolation entries, and the step rewrites the list with its own two last. The values are the same and the env count is unchanged. This is harmless, but the step treats a reorder as a change.historiangw.kekandmxgateway.kek(protected DACLs). The live probe shows the worker cannot read them.icaclsshowsNT SERVICE\OtOpcUaScriptWorker:(OI)(CI)(DENY)(W,D,WDAC,WO,RD,DC)on the root,pki,data,appand the KEK folder.app\script-workerkeeps its explicit RX.DenyPathsvalue lists the 4 folders.pki\own\private\*.pfx,pki\trusted\certs\*.der,data\secrets.db,data\otopcua-localdb.db,app\appsettings.json, the logs, theapp.bakcopy, all three.kekfiles and the PE-cache key.pki\trusted\certs,data,appandscript-worker./health/readywas 200 on both nodes after the run, with no restart.Residual (new issue #891): the same token can still read Users-readable files that belong to other apps or to deploy staging: the OtOpcUaHost
.regexport underE:\psrem-stageand the ScadaBridge/HistorianGatewayappsettings.json. Those were left alone because they are outside OtOpcUa's folders.Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi