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:
@@ -165,6 +165,13 @@ public sealed class OutOfProcessScriptRunner : IScriptRunner, IAsyncDisposable,
|
||||
_scriptRoot = scriptRoot ?? _log;
|
||||
LoadTimeout = loadTimeout;
|
||||
WorkerPath = ResolveWorkerPath(_worker);
|
||||
// Fail closed (startup also refuses it, ScriptRunnerSelection.Validate): a configured identity that cannot be
|
||||
// applied must never silently fall back to the host's own token.
|
||||
if (_worker.ValidateIdentity(OperatingSystem.IsWindows()) is { } identityViolation)
|
||||
throw new InvalidOperationException($"Scripting misconfiguration: {identityViolation}");
|
||||
WorkerSid = string.IsNullOrWhiteSpace(_worker.Identity)
|
||||
? null
|
||||
: ScriptWorkerIdentity.TryResolveSid(_worker.Identity, out var sid, out _) ? sid : null;
|
||||
|
||||
// The PE cache is best effort: a directory or key that cannot be used is a Warning and a cache-less runner,
|
||||
// never a startup failure (the path RULE is enforced at startup by ScriptRunnerSelection.Validate).
|
||||
@@ -211,6 +218,9 @@ public sealed class OutOfProcessScriptRunner : IScriptRunner, IAsyncDisposable,
|
||||
/// <summary>The worker entry assembly this runner launches.</summary>
|
||||
public string WorkerPath { get; }
|
||||
|
||||
/// <summary>The worker identity's SID (<c>Scripting:Worker:Identity</c>), or null when workers keep the host's token.</summary>
|
||||
public string? WorkerSid { get; }
|
||||
|
||||
/// <summary>The overall state (the readiness check, the state gauge).</summary>
|
||||
public ScriptRunnerState State => ComputeState(_shards.Select(s => s.DownFor).ToArray(), _isolation.DownFor, _worker.CircuitOpenAfter);
|
||||
|
||||
@@ -693,7 +703,8 @@ public sealed class OutOfProcessScriptRunner : IScriptRunner, IAsyncDisposable,
|
||||
PeCacheKey: _peCacheKey,
|
||||
MarkerDirectory: _markerDirectory,
|
||||
LoadTimeout: LoadTimeout,
|
||||
CompileThreads: _compileThreads);
|
||||
CompileThreads: _compileThreads,
|
||||
WorkerSid: WorkerSid);
|
||||
return new WorkerShard(label, isolation,
|
||||
(onExiting, ct) => WorkerConnection.StartAsync(spec, _log, OnLogs, onExiting, ct),
|
||||
OnWorkerExit, _log);
|
||||
|
||||
Reference in New Issue
Block a user