<# .SYNOPSIS Isolates the OtOpcUa ScriptWorker processes on a Windows node (lmxopcua#780, design §6, owner decisions §14 #5/#6): the worker's virtual-account identity, the PeCacheDir ACL, an outbound firewall block on the worker executable and a deny on the install folder. Idempotent; -WhatIf prints every change; -Uninstall rolls it back. .DESCRIPTION Only needed when the node runs Scripting:Runner = OutOfProcess or Shadow. It changes, in order: 1. AnchorService a DISABLED service '' whose account is NT SERVICE\, so that virtual account exists and resolves by name. It is never started: the host launches every worker itself, under a restricted token whose restricting SIDs are that account + Everyone + BUILTIN\Users + RESTRICTED, with every privilege removed (Scripting:Worker:Identity; RestrictedWorkerLauncher.cs). Each access check the worker makes must pass for the host account AND that set. 2. PeCacheDir created; ACL replaced: protected, owner Administrators, SYSTEM + Administrators + the host account full control. No Users, no worker access (the HMAC key and launch markers live here). 3. PeEntriesDir \pe, the worker identity gets Modify (workers compile and write HMAC'd entries). 4. WorkerFolderAcl \script-worker, the worker identity gets read + execute. 5. InstallRootDeny , the worker identity is DENIED ReadData/ListDirectory (inherited), so the worker cannot read appsettings*.json, the logs or the host binaries even where Users can. 6. FirewallBlockOutbound a Windows Firewall rule blocking all outbound traffic from \script-worker\ZB.MOM.WW.OtOpcUa.ScriptWorker.exe (every profile). The self-contained Windows publish starts the worker through that apphost; a framework-dependent worker (dotnet.exe) is NOT covered — the analyzer's System.Net ban is then the only control. 7. HostServiceEnv (-ConfigureHostService only) Scripting__Worker__Identity and Scripting__Worker__PeCacheDir in the OtOpcUaHost service environment. Scripting:Runner itself is NOT set here: the runner flip is a separate, per-tier decision (design §12, P2/P3). Re-run it after any deploy that REPLACES the install folder (a rename/swap drops the folder ACEs; files copied into an existing folder inherit them). The host must be restarted for -ConfigureHostService to take effect. .PARAMETER InstallRoot The host install folder (holds OtOpcUa.Host.exe and script-worker\). .PARAMETER HostServiceAccount The OtOpcUaHost service account: DOMAIN\name, NT SERVICE\name, LocalSystem, or a SID. Gets full control of PeCacheDir. .PARAMETER PeCacheDir The compiled-PE cache (Scripting:Worker:PeCacheDir). Absolute and outside InstallRoot (the host refuses otherwise). Default: $env:ProgramData\OtOpcUa\script-pe-cache. .PARAMETER WorkerServiceName The worker identity's virtual account name. Default OtOpcUaScriptWorker (Scripting:Worker:Identity = 'NT SERVICE\OtOpcUaScriptWorker'). .PARAMETER ConfigureHostService Also write the two Scripting__Worker__* entries into the host service's environment (or remove them with -Uninstall). .PARAMETER Uninstall Roll everything back: the firewall rule, every explicit ACE for the worker identity, the anchor service, and (with -ConfigureHostService) the env entries. The PE cache is kept unless -RemovePeCache. .PARAMETER PlanOnly Print the plan and return it as objects without touching (or even reading) the machine. Runs on any OS; used by the dry-run harness scripts/install/tests/Test-ScriptWorkerIsolation.ps1. -HostServiceAccount must then be a SID. .EXAMPLE .\Install-ScriptWorkerIsolation.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' -HostServiceAccount 'OTOPCUA\svc-otopcua' -WhatIf .EXAMPLE .\Install-ScriptWorkerIsolation.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' -HostServiceAccount 'OTOPCUA\svc-otopcua' -ConfigureHostService .EXAMPLE .\Install-ScriptWorkerIsolation.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' -HostServiceAccount 'OTOPCUA\svc-otopcua' -ConfigureHostService -Uninstall #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory)] [string]$InstallRoot, [Parameter(Mandatory)] [string]$HostServiceAccount, [string]$PeCacheDir, [ValidatePattern('^[A-Za-z0-9_.\-]{1,80}$')] [string]$WorkerServiceName = 'OtOpcUaScriptWorker', [string]$HostServiceName = 'OtOpcUaHost', [switch]$ConfigureHostService, [switch]$Uninstall, [switch]$RemovePeCache, [switch]$PlanOnly ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' Import-Module (Join-Path $PSScriptRoot 'ScriptWorkerIsolation.psm1') -Force if (-not $PeCacheDir) { $programData = if ($env:ProgramData) { $env:ProgramData } else { 'C:\ProgramData' } $PeCacheDir = "$($programData.TrimEnd('\'))\OtOpcUa\script-pe-cache" } function Resolve-HostSid([string]$Account) { if ($Account -match '^[Ss]-1-\d+(-\d+)+$') { return $Account.ToUpperInvariant() } if ($PlanOnly) { throw "-PlanOnly needs -HostServiceAccount as a SID (no account lookup without the OS)" } if ($Account -in 'LocalSystem', '.\LocalSystem', 'NT AUTHORITY\SYSTEM') { return 'S-1-5-18' } if ($Account -match '^NT SERVICE\\(.+)$') { return (Get-OtOpcUaServiceSid -Name $Matches[1]) } $name = $Account -replace '^\.\\', "$env:COMPUTERNAME\" ([System.Security.Principal.NTAccount]::new($name)).Translate([System.Security.Principal.SecurityIdentifier]).Value } $hostSid = Resolve-HostSid $HostServiceAccount $plan = Get-ScriptWorkerIsolationPlan -InstallRoot $InstallRoot -HostSid $hostSid -PeCacheDir $PeCacheDir ` -WorkerServiceName $WorkerServiceName -HostServiceName $HostServiceName ` -ConfigureHostService:$ConfigureHostService -Uninstall:$Uninstall -RemovePeCache:$RemovePeCache $workerSid = Get-OtOpcUaServiceSid -Name $WorkerServiceName Write-Host ("ScriptWorker isolation {0}: worker identity NT SERVICE\{1} ({2}), host account {3} ({4})" -f ` $(if ($Uninstall) { 'ROLLBACK' } else { 'install' }), $WorkerServiceName, $workerSid, $HostServiceAccount, $hostSid) if ($PlanOnly) { foreach ($step in $plan) { Write-Host (" [plan] {0,-22} {1}" -f $step.Id, $step.Change) } return $plan } if ($PSVersionTable.PSEdition -eq 'Core' -and -not $IsWindows) { throw 'Install-ScriptWorkerIsolation.ps1 applies changes on Windows only (use -PlanOnly elsewhere)' } if (-not $Uninstall -and -not (Test-Path -LiteralPath (Join-Path $InstallRoot 'script-worker'))) { throw "$InstallRoot\script-worker not found - publish the Host (it carries script-worker\) before isolating it" } $principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() if (-not $WhatIfPreference -and -not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'run elevated (services, firewall rules and folder ACLs need an administrator)' } $results = foreach ($step in $plan) { if ($PSCmdlet.ShouldProcess($step.Target, "$($step.Id): $($step.Change)")) { $outcome = Invoke-ScriptWorkerIsolationStep -Step $step Write-Host (" [{0,-9}] {1,-22} {2}" -f $outcome, $step.Id, $step.Target) [pscustomobject]@{ Id = $step.Id; Target = $step.Target; Outcome = $outcome } } } if (-not $WhatIfPreference) { Write-Host '' if ($Uninstall) { Write-Host 'Rolled back. If the host still sets Scripting:Worker:Identity (appsettings or env), unset it and restart the host.' } else { Write-Host 'Done. Host configuration for this node:' Write-Host " Scripting:Worker:Identity = NT SERVICE\$WorkerServiceName" Write-Host " Scripting:Worker:PeCacheDir = $PeCacheDir" if (-not $ConfigureHostService) { Write-Host ' (not written: re-run with -ConfigureHostService, or set them in appsettings)' } Write-Host ' Restart OtOpcUaHost for a configuration change to take effect.' } } $results