68 lines
3.3 KiB
Markdown
68 lines
3.3 KiB
Markdown
# 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<ISecretResolver>());
|
|
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.
|