Merge #835/#836 review fixes (owner check on unsaved endpoints, credential-use audit)

This commit is contained in:
Joseph Doherty
2026-11-11 04:13:02 -05:00
21 changed files with 790 additions and 32 deletions
+32 -6
View File
@@ -1419,7 +1419,25 @@ through `DriverInstanceIdentities` (only when the config carries an in-scope `se
3. **Test Connect** — `AdminOperationsActor`, before the probe; `TestDriverConnect.DriverInstanceId` names the owner
(the device modal passes it; omitted ⇒ treated as a new driver, i.e. stricter).
4. **Live browse** — `BrowserSessionService.OpenAsync(…, driverInstanceId, …)`, before any browser; `/raw` browse
resolves the owner from the device. Without a ConfigDb it fails closed.
resolves the owner from the device. Without a ConfigDb, or when the ConfigDb read faults, it is refused (a message on
the modal, never a circuit error).
**An unsaved endpoint never gets the named owner's own secrets (review I-1).** Test Connect and browse take a config
that may carry UNSAVED edits — the device modal merges the stored DriverConfig with the device config being typed — so
naming the owner would otherwise let a Designer type their own host into a victim's device and Test Connect it with the
victim's `secret:galaxy/<victim-id>/…`. The server (`DriverInstanceIdentities.EffectiveOwnerAsync`, called by the actor
and by `BrowserSessionService` — the client's claim is not trusted) honours the named owner only when it exists, has the
same type, and every **endpoint value** of the config (`DriverCredentialRules.EndpointFields`: Galaxy `Endpoint`,
OpcUaClient `EndpointUrl`/`EndpointUrls`/`DiscoveryUrl`, Heidenhain `Host`/`Port`) is one that driver already stores
(its DriverConfig or any DeviceConfig). Otherwise the owner is treated as null — no instance-named secret at all — and
the refusal says to save the change first. Saving is the deliberate, persisted act; a saved endpoint change then
behaves like any other saved config.
**Every credential-carrying Test Connect / browse is audited.** When the config carries an in-scope `secret:` or `env:`
reference, `CredentialUseAudit` records an audit event (category `CredentialUse`, action `TestConnect` / `Browse`,
actor = the signed-in user, target = the named driver or `(unsaved driver)`, outcome, and the endpoint host(s) — URI
user-info dropped, never the reference or a value) through the shared `IAuditWriter`, plus a structured Information
log line (`Credential use: …`).
Already-running drivers: a deployed driver resolves its reference exactly as before (the driver itself does not know
the other instances, so the runtime adds no check) — the same deprecated window as #815: the deploy gate refuses the
@@ -1428,10 +1446,11 @@ the driver's own value under `galaxy/<its DriverInstanceId>/…` (or a shared na
at it.
Residuals (accepted): `env:` references are not instance-scoped (a variable is node-wide, and there is no naming
convention to match — a deployment that needs per-instance isolation must use the secret store); and the rule cannot
stop a Designer from editing an **existing** driver's endpoint and Test-Connecting with that driver's own reference —
that is the general "anyone who may author config may redirect it" property described above (the lever is the page
policy). A gateway-side complement — mxaccessgw binding each API key to the client identity / Galaxy it was issued for
convention to match — a deployment that needs per-instance isolation must use the secret store); a reference to a
**shared** name (one no instance is called) is not tied to any owner, so an unsaved endpoint can still be Test-Connected
with it (audited); and a Designer who **saves** a new endpoint on an existing driver can then Test Connect / browse /
deploy it with that driver's own reference — the general "anyone who may author config may redirect it" property
described above (the save is persisted, the use is audited; the lever is the page policy). A gateway-side complement — mxaccessgw binding each API key to the client identity / Galaxy it was issued for
— would make a captured key useless elsewhere; that is cross-repo (`mxaccessgw`) and not implemented here.
#### Key-material file paths are confined to a node directory (#836)
@@ -1443,9 +1462,16 @@ service account can read (another product's client certificate, a host certifica
- The field is a **relative path** (`plant1/user.pfx`) inside the node's user-certificate directory,
**`Drivers:OpcUaClient:UserCertificateDirectory`** (env `Drivers__OpcUaClient__UserCertificateDirectory`; default
`<LocalAppData>/OtOpcUa/pki/user-certificates`, beside the driver's PKI stores — for a service running as LocalSystem
that is under `C:\Windows\System32\config\systemprofile\AppData\Local`). Segments are `[A-Za-z0-9._-]`; an absolute,
that is under `C:\Windows\System32\config\systemprofile\AppData\Local`). A **relative** directory setting is resolved
against the install directory (`AppContext.BaseDirectory`), never the working directory (a Windows service starts in
`System32`). Segments are `[A-Za-z0-9._-]`; an absolute,
rooted, drive-qualified or UNC path, `.`/`..`/empty segments and anything else are refused
(`CredentialFilePaths`, rule row `DriverCredentialPolicy.OpcUaClientUserCertificatePathField`).
- The rule applies unless the **same JSON object** explicitly sets `AuthType` to something other than `Certificate`
(string or enum ordinal) — the driver never loads the file then, and the AdminUI form no longer persists the hidden
field. An object without `AuthType` stays checked, and the deploy gate also checks the driver's config **as the
runtime merges it with its devices**, so splitting `AuthType: Certificate` (device) from the path (driver) cannot slip
a path past the gate.
- **Refused at the same five seams as #815** (save, deploy gate `CredentialReferenceOutOfScope`, Test Connect, browse —
messages never carry the path) and **at load time**: `OpcUaUserIdentityFactory.ResolveSecretRefsAsync` (called by the
running driver and the AdminUI browse before the identity is built) joins it to the directory, canonicalises it, and