Commit Graph

1 Commits

Author SHA1 Message Date
Joseph Doherty 2193d9f2e4 fix(config): refuse to store a Sql credential in a node config override (#499)
`ClusterNode.DriverConfigOverridesJson` is the third surface a Sql driver's config
is persisted on, and the only one nothing gated. It is a map keyed by
`DriverInstanceId` merged onto the cluster-level `DriverConfig`, so a literal
`connectionString` pasted there leaks exactly as one pasted into the driver — the
leak #498 closed for `DriverInstance.DriverConfig` and `Device.DeviceConfig`.

Gated at the SAVE, not at the deploy — a deliberate departure from both options the
issue offered:

- `DraftSnapshot` carries no `ClusterNode` rows, and adding them would widen the
  snapshot and every builder of one for a single rule about a value that never
  enters the artifact.
- More to the point, a deploy gate is the wrong instrument here. Node overrides are
  not in the artifact, so blocking a deploy would not stop the credential being
  stored — it is already in the database and replicated by then. Refusing the save
  is the only point where "refuse to store it" is literally true, which is #498's
  own framing: discarding a secret on read is not the same as refusing to store it.

The check moves into a shared `SqlCredentialGuard` so the two enforcement points
cannot drift; `DraftValidator` now calls it instead of its own private helper.

Kept deliberately narrow — the `Sql` driver's `connectionString` only, and only for
instance ids that ARE Sql drivers. The issue asked whether to generalise to
credential-shaped keys across every driver type; not doing that, for the same
reason #498 did not: a broader sweep would start refusing configs that are
legitimate today for drivers which never made an indirect-credential guarantee,
which is a regression rather than defence in depth. Widen per driver, as each gains
its own contract.

The error names the offending driver instance(s) and NEVER the value — it reaches
the AdminUI and the audit trail.

14 new cases cover both halves, including the case variants (System.Text.Json binds
`ConnectionString` to `connectionString`, so a case variant is the same key, not a
bypass), non-Sql ids being ignored, and every blank/malformed/non-object shape.
2026-07-26 10:30:04 -04:00