feat(mcpd): pluggable SecretBackend + OpenBao driver + migrate #51

Merged
michal merged 1 commits from feat/secretbackend into main 2026-04-19 21:39:17 +00:00
Owner

Summary

Phase 0 of the larger Llm + SecretBackend plan: every API key in mcpctl lives in Postgres as plaintext JSON. Before centralising more credentials (LLM keys, etc.), we want a pluggable backend so the deployment can point at the cluster's existing OpenBao and drop DB-level access to sensitive rows.

  • New resource: SecretBackend with driver dispatch on type. Bootstrap row plaintext/default is seeded at startup; it's the trust root.
  • New driver: openbao — talks raw HTTP to KV v2 (also Vault-compatible). Auth via static token stored in a plaintext Secret, resolved once and cached. Broke the circular dep through a deferred secretResolverBridge in mcpd startup.
  • New migration CLI: mcpctl migrate secrets --from X --to Y — per-secret atomic, idempotent on restart, optional --dry-run and --keep-source.
  • RBAC: secretbackends resource + run:migrate-secrets operation.
  • CLI: create|get|describe|delete secretbackend (+ apply -f round-trip), shell completions regenerated, docs/secret-backends.md written.

Test plan

  • 11 new driver unit tests (plaintext + openbao write/read/404/delete/list/token cache/namespace)
  • SecretService/env-resolver/backup/secret-routes tests updated for new signatures
  • Full workspace suite: 1792/1792 passing
  • TypeScript clean across mcpd + cli
  • Shell completions test passes after regenerate
  • End-to-end: deploy to k8s, create an openbao backend pointing at the cluster's OpenBao, migrate existing secrets, verify k8s server pods still resolve env via the new backend

🤖 Generated with Claude Code

## Summary Phase 0 of the larger [Llm + SecretBackend plan](../../../../../.claude/plans/lets-discuss-something-i-bright-lovelace.md): every API key in mcpctl lives in Postgres as plaintext JSON. Before centralising more credentials (LLM keys, etc.), we want a pluggable backend so the deployment can point at the cluster's existing OpenBao and drop DB-level access to sensitive rows. - New resource: `SecretBackend` with driver dispatch on `type`. Bootstrap row `plaintext/default` is seeded at startup; it's the trust root. - New driver: `openbao` — talks raw HTTP to KV v2 (also Vault-compatible). Auth via static token stored in a plaintext `Secret`, resolved once and cached. Broke the circular dep through a deferred `secretResolverBridge` in mcpd startup. - New migration CLI: `mcpctl migrate secrets --from X --to Y` — per-secret atomic, idempotent on restart, optional `--dry-run` and `--keep-source`. - RBAC: `secretbackends` resource + `run:migrate-secrets` operation. - CLI: `create|get|describe|delete secretbackend` (+ `apply -f` round-trip), shell completions regenerated, docs/secret-backends.md written. ## Test plan - [x] 11 new driver unit tests (plaintext + openbao write/read/404/delete/list/token cache/namespace) - [x] SecretService/env-resolver/backup/secret-routes tests updated for new signatures - [x] Full workspace suite: **1792/1792 passing** - [x] TypeScript clean across mcpd + cli - [x] Shell completions test passes after regenerate - [ ] End-to-end: deploy to k8s, create an openbao backend pointing at the cluster's OpenBao, migrate existing secrets, verify k8s server pods still resolve env via the new backend 🤖 Generated with [Claude Code](https://claude.com/claude-code)
michal added 1 commit 2026-04-18 18:30:23 +00:00
feat(mcpd): pluggable SecretBackend abstraction + OpenBao driver + migrate
All checks were successful
CI/CD / typecheck (pull_request) Successful in 51s
CI/CD / lint (pull_request) Successful in 1m47s
CI/CD / test (pull_request) Successful in 1m3s
CI/CD / smoke (pull_request) Successful in 4m34s
CI/CD / build (pull_request) Successful in 3m50s
CI/CD / publish (pull_request) Has been skipped
029c3d5f34
Why: API keys live in Postgres as plaintext JSON. A DB read exposes every
credential in the system. Before centralising more secrets (LLM keys, etc.)
we want to be able to point at an external KV store and drop DB access to
sensitive rows.

New model:
- `SecretBackend` resource (CRUD + isDefault invariant) owns how a secret is
  stored. `Secret` gains `backendId` FK and `externalRef`. Reads/writes
  dispatch through a driver.
- `plaintext` driver (near-noop, uses existing Secret.data column) is seeded
  as the `default` row at startup. Acts as trust root / bootstrap.
- `openbao` driver (also HashiCorp Vault KV v2 compatible) talks plain HTTP,
  no SDK dependency. Auth via static token pulled from a plaintext-backed
  `Secret` through the injected SecretRefResolver. Caches resolved token.
- `SecretMigrateService` moves secrets one-at-a-time: read → write dest →
  flip row → best-effort source delete. Interrupted runs are idempotent
  (skips secrets already on destination).

CLI surface:
- `mcpctl create|get|describe|delete secretbackend` + `--default` on create.
- `mcpctl migrate secrets --from X --to Y [--names a,b] [--keep-source] [--dry-run]`
- `apply -f` round-trips secretbackends (yaml/json multi-doc + grouped).
- RBAC: `secretbackends` resource + `run:migrate-secrets` operation.
- Fish + bash completions regenerated.

docs/secret-backends.md covers the OpenBao policy, chicken-and-egg auth flow,
and the migration semantics.

Broke the circular dep (OpenBao needs SecretService to resolve its own token,
SecretService needs SecretBackendService) with a deferred-resolver bridge in
mcpd startup. 11 new driver unit tests; existing env-resolver/secret-route/
backup tests updated for the new service signatures. Full suite: 1792/1792.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
michal merged commit 97ac1e75ef into main 2026-04-19 21:39:17 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: michal/mcpctl#51