Files
scadaproj/docs/plans/2026-07-19-secrets-interactive-cli-design.md
T
2026-07-19 08:41:41 -04:00

5.8 KiB

Design: secret interactive console (ZB.MOM.WW.Secrets CLI, 0.3.0)

Date: 2026-07-19 · Status: Approved · Execution: git worktree worktree-secrets-interactive-cli (another agent is active on main)

1. Purpose & posture

Extend the existing headless secret CLI (src/ZB.MOM.WW.Secrets.Cli) with a menu-driven Spectre.Console TUI for two operator jobs:

  1. Deployment setup — seed a deployment's secrets before first startup (apps fail closed when a ${secret:...} reference is missing).
  2. Lockout recovery — diagnose and fix wrong/lost-KEK and missing-secret states.

No authentication: the tool only runs locally with direct file/DB access — the same trust model as the existing headless verbs. All existing verbs stay byte-identical for scripts.

2. Entry point

secret with no args in a real terminal (!Console.IsInputRedirected && !Console.IsOutputRedirected) launches the TUI. Redirected I/O keeps today's usage-and-exit-2 behavior, so nothing scriptable changes. All other Program.cs paths are untouched.

3. Target selection & sessions

First screen picks a store target:

  • Recent targets — persisted to ~/.zb-secrets/recent-targets.json (names and paths only; NEVER key material).
  • Browse to an app's appsettings.json.
  • Manual entry — db path + KEK source.

TargetConfigReader loads the app's real config the way the app would (appsettings.json + optional appsettings.<Environment>.json overlay + environment variables; section Secrets by default, overridable) and binds SecretsOptions (+ SqlServerSecretsOptions when present) — operating on exactly what the app will read at startup, so a fix cannot land in the wrong store.

SecretsSessionFactory constructs the stack directly — SecretsSqliteConnectionFactory / SqlServerSecretStore, MasterKeyProviderFactory, AesGcmEnvelopeCipher, the matching migrator — no per-target DI host. If the configured KEK env var isn't set in the CLI's shell (common: it exists only in the service's environment), the session opens degraded: metadata ops work; decrypt/seal ops prompt for the key via masked input or file path, held in memory only.

Dependency changes to ZB.MOM.WW.Secrets.Cli: add Spectre.Console and a project reference to ZB.MOM.WW.Secrets.Replicator.SqlServer (to open SQL-hub stores such as ScadaBridge central).

4. Menu actions

Action Behavior
List / set-rotate / get / delete Thin TUI wrappers over the same core seams SecretCommands uses. Masked value entry; tables for listings; get reveals once after confirm, never cached.
Reference audit + seeding Scan the target app's fully-composed config for ${secret:NAME} tokens (same regex as SecretReferenceExpander); report each OK / missing / tombstoned / undecryptable; walk through seeding every non-OK one with masked prompts. This is the deployment-setup flow.
KEK doctor Verify the session KEK against the store: per-row KekId match + actual DEK-unwrap probe; report row classes (OK / wrong-KEK / corrupt). Guided remedies: rewrap-all (old key from env/file/masked paste, reusing KekRotationService) or re-set affected secrets when the old KEK is truly lost. This is the lockout flow.
Bundle export / import Export rows as ciphertext-only JSON (name, metadata, wrapped DEK, kekId, ciphertext — safe at rest, useless without the KEK). Import into another store; conflicts resolved by core SecretLastWriterWins with per-row override prompt. Cross-KEK import offers inline rewrap given the source KEK.
Switch target / quit Rebuild session for a new target; quit exits 0.

5. Structure

New Interactive/ layer inside ZB.MOM.WW.Secrets.Cli:

  • InteractiveShell — menu loop; the ONLY class that touches IAnsiConsole.
  • UI-free services: TargetConfigReader, SecretsSessionFactory, ReferenceAuditor, KekDoctor, SecretBundleCodec + import/export service, RecentTargetsStore.

Services never touch the console; the shell never touches the store directly. Secret values are never written to recents, bundles (plaintext), logs, or screen except the explicit reveal action.

6. Error handling

Every action catches SecretDecryptionException / ArgumentException / IO / SQL failures and renders a red panel with a remedy hint (usually "run KEK doctor"), returning to the menu — the shell never crashes out mid-recovery. Ctrl-C at a prompt cancels back to the menu; at the menu it exits 0.

7. Testing

  • Unit tests for all services in tests/ZB.MOM.WW.Secrets.Tests/Cli/ (temp SQLite stores, fake KEK providers — same style as existing tests).
  • Shell flows scripted via Spectre.Console.Testing's TestConsole: menu navigation, masked entry, degraded-KEK path, audit→seed walk-through, doctor→rewrap.
  • Pin test: non-TTY no-args still prints usage / exit 2.
  • Suite stays 0-warning (TreatWarningsAsErrors posture of the repo).

8. Versioning & execution

  • Directory.Build.props version bumps 0.2.30.3.0 (new feature; CLI is unpacked but the 5 packages ship together — publish decision deferred to rollout).
  • Implemented on worktree branch worktree-secrets-interactive-cli; merge to main when the full suite is green.

Alternatives considered

  • Separate ZB.MOM.WW.Secrets.Cli.Interactive library — cleaner layering if another front-end ever hosts the flows; rejected as YAGNI for a one-exe operator tool.
  • Prompts inside SecretCommands — least code but couples UI into the tested headless layer and breaks its TextWriter-only contract; rejected.
  • REPL shell / wizards-only UX — rejected in favor of discoverable menus (operators under lockout stress shouldn't need to remember verbs).
  • Own profiles file as source of truth — rejected: can drift from what the app actually reads; parsing the app's own config is the honest source.