docs(components): normalize Secrets component + index the shared lib
Add components/secrets/ (SPEC, realized shared-contract, code-verified current-state for OtOpcUa/mxaccessgw/ScadaBridge, GAPS adoption backlog) and register the Secrets row in CLAUDE.md + components/README.md. The ZB.MOM.WW.Secrets lib is built + published 0.1.2 + reference-consumer-proven (HistorianGateway, live vs the wonder historian); per-app adoption is the tracked follow-on.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
# MxAccessGateway — secrets current state
|
||||
|
||||
Descriptive baseline (code-verified) of how `mxaccessgw` handles secrets **today**, before
|
||||
adopting `ZB.MOM.WW.Secrets`. Paths are relative to `~/Desktop/MxAccessGateway`.
|
||||
|
||||
## Where each secret lives today
|
||||
|
||||
| Secret | Location today | At-rest protection |
|
||||
|---|---|---|
|
||||
| gRPC inbound API keys | Peppered HMAC-SHA256 hashes in SQLite `gateway-auth.db` (`MxGateway:Authentication:SqlitePath`, `…Server/appsettings.json:17`) via `ZB.MOM.WW.Auth.ApiKeys` (`…/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:70`) | Hashed (not reversible); plaintext key never stored |
|
||||
| API-key **pepper** | Read by config-key **name** `MxGateway:ApiKeyPepper`; value supplied out-of-band (env `MxGateway__ApiKeyPepper`), intentionally **not** in appsettings (`AuthStoreServiceCollectionExtensions.cs:36,54-55`) | Runtime-supplied; no dedicated env reader (default config binding); flagged open item SEC-10 |
|
||||
| LDAP dashboard bind password | **Plaintext** `appsettings.json:29` `ServiceAccountPassword: "serviceaccount123"`; also a hardcoded default in `Configuration/LdapOptions.cs:61` | None |
|
||||
| Galaxy-repository SQL connection string | **Plaintext** `appsettings.json:76` (currently `Integrated Security=True`, but the field is a raw connstr that would carry `Password=` in plaintext) | None (redacted for **display** only, `Dashboard/DashboardConnectionStringDisplay.cs:10-24`) |
|
||||
| TLS PFX | Generated/persisted with a **null password**, ACL-protected on disk (`Security/Tls/SelfSignedCertificateProvider.cs:153,193`) | Filesystem ACL, no password |
|
||||
| MXAccess/Wonderware creds | **Not stored** — pass-through runtime gRPC request payload forwarded to COM (`Worker/MxAccess/MxAccessCommandExecutor.cs:318-325`) | N/A |
|
||||
|
||||
## Encryption-at-rest today
|
||||
|
||||
- **None** for the secrets above. The only Data Protection use is short-lived SignalR hub
|
||||
bearer tokens (`Dashboard/HubTokenService.cs:43-46`) — and Data Protection is **never
|
||||
explicitly configured** (no `AddDataProtection`/`PersistKeysTo`/`ProtectKeysWith`/
|
||||
`SetApplicationName` anywhere), so the key ring falls back to the ASP.NET default. Not
|
||||
applied to any credential.
|
||||
|
||||
## Config expansion today
|
||||
|
||||
- **None.** Stock `WebApplication.CreateBuilder` chain (`GatewayApplication.cs:60`); no
|
||||
`${...}` expansion, no custom config post-processing before options binding. Secrets are
|
||||
read verbatim from resolved config values.
|
||||
|
||||
## Adoption plan (toward SPEC)
|
||||
|
||||
1. **Highest value first:** move the plaintext **LDAP `ServiceAccountPassword`** and the
|
||||
**Galaxy connection-string password** behind `${secret:ldap/mxgateway/bind}` /
|
||||
`${secret:sql/mxgateway/galaxy}` — pre-host `SecretReferenceExpander` before the existing
|
||||
`GatewayOptionsValidator` runs. Delete the hardcoded plaintext defaults in `LdapOptions`.
|
||||
2. **Pepper:** the pepper is already runtime-supplied by design — optionally source it via
|
||||
`${secret:apikey-pepper/mxgateway}` to close SEC-10's "no dedicated env reader" gap while
|
||||
keeping it out of appsettings.
|
||||
3. **Master key:** env provider (`ZB_SECRETS_MASTER_KEY`) for the Windows/NSSM deployment,
|
||||
or DPAPI provider (Windows box) if a machine-bound key is preferred.
|
||||
4. **UI:** mount `/admin/secrets` in the existing Blazor dashboard; map the gateway's
|
||||
`Administrator` role onto `secrets:manage`/`secrets:reveal`.
|
||||
5. **Keep bespoke:** the peppered-HMAC API-key store (already best-practice — hashed, not
|
||||
encrypted-reversible) and the ACL-protected passwordless TLS PFX stay as-is; MXAccess
|
||||
pass-through creds are out of scope (never stored).
|
||||
@@ -0,0 +1,54 @@
|
||||
# OtOpcUa — secrets current state
|
||||
|
||||
Descriptive baseline (code-verified) of how OtOpcUa handles secrets **today**, before
|
||||
adopting `ZB.MOM.WW.Secrets`. Paths are relative to `~/Desktop/OtOpcUa`.
|
||||
|
||||
## Where each secret lives today
|
||||
|
||||
| Secret | Location today | At-rest protection |
|
||||
|---|---|---|
|
||||
| ConfigDb (SQL Server) connstr password | Connection string `ConfigDb` / env `OTOPCUA_CONFIG_CONNECTION` (`…Configuration/ServiceCollectionExtensions.cs:10,21-25`; `DesignTimeDbContextFactory.cs:16,25-31` — "Do not add a plaintext password as a fallback"); not in committed appsettings | Whatever config source supplies it (plaintext) |
|
||||
| LDAP service-account password | `Security:Ldap:ServiceAccountPassword` (`Security/Ldap/LdapOptions.cs:22,60,63`, bound `Security/ServiceCollectionExtensions.cs:41`); not in committed appsettings | None |
|
||||
| Deploy API key | `Security:DeployApiKey` compared via `FixedTimeEquals` (`AdminUI/Api/DeployApiEndpoints.cs:30-54`); disabled when unset | Plaintext config value |
|
||||
| HistorianGateway client API key | Env `ServerHistorian__ApiKey` (`Runtime/Historian/ServerHistorianOptions.cs:41-46`; appsettings ships `""` + "NEVER commit") | None (env at rest) |
|
||||
| Galaxy/MxAccess gateway API key | `GalaxyDriverOptions.ApiKeySecretRef` resolved by `GalaxySecretRef.cs:43-88` into 4 forms: `env:` / `file:` / `dev:` / literal; stored in the driver `DriverConfig` JSON in the **central config DB** | `env:`/`file:` indirect out; `dev:`/literal are **cleartext in the DB** (self-documented `GalaxySecretRef.cs:86`) |
|
||||
| OPC UA client-driver session + PFX passwords | `OpcUaClientDriverOptions.Password` / `UserCertificatePassword` in the driver `DriverConfig` JSON in the config DB (`Driver.OpcUaClient/OpcUaClientDriver.cs:382,495`) | **Plaintext** in the config DB |
|
||||
| JWT signing key | `Security:Jwt:SigningKey` (HS256, `Jwt/JwtOptions.cs:5-9`); not in committed appsettings | Plaintext config value |
|
||||
|
||||
End-user login/OPC-UA passwords flow in-memory only (never persisted); the desktop client
|
||||
deliberately re-prompts each launch.
|
||||
|
||||
## Encryption-at-rest today
|
||||
|
||||
- The only mechanism is **ASP.NET Core Data Protection** for the auth cookie/JWT payload —
|
||||
**not** for any credential above: `Security/ServiceCollectionExtensions.cs:73-75`
|
||||
(`AddDataProtection().PersistKeysToDbContext<OtOpcUaConfigDbContext>().SetApplicationName("OtOpcUa")`),
|
||||
key ring in the ConfigDb `DataProtectionKeys` table (`OtOpcUaConfigDbContext.cs:70-73`).
|
||||
No DPAPI, no `ProtectKeysWith*` — the key ring itself is unencrypted-at-rest in SQL.
|
||||
- **Log redaction** (not at-rest): `Configuration/LocalCache/ResilientConfigReader.cs:76-89`
|
||||
scrubs `Password|Pwd|User Id|AccessToken|Api-Key` fragments before logging.
|
||||
|
||||
## Config expansion today
|
||||
|
||||
- **None** as a general mechanism (no `${...}` expander; stock
|
||||
`AddJsonFile`+`AddEnvironmentVariables`+`AddCommandLine`, `Host/Program.cs:59-73`). The one
|
||||
partial exception is the driver-level **`GalaxySecretRef`** (`env:`/`file:`/`dev:`/literal
|
||||
indirection, `GalaxySecretRef.cs:23`) which already anticipates "a future PR can swap any
|
||||
of these arms for a DPAPI-backed lookup" — a natural hook for `${secret:}`.
|
||||
|
||||
## Adoption plan (toward SPEC)
|
||||
|
||||
1. **Highest value first:** the driver `DriverConfig` secrets that sit **cleartext in the
|
||||
config DB** — the OpcUaClient `Password`/`UserCertificatePassword` and the `dev:`/literal
|
||||
Galaxy API-key arm. Add a `secret:` arm to `GalaxySecretRef` (and the OpcUaClient options)
|
||||
resolving through `ISecretResolver`, replacing the cleartext-in-DB path.
|
||||
2. **Pre-host `${secret:}`** for `Security:Jwt:SigningKey`, `Security:Ldap:ServiceAccountPassword`,
|
||||
`Security:DeployApiKey`, and the `ConfigDb` connstr password — expander runs before the
|
||||
existing `Ldap`/`OpcUa` options validators.
|
||||
3. **Master key:** env provider (`ZB_SECRETS_MASTER_KEY`) per node. **Clustered:** OtOpcUa is
|
||||
Akka-clustered, so every node must resolve the **same KEK** — use `FileMasterKeyProvider`
|
||||
with a shared mounted key, and when the `ZB.MOM.WW.Secrets.Akka` replicator is built, adopt
|
||||
it (or point `ISecretStore` at the shared ConfigDb SQL store). See SPEC "Clustered pairs".
|
||||
4. **UI:** mount `/admin/secrets` in the AdminUI; map OtOpcUa's admin role onto the two policies.
|
||||
5. **Keep bespoke:** the Data-Protection cookie/JWT key ring, in-memory end-user passwords,
|
||||
and the file-system OPC UA PKI Directory stores.
|
||||
@@ -0,0 +1,61 @@
|
||||
# ScadaBridge — secrets current state
|
||||
|
||||
Descriptive baseline (code-verified) of how ScadaBridge handles secrets **today**, before
|
||||
adopting `ZB.MOM.WW.Secrets`. Paths are relative to `~/Desktop/ScadaBridge`.
|
||||
|
||||
## Where each secret lives today
|
||||
|
||||
| Secret | Location today | At-rest protection |
|
||||
|---|---|---|
|
||||
| MSSQL ConfigDb password | `ScadaBridge:Database:ConfigurationDb` connstr (`Host/Program.cs:221-223`); committed dev plaintext `docker/central-node-a/appsettings.Central.json:21` | **Plaintext** |
|
||||
| Central `dbo` MachineDataDb password | `ScadaBridge:Database:MachineDataDb` (`StartupValidator.cs:63-65`; `appsettings.Central.json:22`) | **Plaintext** |
|
||||
| Site SQLite | `SiteDbPath` / StoreAndForward `SqliteDbPath` (`appsettings.Site.json:26,34`) — plain file paths | None (no SQLCipher) |
|
||||
| LDAP service-account password | `ScadaBridge:Security:Ldap:ServiceAccountPassword` via `ZB.MOM.WW.Auth.Ldap` (`Program.cs:140-142`); committed `serviceaccount123` `appsettings.Central.json:32` (+ loose `ldap_login.txt` etc. at repo root) | **Plaintext** |
|
||||
| Inbound `/api` keys | Hashed (peppered-HMAC) in SQLite `data/inbound-api-keys.sqlite` via `ZB.MOM.WW.Auth.ApiKeys` (`Program.cs:184-217`); legacy SQL-Server key store **retired** (`20260602092753_RetireInboundApiKeyStore`) | Hashed (not reversible) |
|
||||
| API-key **pepper** | `ScadaBridge:InboundApi:ApiKeyPepper` (`InboundAPI/InboundApiOptions.cs:23-36`); committed dev `dev-only-insecure-pepper-…` `docker/docker-compose.yml:18,45` (both central nodes share) | **Plaintext** env |
|
||||
| JWT signing key | `ScadaBridge:Security:JwtSigningKey` (HS256, `Security/SecurityOptions.cs:34`); `appsettings.Central.json:38` | **Plaintext** |
|
||||
| `DatabaseConnectionDefinition.ConnectionString` | ConfigDb column | **Encrypted** (Data Protection — see below) |
|
||||
| MxGateway per-endpoint `ApiKey` | Inside `DataConnection.PrimaryConfiguration`/`BackupConfiguration` JSON in ConfigDb; column mapped **without** the encrypting converter (`ConfigurationDatabase/Configurations/SiteConfiguration.cs:52-56`) | **Plaintext in ConfigDb** (redacted for display/audit only, `ConfigSecretScrubber.cs:18-19`) |
|
||||
| Akka remoting | HOCON `AkkaHostedService.cs:250-308` — hostname/port only | **No secure-cookie / no TLS / no shared secret** (plain TCP) |
|
||||
|
||||
## Encryption-at-rest today (the one existing analog)
|
||||
|
||||
- **`EncryptedStringConverter`** (ASP.NET Core Data Protection EF value converter,
|
||||
`ConfigurationDatabase/EncryptedStringConverter.cs:20-52`, purpose
|
||||
`"…ConfigurationDatabase.EncryptedColumn"`) encrypts **four** ConfigDb columns
|
||||
(`ScadaBridgeDbContext.cs:319-348`): `SmtpConfiguration.Credentials`,
|
||||
`SmsConfiguration.AuthToken`, `ExternalSystemDefinition.AuthConfiguration`,
|
||||
`DatabaseConnectionDefinition.ConnectionString`.
|
||||
- **Key ring** lives **in ConfigDb** (`IDataProtectionKeyContext` + `DataProtectionKeys`,
|
||||
`AddDataProtection().PersistKeysToDbContext<ScadaBridgeDbContext>()`) — shared across
|
||||
central nodes. **No `SetApplicationName`** (discriminator = content-root path) and **no
|
||||
`ProtectKeysWith*`** — the key-ring keys are stored **unencrypted** in the DB; the DB is
|
||||
the only protection boundary.
|
||||
|
||||
## Config expansion today
|
||||
|
||||
- **None functional.** Stock `AddJsonFile`+`AddEnvironmentVariables`+`AddCommandLine`
|
||||
(`Program.cs:38-43`). The `${SCADABRIDGE_*}` tokens in `appsettings.Central.json:23,33,35`
|
||||
are **literal, non-functional placeholders** — the real values arrive by **whole-key env
|
||||
override** (`ScadaBridge__Security__Ldap__ServiceAccountPassword`, etc.), documented in the
|
||||
`_secrets` note (`appsettings.Central.json:21`). `StartupValidator` (`ConfigPreflight` from
|
||||
the shared `ZB.MOM.WW.Configuration` package) validates presence/shape only — no resolution.
|
||||
|
||||
## Adoption plan (toward SPEC)
|
||||
|
||||
1. **Formalize the existing placeholders:** the non-functional `${SCADABRIDGE_*}` +
|
||||
whole-key-override convention is exactly the pattern `${secret:}` replaces — swap the
|
||||
plaintext ConfigDb/MachineDataDb passwords, LDAP password, JWT signing key, and pepper to
|
||||
`${secret:…}` tokens resolved pre-host, **before** `StartupValidator`/`ConfigPreflight`
|
||||
runs. Delete the committed dev plaintext + the loose `*_login.txt` files.
|
||||
2. **Close the MxGateway `ApiKey` plaintext-in-ConfigDb gap** — either extend
|
||||
`EncryptedStringConverter` to that JSON column or resolve it via `ISecretResolver`.
|
||||
3. **Clustered sync:** ScadaBridge is Akka-clustered (central pair + site nodes). Two SPEC-
|
||||
sanctioned options: (a) point `ISecretStore` at the **shared ConfigDb (SQL-Server store)**
|
||||
— one source of truth, mirrors how the Data-Protection key ring is already shared; or
|
||||
(b) adopt the `ZB.MOM.WW.Secrets.Akka` replicator (built on adoption) with a shared KEK
|
||||
(`FileMasterKeyProvider`, same mounted key on every node — hard requirement).
|
||||
4. **UI:** mount `/admin/secrets` in CentralUI; map ScadaBridge's admin role onto the policies.
|
||||
5. **Keep bespoke:** the existing Data-Protection column encryption can coexist (or migrate);
|
||||
the peppered-HMAC inbound-key SQLite store stays (already hashed). Akka remoting
|
||||
auth/TLS is a separate hardening concern, not this component.
|
||||
Reference in New Issue
Block a user