Merge #848 #849 #850 credential name claim/pin, env: per-instance rule, merged preflight

This commit is contained in:
Joseph Doherty
2026-11-11 06:36:06 -05:00
17 changed files with 958 additions and 85 deletions
+63 -8
View File
@@ -1415,7 +1415,7 @@ driver, device, field, allowed form, never the value. The same list, plus runnin
warn-only resolution (**Running**, from driver-health diagnostics, fleet-wide), is shown as a banner on `/raw`,
with a Re-check button.
#### Per-instance credential references (#835)
#### Per-instance credential references (#835, #848, #849)
The namespace confines a reference to values provisioned for that driver *type*, but not to one *instance*: before
#835 a Designer could create a Galaxy driver pointing at their own endpoint with
@@ -1430,7 +1430,31 @@ that instance's **id or name**, case-insensitively.
| `secret:galaxy/DRV-a/apikey`, `secret:galaxy/PlantA/apikey` | allowed — its own |
| `secret:galaxy/gw/apikey`, `secret:galaxy/apikey` (no instance is called `gw` / `apikey`) | allowed — a shared key |
| `secret:galaxy/DRV-b/apikey`, `secret:galaxy/PlantB/apikey`, `secret:galaxy/plantb` | **refused** — another instance's |
| `env:OTOPCUA_GALAXY_…` | not checked — environment variables carry no instance convention (see residuals) |
| `env:OTOPCUA_GALAXY_DRV_A_KEY`, `env:OTOPCUA_GALAXY_PLANTA_KEY` | allowed — its own (#849, see below) |
| `env:GALAXY_MXGW_API_KEY`, `env:OTOPCUA_GALAXY_API_KEY` (no instance is called `API`) | allowed — a shared variable |
| `env:OTOPCUA_GALAXY_DRV_B_KEY`, `env:OTOPCUA_GALAXY_PLANTB_KEY`, `env:GALAXY_MXGW_PLANTB` | **refused** — another instance's (#849) |
**`env:` references have the same rule (#849).** The convention is `env:<PREFIX><INSTANCE>_…` for every prefix of the
driver's scope (`OTOPCUA_GALAXY_`, `GALAXY_MXGW_`, `OTOPCUA_OPCUACLIENT_`, `OTOPCUA_HEIDENHAIN_`), where `INSTANCE` is
a driver id or name in environment form — ASCII letters upper-cased, digits kept, every other character `_`
(`DRV-1a2b` ⇒ `DRV_1A2B`, `Line 1` ⇒ `LINE_1`). The part after the prefix **names** an instance when it equals that
form or starts with it followed by `_` (so `PLANTB` names `env:OTOPCUA_GALAXY_PLANTB_KEY` but not
`env:OTOPCUA_GALAXY_PLANTBX_KEY`). It is applied at every seam below with the same code
(`CredentialReferenceOtherInstance`, message tagged `lmxopcua#849`). Because `_` both separates words and stands in for
`-`/space, a variable can name more than one instance (`OTOPCUA_GALAXY_LINE_1_KEY` names both `Line` and `Line 1`); as
for secrets, any match against another instance wins — use the id form (`env:OTOPCUA_GALAXY_DRV_1A2B_KEY`) for
per-instance variables. Shared variables that start with no instance's id or name (`GALAXY_MXGW_API_KEY`) keep working.
- **Id-form references are matched by id only.** A reference whose instance part has the shape of a system-generated
id — `secret:…/DRV-<12 hex>/…` or `env:<PREFIX>DRV_<12 HEX>` alone or followed by `_` — is never matched against
driver **names**, so a driver named `drv` (env form `DRV`), or one named like another's id, cannot make every
sibling's id-form variable "another instance's", whether that name was created before or after this rule. New names
of that form are refused outright at create / rename / modal update (`DriverCredentialPolicy.IsReservedInstanceName`:
env form `DRV` or starting with `DRV_<12 HEX>`).
- **Name-form variables are only reliable for ASCII letter/digit names.** Normalisation turns every other character —
including non-ASCII letters — into `_`, so `Linie Süd` and `Linie S_d` share `LINIE_S_D`, and a name with no ASCII
letter or digit at all normalises to empty and gets **no** name protection (only its id does). Use the id form for
per-instance variables.
Rules worth knowing:
@@ -1439,11 +1463,24 @@ Rules worth knowing:
`secret:galaxy/<VictimName>/…`. Consequence: two same-type drivers that share a *name* cannot use name-scoped
secrets — scope them by **driver id** (`secret:galaxy/<DriverInstanceId>/…`, ids are unique) instead.
- A driver **being created** owns nothing yet, so any reference naming an existing instance is refused.
- Creating or renaming a driver whose name equals a segment other drivers already use as a *shared* name makes those
drivers' next deploy fail (`CredentialReferenceOtherInstance`) — rename the shared secret or the driver.
- **A driver's name is checked against the references other drivers store (#848).** Creating a driver — or renaming
one, through the `/raw` rename or the driver config modal — whose new name is the instance part of a reference
**stored on another same-type driver** (its DriverConfig or any DeviceConfig; `secret:` segment or `env:` part) is
refused at save (`DriverConfigSaveGate.ValidateNameAsync` → `DriverInstanceIdentities.DescribeNameChangeProblemsAsync`):
otherwise naming a driver `gw` (or `API`, for `env:GALAXY_MXGW_API_KEY`) would turn every other driver's shared
reference into "another instance's" and fail their next deploy. The message names the other driver, its id, the device
and the field path — never the reference or a value.
- **A driver cannot be renamed away from a name its own references are named after (#848).** If the driver's stored
references (or the config being saved with the rename) use its *current name* — `secret:galaxy/PlantA/…`,
`env:OTOPCUA_GALAXY_PLANTA_…` — and do not also name its id or the new name, the rename is refused: it would free
the old name, and that reference would then look like an unclaimed shared name to every other driver's save, Test
Connect and browse (the issue's "renaming the victim frees its name until the deploy gate runs" window). Move the
reference to the id form first (the config modal can do both in one save), then rename. A case-only rename changes
nothing the rule matches and is allowed. Id-scoped references are immune — ids never change.
One rule — `DriverCredentialPolicy.DescribeOtherInstanceProblems` — at the same seams as #815, reading the instance list
through `DriverInstanceIdentities` (only when the config carries an in-scope `secret:` reference at all):
through `DriverInstanceIdentities` (only when the config carries an in-scope `secret:` or `env:` reference at all — so
since #849 a browse of an `env:`-only config also needs the ConfigDb and fails closed without one):
1. **AdminUI save** — `DriverConfigSaveGate.ValidateAsync` (every `RawTreeService` driver/device create + update; the
owner is the driver itself, or the device's driver).
@@ -1477,8 +1514,24 @@ config at its next deploy, and the upgrade preflight / `/raw` banner lists it (D
the driver's own value under `galaxy/<its DriverInstanceId>/…` (or a shared name no instance uses) and point the field
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); a reference to a
Upgrade note (#848/#849): the `env:` rule is new, so a stored config whose variable starts with **another same-type
driver's id or name** (e.g. a Galaxy driver named `API` beside `env:GALAXY_MXGW_API_KEY` users) is refused at its next
deploy — the upgrade preflight / `/raw` banner lists it. Rename that driver (the #848 rename check allows it, since the
references naming it belong to other drivers — unless its own references use the name too, in which case move those to
its id form first) or move the other drivers' variables to their id form. Existing driver names are not
re-checked against stored references except when they change.
Residuals (accepted): a **deleted** driver's id and name stop protecting its secrets and variables — after the delete
any driver may reference `secret:galaxy/<deleted-id>/…` — so remove (or rotate) a deleted driver's secrets with it. The
#848 rename pin does not stop a Designer who may edit the victim driver: a **single** config-modal save can move (or
drop) its name-scoped reference and rename it in the same commit, after which the old name is free for any driver. That
is the same "may author the victim's config ⇒ may redirect it" property as a saved endpoint change below (the save is a
persisted, visible draft change, and the later credential use is audited) — use id-scoped references where this
matters. For the same reason a low-severity race is accepted: a rename and a **concurrent** device save that adds a
reference to the old name are not tied together (the driver's `RowVersion` does not cover its device rows), so both can
commit and leave a device reference named after a name the driver no longer has — again needs a Designer who may author that driver's
config, and audited at use. A variable is node-wide, so the `env:` rule
constrains which driver may *name* it, not who can read the process environment. 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
@@ -1503,7 +1556,9 @@ service account can read (another product's client certificate, a host certifica
(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.
a path past the gate. The upgrade preflight and the `/raw` banner run the same merged check
(`CredentialReferenceDeployChecks.MergedOnlyProblems`, shared with the gate) and list such a split as a Draft finding
"(merged with its devices)", so it shows before a deploy does (#850).
- **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