feat(scripting): ScriptWorker isolation on Windows: restricted worker identity, pipe ACL, firewall + PeCacheDir installer (lmxopcua#780)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
Owner decisions §14 #5/#6 as built (design §12.5): - Scripting:Worker:Identity (default unset, Windows only, fail-closed validation): each worker starts under a restricted token derived from the host's. Every privilege but SeChangeNotify is removed; the restricting SIDs are NT SERVICE\OtOpcUaScriptWorker's SID + Everyone + Users + RESTRICTED; the token and default DACLs are host + SYSTEM + identity + RESTRICTED. The worker is created suspended, put in its Job Object before it runs, inherits only its stdio pipes, gets an ACL'd work dir as TEMP, and runs with DOTNET_EnableDiagnostics=0. The identity is granted on the host's window station and desktop, because USER32 init made coreclr.dll fail with ERROR_DLL_INIT_FAILED on windev. The logon SID is not used because the host's process DACL gives it PROCESS_VM_READ. - The worker pipe on Windows always has an explicit protected DACL: host account + identity only. There is no owner ACE, so an elevated host's Administrators gets no access. - The worker treats access denied on its parent-pid poll as alive; the Job Object covers that case. - scripts/install/Install-ScriptWorkerIsolation.ps1 + ScriptWorkerIsolation.psm1 are idempotent and support -WhatIf, -Uninstall and -PlanOnly. They create the disabled anchor service, the PeCacheDir ACL (protected, no Users, key and markers host-only, pe\ Modify for the identity), read+execute on script-worker\, a ReadData deny on the install root, the outbound firewall block on the worker exe, and optionally the host-service env. - Tests: the dry-run harness scripts/install/tests/Test-ScriptWorkerIsolation.ps1 (46 assertions, mutation-checked); ScriptWorkerIdentityTests; WorkerIsolationTests, which cover the pipe ACL, token access and a real restricted worker running a script, all green on windev. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -45,6 +45,7 @@ public sealed class ScriptRunnerSelectionTests
|
||||
["Scripting:Worker:IsolationProbationRuns"] = "9",
|
||||
["Scripting:Worker:PeCacheDir"] = "/var/cache/otopcua-pe",
|
||||
["Scripting:Worker:ExecutablePath"] = "/opt/w/ZB.MOM.WW.OtOpcUa.ScriptWorker.dll",
|
||||
["Scripting:Worker:Identity"] = @"NT SERVICE\OtOpcUaScriptWorker",
|
||||
}), out var adjustments);
|
||||
|
||||
adjustments.ShouldBeEmpty();
|
||||
@@ -58,6 +59,7 @@ public sealed class ScriptRunnerSelectionTests
|
||||
bound.Worker.IsolationProbationRuns.ShouldBe(9);
|
||||
bound.Worker.PeCacheDir.ShouldBe("/var/cache/otopcua-pe");
|
||||
bound.Worker.ExecutablePath.ShouldBe("/opt/w/ZB.MOM.WW.OtOpcUa.ScriptWorker.dll");
|
||||
bound.Worker.Identity.ShouldBe(@"NT SERVICE\OtOpcUaScriptWorker");
|
||||
bound.IsOutOfProcess(ScriptSeam.Calculation).ShouldBeTrue();
|
||||
bound.IsOutOfProcess(ScriptSeam.VirtualTag).ShouldBeFalse();
|
||||
}
|
||||
@@ -115,6 +117,36 @@ public sealed class ScriptRunnerSelectionTests
|
||||
Should.Throw<InvalidOperationException>(() => ScriptRunnerSelection.Validate(options, root)).Message.ShouldContain(why);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_worker_identity_is_refused_at_startup_off_Windows_and_accepted_on_it()
|
||||
{
|
||||
var options = new ScriptingOptions
|
||||
{
|
||||
Runner = ScriptRunnerMode.OutOfProcess,
|
||||
Worker = new ScriptWorkerOptions { Identity = @"NT SERVICE\OtOpcUaScriptWorker" },
|
||||
};
|
||||
if (OperatingSystem.IsWindows())
|
||||
Should.NotThrow(() => ScriptRunnerSelection.Validate(options, "C:\\srv"));
|
||||
else
|
||||
Should.Throw<InvalidOperationException>(() => ScriptRunnerSelection.Validate(options, "/srv"))
|
||||
.Message.ShouldContain("only supported on Windows");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"OTOPCUA\svc-worker")]
|
||||
[InlineData("S-1-5-32-545")]
|
||||
[InlineData(@"NT SERVICE\bad name")]
|
||||
public void A_malformed_or_shared_worker_identity_fails_startup(string identity)
|
||||
{
|
||||
var options = new ScriptingOptions
|
||||
{
|
||||
Runner = ScriptRunnerMode.OutOfProcess,
|
||||
Worker = new ScriptWorkerOptions { Identity = identity },
|
||||
};
|
||||
Should.Throw<InvalidOperationException>(() => ScriptRunnerSelection.Validate(options, "/srv"))
|
||||
.Message.ShouldContain("Scripting:Worker:Identity");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_pe_cache_dir_outside_the_content_root_is_accepted()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user