docs(security): credential-reference namespaces + migration note (#815)
docs/security.md gains the canonical table of per-driver namespaces, the five
enforcement seams, the out-of-scope cases (appsettings ${secret:}, Sql
connectionStringRef) and the migration steps for existing configs. Galaxy,
OpcUaClient, Heidenhain, Configuration and driver-specs docs, the Galaxy
options XML docs, the drivers rule file and the docker-dev compose comment
point at it. docker-dev's env:GALAXY_MXGW_API_KEY stays valid (allowed legacy
prefix), so no rig change is needed.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -319,6 +319,14 @@ host logger factory (wired in `DriverFactoryBootstrap`; the FOCAS and TwinCAT fa
|
||||
`BadConfigurationError` on that role's reads only
|
||||
(`HeidenhainCredentialPersisted`). Hardware-unproven: the in-process simulator + Java golden vectors are the evidence;
|
||||
`Category=LiveIntegration` (`HEIDENHAIN_LSV2_HOST`) is the commissioning gate. `docs/drivers/Heidenhain.md`.
|
||||
- **Credential references are namespaced per driver (#815):** ONE table (`Core.Abstractions.DriverCredentialPolicy`) +
|
||||
ONE parser/resolver (`CredentialReferences`) for Galaxy (`secret:galaxy/`·`secret:otopcua/galaxy/`·`env:OTOPCUA_GALAXY_`·
|
||||
`env:GALAXY_MXGW_`), OpcUaClient (`secret:opcuaclient/`·`env:OTOPCUA_OPCUACLIENT_`) and Heidenhain. Enforced at save
|
||||
(`DriverConfigSaveGate`), deploy (`CredentialReferenceOutOfScope`), Test Connect (`AdminOperationsActor`, before the
|
||||
probe), browse (`BrowserSessionService`, before the browser) and at resolve time before any store/env read; `file:` is
|
||||
refused everywhere. A NEW driver that resolves a `secret:`/`env:` field and sends it to an authored endpoint MUST add a
|
||||
row there and resolve through `CredentialReferences` — never `ISecretResolver.GetAsync` on a raw authored name.
|
||||
`docs/security.md` §"Credential references are namespaced per driver".
|
||||
- **Native condition state survives a full rebuild (#641):** `RebuildAddressSpace` captures each NATIVE condition's
|
||||
live node state (client acks included), the same-apply re-materialise restores it, and `CompleteAddressSpaceApply`
|
||||
(called by `OpcUaPublishActor` at the end of every apply) reports ONE event per restored off-normal condition and
|
||||
|
||||
@@ -679,7 +679,8 @@ services:
|
||||
Serilog__MinimumLevel__Override__Microsoft.EntityFrameworkCore: "Warning"
|
||||
Serilog__MinimumLevel__Override__Microsoft.AspNetCore: "Warning"
|
||||
# Resolved at runtime by GalaxyDriver.ResolveApiKey when a DriverInstance's
|
||||
# Gateway.ApiKeySecretRef = "env:GALAXY_MXGW_API_KEY".
|
||||
# Gateway.ApiKeySecretRef = "env:GALAXY_MXGW_API_KEY" (#815: GALAXY_MXGW_* is an allowed
|
||||
# Galaxy env prefix alongside OTOPCUA_GALAXY_*; any other variable is refused).
|
||||
GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua2_GI7-tNozYE6cXGUSgEzL3AHDV7bYcYIHdMwKYgyHdX4}"
|
||||
# Pair-local config cache + REPLICATION ON — the site-a pair is the enablement demo.
|
||||
# site-a-1 is the initiator: it binds the h2c sync listener on 9001 AND dials the peer.
|
||||
|
||||
@@ -385,7 +385,7 @@ The Galaxy/MxAccess connection settings are **not an `appsettings` section.** Th
|
||||
|
||||
| Record | Key fields (default) | Meaning |
|
||||
|---|---|---|
|
||||
| `GalaxyGatewayOptions` (`Gateway`) | `Endpoint`; `ApiKeySecretRef`; `UseTls` (`true`); `CaCertificatePath` (`null`); `ConnectTimeoutSeconds` (`10`); `DefaultCallTimeoutSeconds` (`30`); `StreamTimeoutSeconds` (`0` = unlimited) | mxaccessgw gateway connection. `ApiKeySecretRef` supports `env:NAME` / `file:PATH` / `dev:KEY` / literal forms (resolved at `InitializeAsync`); prefer `env:`/`file:` in production. Never store a cleartext key. |
|
||||
| `GalaxyGatewayOptions` (`Gateway`) | `Endpoint`; `ApiKeySecretRef`; `UseTls` (`true`); `CaCertificatePath` (`null`); `ConnectTimeoutSeconds` (`10`); `DefaultCallTimeoutSeconds` (`30`); `StreamTimeoutSeconds` (`0` = unlimited) | mxaccessgw gateway connection. `ApiKeySecretRef` supports `secret:galaxy/<name>` (or `secret:otopcua/galaxy/<name>`) / `env:OTOPCUA_GALAXY_<NAME>` (or legacy `env:GALAXY_MXGW_<NAME>`) / `dev:KEY` / literal forms (resolved at `InitializeAsync`); prefer `secret:` in production. Any other `secret:`/`env:` reference and every `file:` reference is refused at save, deploy (`CredentialReferenceOutOfScope`), Test Connect, browse and resolve — #815, see `docs/security.md` §"Credential references are namespaced per driver". Never store a cleartext key. |
|
||||
| `GalaxyMxAccessOptions` (`MxAccess`) | `ClientName`; `PublishingIntervalMs` (`1000`); `WriteUserId` (`0` = anonymous); `EventPumpChannelCapacity` (`50000`) | MXAccess client identity + tuning. `ClientName` **must be unique per OtOpcUa instance** (redundancy pairs enforce this). |
|
||||
| `GalaxyRepositoryOptions` (`Repository`) | `DiscoverPageSize` (`5000`); `WatchDeployEvents` (`true`) | Galaxy Repository browse paging + deploy-event watching. |
|
||||
| `GalaxyReconnectOptions` (`Reconnect`) | `InitialBackoffMs` (`500`); `MaxBackoffMs` (`30000`); `ReplayOnSessionLost` (`true`) | In-driver reconnect-supervisor backoff. |
|
||||
|
||||
@@ -81,7 +81,7 @@ Project root files:
|
||||
|
||||
`DriverConfig` JSON binds to `Config/GalaxyDriverOptions.cs`. The four sections are:
|
||||
|
||||
- **`Gateway`** — endpoint, API key secret ref, TLS knobs, connect/call/stream timeouts. `StreamTimeoutSeconds = 0` keeps the long-lived `StreamEvents` RPC open for the driver's lifetime.
|
||||
- **`Gateway`** — endpoint, API key secret ref (`secret:galaxy/<name>` · `secret:otopcua/galaxy/<name>` · `env:OTOPCUA_GALAXY_<NAME>` · legacy `env:GALAXY_MXGW_<NAME>` · `dev:KEY` · literal; anything else, incl. `file:`, is refused — #815, [`docs/security.md`](../security.md) §"Credential references are namespaced per driver"), TLS knobs, connect/call/stream timeouts. `StreamTimeoutSeconds = 0` keeps the long-lived `StreamEvents` RPC open for the driver's lifetime.
|
||||
- **`MxAccess`** — `ClientName` (must be unique per OtOpcUa instance — redundancy pairs enforce uniqueness at install time), `PublishingIntervalMs` (forwarded as `buffered_update_interval_ms` on subscribe), `WriteUserId` for ArchestrA secured-write, `EventPumpChannelCapacity` (default 50_000 — one second of headroom at 50k tags / 1Hz; tune via the `galaxy.events.dropped` metric).
|
||||
- **`Repository`** — `DiscoverPageSize`, `WatchDeployEvents`.
|
||||
- **`Reconnect`** — `InitialBackoffMs`, `MaxBackoffMs`, `ReplayOnSessionLost` (calls the gateway's `ReplaySubscriptions` RPC after reconnect rather than re-issuing subscribe-bulk for every tag).
|
||||
@@ -103,8 +103,10 @@ Galaxy password, so `GalaxyAckCredentialResolver` picks the account per acknowle
|
||||
not silently downgraded to the fallback.
|
||||
2. **The driver's fallback account** — `AlarmAck.FallbackUser` / `FallbackDomain` /
|
||||
`FallbackPasswordSecretRef` in `DriverConfig`, used by keys without an identity and by anonymous
|
||||
sessions. The password ref must be `secret:NAME` / `${secret:NAME}` (production), `env:NAME`,
|
||||
`file:PATH` or `dev:VALUE`; a bare literal is refused so a password never sits in DriverConfig.
|
||||
sessions. The password ref must be `secret:galaxy/NAME` / `secret:otopcua/galaxy/NAME` (or the `${secret:…}`
|
||||
spelling; production), `env:OTOPCUA_GALAXY_NAME` / `env:GALAXY_MXGW_NAME`, or `dev:VALUE`; a bare literal, a
|
||||
`file:` reference or a reference outside the Galaxy namespace is refused (#815) — the password is sent to the
|
||||
authored gateway endpoint, so it must not be able to name another subsystem's secret.
|
||||
3. **Neither** → the driver throws `DriverOperationRefusedException`; **nothing is sent to the
|
||||
gateway**. The refusal is excluded from the resilience breaker (it says nothing about gateway
|
||||
health). How the client learns of it (#760):
|
||||
|
||||
@@ -91,7 +91,11 @@ Host at their own machine. Anything outside the namespace is refused by **every*
|
||||
(`HeidenhainCredentials`, fail-closed independently of earlier validation), the AdminUI **save path**
|
||||
(`RawTreeService` create/update driver + device refuse the config server-side via `DriverConfigSaveGate`, and the
|
||||
driver/device modals disable Save while it is invalid — so a literal never reaches the config database), and the
|
||||
deploy gate `HeidenhainCredentialPersisted`. Messages never echo the value.
|
||||
deploy gate `HeidenhainCredentialPersisted`. Messages never echo the value. Since #815 the parser and resolver are
|
||||
the shared `CredentialReferences` / `DriverCredentialPolicy` (`Core.Abstractions`) that Galaxy and OpcUaClient use
|
||||
too, and Test Connect / live browse also refuse an out-of-namespace reference before any probe or browser runs; a
|
||||
prefix in another casing (`SECRET:`) is refused, and `${secret:heidenhain/<name>}` is accepted as a spelling of
|
||||
`secret:heidenhain/<name>`.
|
||||
|
||||
Resolved **at each login**, never held in options or session state. Fail-closed: an absent secret or unset
|
||||
variable fails the login rather than sending the reference text. An unusable **INSPECT** credential fails the
|
||||
|
||||
@@ -81,6 +81,12 @@ for every field (security policy/mode, auth type, session timeout, keep-alive,
|
||||
browse root, node/depth caps). `ReconnectPeriod` was **deleted** (#676 — unused since #670 removed the SDK reconnect
|
||||
handler, and the AdminUI no longer shows it); stored configs that still carry `reconnectPeriod` keep parsing in the
|
||||
factory, the Test-Connect probe and the browse picker (unknown keys are skipped), and the form drops it on the next save.
|
||||
**Credentials (#815).** `Password` / `UserCertificatePassword` are a literal, `secret:opcuaclient/<name>` (the node's
|
||||
secret store) or `env:OTOPCUA_OPCUACLIENT_<NAME>`. They are resolved on the node and sent to the configured endpoint,
|
||||
so any other `secret:`/`env:` reference (and `file:`) is refused at save, deploy (`CredentialReferenceOutOfScope`), Test
|
||||
Connect, browse and again at session open, before anything is read. Migrate an existing `secret:<name>` by re-creating
|
||||
the secret as `opcuaclient/<name>`. See [`docs/security.md`](../security.md) §"Credential references are namespaced per driver".
|
||||
|
||||
`ProbeTimeoutSeconds` (default 15, AdminUI "Probe timeout") caps the Test-Connect probe's deadline — it can shorten
|
||||
the budget the host hands the probe, never extend it (review 2026-10-21 O-4; it used to be authored and consumed by nothing).
|
||||
|
||||
|
||||
@@ -1144,6 +1144,67 @@ Supporting hygiene, none of which is the control:
|
||||
If a deployment needs the browse surface narrower than "anyone who may author config", the lever is the
|
||||
page policy (and the LDAP group→role mapping behind it), not the browser.
|
||||
|
||||
#### Credential references are namespaced per driver (#815)
|
||||
|
||||
The outbound-connect capability above has a sharper edge whenever the config carries a **credential
|
||||
reference**: a Designer authors both the endpoint *and* the reference, and the node resolves the reference
|
||||
and sends the value to that endpoint — on Test Connect, on a live browse, and at runtime. Before #815 an
|
||||
unscoped reference let anyone who may author config exfiltrate any value the admin node could read, with no
|
||||
deploy: point a Galaxy `Gateway.Endpoint` at their own host, set `ApiKeySecretRef` to
|
||||
`env:ConnectionStrings__ConfigDb` (or `env:ServerHistorian__ApiKey`, or `file:<any path>`), and click Test
|
||||
Connect / browse; or set an OpcUaClient `Password` to `secret:<any store entry>`.
|
||||
|
||||
Every operator-authored reference is now confined to its driver's namespace — one table,
|
||||
`DriverCredentialPolicy` (`Core.Abstractions`), one parser/resolver, `CredentialReferences`:
|
||||
|
||||
| Driver | Fields | Allowed references | Literal |
|
||||
|---|---|---|---|
|
||||
| `GalaxyMxGateway` | `Gateway.ApiKeySecretRef` | `secret:galaxy/<name>`, `secret:otopcua/galaxy/<name>`, `env:OTOPCUA_GALAXY_<NAME>`, legacy `env:GALAXY_MXGW_<NAME>` | allowed (warns), `dev:<key>` |
|
||||
| `GalaxyMxGateway` | `AlarmAck.FallbackPasswordSecretRef` | same | `dev:<value>` only |
|
||||
| `OpcUaClient` | `Password`, `UserCertificatePassword` | `secret:opcuaclient/<name>`, `env:OTOPCUA_OPCUACLIENT_<NAME>` | allowed |
|
||||
| `Heidenhain` | `InspectPassword`, `DncPassword`, `DataPassword` | `secret:heidenhain/<name>`, `env:OTOPCUA_HEIDENHAIN_<NAME>` (#803) | refused |
|
||||
|
||||
Rules (all drivers): the prefixes are lower-case and exact (`SECRET:` / `Env:` are refused, never treated
|
||||
as a literal); `${secret:<name>}` is accepted as a spelling of `secret:<name>`; secret names are
|
||||
`[A-Za-z0-9._-]` segments with no empty, `.` or `..` segment; the variable suffix is `[A-Z0-9_]+`; **`file:` is
|
||||
refused everywhere** (Galaxy used to read any file the node could open). A field is matched at any depth of
|
||||
the DriverConfig / DeviceConfig, case-insensitively, as the drivers bind it.
|
||||
|
||||
Enforced, fail-closed, at every seam — the earlier ones exist for a clear message, the last one is the guarantee:
|
||||
|
||||
1. **AdminUI save** — `DriverConfigSaveGate` (`RawTreeService` driver/device create + update): the config is
|
||||
never stored.
|
||||
2. **Deploy gate** — `CredentialReferenceDeployChecks`, code **`CredentialReferenceOutOfScope`** (Heidenhain keeps
|
||||
`HeidenhainCredentialPersisted`).
|
||||
3. **Test Connect** — `AdminOperationsActor` refuses before any probe runs (`"Test Connect refused: …"`).
|
||||
4. **Live browse** — `BrowserSessionService.OpenAsync` refuses before any bespoke or universal browser opens.
|
||||
5. **Resolve time** — `GalaxySecretRef`, `GalaxyAckCredentialResolver`, `OpcUaUserIdentityFactory` (driver +
|
||||
browser) and `HeidenhainCredentials` all resolve through `CredentialReferences`, which re-checks the scope
|
||||
**before** any secret-store or environment read.
|
||||
|
||||
Messages name the field and the allowed forms, never the value. Out of scope by design: `${secret:}` tokens in
|
||||
`appsettings.json` (host config, not operator-authored), and the `Sql` driver's `connectionStringRef`, which is
|
||||
already a fixed prefix (`Sql__ConnectionStrings__<ref>`) whose value carries its own server — a Designer can
|
||||
choose *which* provisioned database a ref names, but cannot redirect its credentials to another host.
|
||||
|
||||
**Migration (existing configs).** After the upgrade, a stored config with an out-of-namespace reference fails
|
||||
its next deploy (`CredentialReferenceOutOfScope`), its Test Connect / browse, and — for a running driver — its
|
||||
next (re)initialise, which faults the driver with the same message. To migrate:
|
||||
|
||||
- **Galaxy API key** — `env:GALAXY_MXGW_API_KEY` (the docker-dev rig and docs' name) keeps working. Any other
|
||||
variable: rename it to `OTOPCUA_GALAXY_<NAME>` on every node that runs the driver *and* the admin nodes
|
||||
(Test Connect / browse), and set `env:OTOPCUA_GALAXY_<NAME>`. A store secret already named `galaxy/…` keeps
|
||||
working; otherwise re-create it as `galaxy/<inst>/apikey` (`/admin/secrets` or the `secret` CLI). A
|
||||
`file:<path>` key must move into the store or a namespaced variable.
|
||||
- **Galaxy fallback ack password** — the documented `secret:otopcua/galaxy/…` name keeps working; `env:` / `file:`
|
||||
references move as above.
|
||||
- **OpcUaClient** — re-create `secret:<name>` as `opcuaclient/<name>` (and update the field), or move an
|
||||
environment value to `OTOPCUA_OPCUACLIENT_<NAME>`. Literal passwords are unaffected.
|
||||
|
||||
Residual (accepted): within one namespace, a Designer can still point one driver instance at another
|
||||
instance's secret (e.g. `secret:galaxy/<other-inst>/apikey`) — the value then reaches the endpoint the Designer
|
||||
chose. Per-instance scoping is a possible follow-up.
|
||||
|
||||
### Role grant source
|
||||
|
||||
Admin reads `LdapGroupRoleMapping` rows from the Config DB (`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/LdapGroupRoleMapping.cs`) — the same pattern as the data-plane `NodeAcl` but scoped to Admin roles + (optionally) one cluster for multi-site fleets (a system-wide row, `IsSystemWide = true`, stacks additively with cluster-scoped rows). The `RoleGrants.razor` page lets `Administrator`s edit these mappings without leaving the UI.
|
||||
|
||||
@@ -51,7 +51,7 @@ Per `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Config/GalaxyDriverOptions.cs`:
|
||||
{
|
||||
"Gateway": {
|
||||
"Endpoint": "http://localhost:5120",
|
||||
"ApiKeySecretRef": "secret:galaxy-gw-api-key",
|
||||
"ApiKeySecretRef": "secret:galaxy/gw/apikey",
|
||||
"UseTls": true,
|
||||
"CaCertificatePath": null,
|
||||
"ConnectTimeoutSeconds": 10,
|
||||
|
||||
@@ -64,9 +64,11 @@ public sealed record GalaxyDriverOptions(
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="FallbackPasswordSecretRef"/> must be a reference — <c>secret:NAME</c> (production),
|
||||
/// <c>env:NAME</c>, <c>file:PATH</c> or <c>dev:VALUE</c> (rigs only). A bare literal is rejected so a
|
||||
/// Galaxy password can never sit in cleartext in the DriverConfig JSON / config DB.
|
||||
/// <see cref="FallbackPasswordSecretRef"/> must be a Galaxy-namespaced reference (lmxopcua#815) —
|
||||
/// <c>secret:galaxy/NAME</c> / <c>secret:otopcua/galaxy/NAME</c> (production), <c>env:OTOPCUA_GALAXY_NAME</c>
|
||||
/// / <c>env:GALAXY_MXGW_NAME</c>, or <c>dev:VALUE</c> (rigs only). A bare literal, <c>file:</c> and any other
|
||||
/// reference are rejected so a Galaxy password never sits in cleartext in the config DB and the field can
|
||||
/// never name another subsystem's secret.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="FallbackUser">Galaxy user the fallback acknowledges as; null/empty = no fallback.</param>
|
||||
@@ -84,20 +86,16 @@ public sealed record GalaxyAlarmAckOptions(
|
||||
|
||||
/// <summary>
|
||||
/// Connection details for the MxAccess gateway. <see cref="ApiKeySecretRef"/> is
|
||||
/// resolved by <see cref="GalaxySecretRef.ResolveApiKeyAsync"/> at InitializeAsync time. Five forms
|
||||
/// supported, in priority order:
|
||||
/// resolved by <see cref="GalaxySecretRef.ResolveApiKeyAsync"/> at InitializeAsync time. References are confined
|
||||
/// to the Galaxy namespace (lmxopcua#815 — the key is sent to the authored <c>Endpoint</c>):
|
||||
/// <list type="bullet">
|
||||
/// <item><c>env:NAME</c> — read from an environment variable (recommended for
|
||||
/// production; the central config DB holds only the indirection, not the key).</item>
|
||||
/// <item><c>file:PATH</c> — read from an ACL'd file outside the repo.</item>
|
||||
/// <item><c>secret:NAME</c> — resolved through the shared <c>ZB.MOM.WW.Secrets</c>
|
||||
/// <item><c>secret:galaxy/NAME</c> (or <c>secret:otopcua/galaxy/NAME</c>) — the shared <c>ZB.MOM.WW.Secrets</c>
|
||||
/// encrypted store; fail-closed if absent (the production path).</item>
|
||||
/// <item><c>dev:KEY</c> — explicit cleartext opt-in for dev rigs / parity tests;
|
||||
/// no startup warning.</item>
|
||||
/// <item>Anything else — treated as a literal cleartext API key for back-compat.
|
||||
/// The resolver emits a <c>Warning</c> at startup so an operator who accidentally
|
||||
/// committed a cleartext key sees it; production should migrate to <c>env:</c>
|
||||
/// or <c>file:</c>.</item>
|
||||
/// <item><c>env:OTOPCUA_GALAXY_NAME</c> (or legacy <c>env:GALAXY_MXGW_NAME</c>) — an environment variable.</item>
|
||||
/// <item><c>dev:KEY</c> — explicit cleartext opt-in for dev rigs / parity tests; no startup warning.</item>
|
||||
/// <item>Any other unprefixed string — a literal cleartext API key for back-compat, with a startup
|
||||
/// <c>Warning</c>.</item>
|
||||
/// <item>Any other <c>secret:</c>/<c>env:</c> reference, and every <c>file:</c> reference — refused.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
// PR 6.5 tuning notes:
|
||||
|
||||
Reference in New Issue
Block a user