fix(security): per-instance credential references (#835) and confined OpcUaClient user-certificate path (#836)

#835: inside one driver namespace, a secret:<ns>/<X>/... reference whose first
segment names ANOTHER existing same-type driver instance (id or name,
case-insensitive; another-instance match beats an own-name match, so renaming
one's driver after a victim's unlocks nothing) is refused at save
(DriverConfigSaveGate.ValidateAsync), deploy (CredentialReferenceOtherInstance,
Heidenhain included), Test Connect (TestDriverConnect.DriverInstanceId, ConfigDb
read off the mailbox) and live browse (BrowserSessionService, fail-closed without
a ConfigDb). One rule: DriverCredentialPolicy.DescribeOtherInstanceProblems +
DriverInstanceIdentities. Shared names and the driver's own id stay valid; env:
is not instance-scoped. Running drivers are unchanged (warn-only window: the
deploy gate and the upgrade preflight / /raw banner list them).

#836: OpcUaClient UserCertificatePath is a relative path inside
Drivers:OpcUaClient:UserCertificateDirectory (default
<LocalAppData>/OtOpcUa/pki/user-certificates). Syntactic rule (CredentialFilePaths,
a DriverCredentialPolicy path field) at every #815 seam; at load time
ResolveSecretRefsAsync joins, canonicalises and requires lexical AND
link-resolved containment before the PFX password is resolved, and
LoadCertificate refuses an unresolved path. A running driver's pre-#836 absolute
path keeps loading with a rate-limited warning (LegacyCredentialReferenceTracker.ReportPath).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-11-11 03:52:48 -05:00
parent 481985e20b
commit 63331b0416
31 changed files with 1555 additions and 49 deletions
@@ -611,6 +611,19 @@ public sealed class AdminOperationsActor : ReceiveActor
var sw = System.Diagnostics.Stopwatch.StartNew();
try
{
// lmxopcua#835: a reference naming ANOTHER driver instance's secret (secret:galaxy/<other>/…) is refused
// before the probe resolves it — off the mailbox, since it reads the ConfigDb (only when the config
// carries an in-scope secret: reference at all).
if (DriverCredentialPolicy.HasInstanceSegments(msg.DriverType, msg.ConfigJson))
{
await using var db = await _dbFactory.CreateDbContextAsync(cts.Token);
var instanceProblems = await DriverInstanceIdentities.DescribeOtherInstanceProblemsAsync(
db, msg.DriverType, msg.ConfigJson, msg.DriverInstanceId, cts.Token);
if (instanceProblems.Count > 0)
return new TestDriverConnectResult(
false, "Test Connect refused: " + string.Join(" ", instanceProblems), null, msg.CorrelationId);
}
var result = await probe.ProbeAsync(msg.ConfigJson, timeout, cts.Token);
sw.Stop();
return new TestDriverConnectResult(