# ZB.MOM.WW.Secrets A reusable, envelope-encrypted **secrets manager** for the `ZB.MOM.WW.*` SCADA family: store secrets (SQL passwords, API tokens, connection strings) encrypted at rest and get the plaintext back on demand — from application code, from configuration, or from an operator UI. ## Packages | Package | Purpose | |---|---| | `ZB.MOM.WW.Secrets.Abstractions` | Contracts only (`ISecretStore`, `ISecretResolver`, `IMasterKeyProvider`, `ISecretCipher`, `ISecretReplicator`, `ISecretCacheInvalidator`, `ISecretActorAccessor`), value types, exceptions. Dependency-light. | | `ZB.MOM.WW.Secrets` | Implementation: AES-256-GCM envelope cipher, env/file/DPAPI master-key providers, SQLite store, TTL resolver (audited), `${secret:}` config expander, `AddZbSecrets`. | | `ZB.MOM.WW.Secrets.Ui` | Blazor RCL on `ZB.MOM.WW.Theme`: list / add / rotate / delete + policy-gated, audited reveal. | A `secret` CLI (`set` / `get` / `list` / `rm` / `rotate`) ships in the repo (not packed). ## How it protects secrets Each value is encrypted with a fresh **data key (DEK)** using AES-256-GCM; the DEK is then **wrapped** by a **master KEK** that never touches the database. The AAD binds the ciphertext to the secret's name and the wrapped DEK to the KEK id, so rows can't be swapped and a tampered/mis-keyed row **fails closed** (`SecretDecryptionException`) rather than returning a wrong value. The master KEK is resolved through a pluggable `IMasterKeyProvider` (environment variable, key file, or Windows DPAPI). ## Wiring it up ```csharp builder.Services.AddZbSecrets(builder.Configuration, "Secrets"); // After config load, BEFORE options validation — expand ${secret:...} references: var expander = new SecretReferenceExpander(app.Services.GetRequiredService()); await expander.ExpandConfigurationAsync((IConfigurationRoot)builder.Configuration, ct); ``` ```jsonc // appsettings.json "Secrets": { "SqlitePath": "secrets.db", "MasterKey": { "Source": "Environment", "EnvVarName": "ZB_SECRETS_MASTER_KEY" }, "RunMigrationsOnStartup": true, "ResolveCacheTtl": "00:00:30" }, // Keep plaintext out of config — reference a stored secret instead: "ConnectionStrings": { "Historian": "Server=...;User Id=sa;Password=${secret:sql/historian-password}" } ``` The master key is 32 bytes, provided base64 in `ZB_SECRETS_MASTER_KEY` (Environment source). A missing/invalid key fails closed at startup (`MasterKeyUnavailableException`). ## Runtime + human access - **App code:** inject `ISecretResolver` and call `GetAsync(name, ct)`. Every resolve is audited via `ZB.MOM.WW.Audit` (name + outcome only — the value is never logged). - **Operators:** mount the `ZB.MOM.WW.Secrets.Ui` `/admin/secrets` page. The list shows **metadata only**; revealing a value requires the `secrets:reveal` authorization policy and is audited. Add/rotate/delete require `secrets:manage`. ## Clustered deployments Secrets storage is local SQLite by default. The schema already carries the `revision` / `updated_utc` / tombstone columns and an `ISecretReplicator` seam for cluster replication, but the Akka replicator (`ZB.MOM.WW.Secrets.Akka`) is a deferred follow-on. **When replicating across a node pair, every node must resolve the _same_ master KEK** (e.g. the same mounted key file) — a row wrapped by an unknown KEK fails closed.